From 04519d1b0a086a31d918d9453971880d69fd26e3 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Thu, 29 May 2025 14:57:02 +0530 Subject: [PATCH 01/24] Added migrations for component_permissions and component_users table --- ...748509644056-CreateComponentPermissions.ts | 51 +++++++++++++ .../1748509665915-CreateComponentUsers.ts | 76 +++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 server/migrations/1748509644056-CreateComponentPermissions.ts create mode 100644 server/migrations/1748509665915-CreateComponentUsers.ts diff --git a/server/migrations/1748509644056-CreateComponentPermissions.ts b/server/migrations/1748509644056-CreateComponentPermissions.ts new file mode 100644 index 0000000000..baa66f01de --- /dev/null +++ b/server/migrations/1748509644056-CreateComponentPermissions.ts @@ -0,0 +1,51 @@ +import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm'; + +export class CreateComponentPermissions1748509644056 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: 'component_permissions', + columns: [ + { + name: 'id', + type: 'uuid', + isGenerated: true, + default: 'gen_random_uuid()', + isPrimary: true, + }, + { + name: 'component_id', + type: 'uuid', + isNullable: false, + }, + { + name: 'type', + type: 'enum', + enum: ['SINGLE', 'GROUP'], + }, + { + name: 'created_at', + type: 'timestamp', + isNullable: false, + default: 'now()', + }, + ], + }), + true + ); + + await queryRunner.createForeignKey( + 'component_permissions', + new TableForeignKey({ + columnNames: ['component_id'], + referencedColumnNames: ['id'], + referencedTableName: 'components', + onDelete: 'CASCADE', + }) + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable('component_permissions'); + } +} diff --git a/server/migrations/1748509665915-CreateComponentUsers.ts b/server/migrations/1748509665915-CreateComponentUsers.ts new file mode 100644 index 0000000000..dbc4e3ef45 --- /dev/null +++ b/server/migrations/1748509665915-CreateComponentUsers.ts @@ -0,0 +1,76 @@ +import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm'; + +export class CreateComponentUsers1748509665915 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: 'component_users', + columns: [ + { + name: 'id', + type: 'uuid', + isGenerated: true, + default: 'gen_random_uuid()', + isPrimary: true, + }, + { + name: 'component_permissions_id', + type: 'uuid', + isNullable: false, + }, + { + name: 'user_id', + type: 'uuid', + isNullable: true, + }, + { + name: 'permission_groups_id', + type: 'uuid', + isNullable: true, + }, + { + name: 'created_at', + type: 'timestamp', + isNullable: false, + default: 'now()', + }, + ], + }), + true + ); + + await queryRunner.createForeignKey( + 'component_users', + new TableForeignKey({ + columnNames: ['component_permissions_id'], + referencedColumnNames: ['id'], + referencedTableName: 'component_permissions', + onDelete: 'CASCADE', + }) + ); + + await queryRunner.createForeignKey( + 'component_users', + new TableForeignKey({ + columnNames: ['user_id'], + referencedColumnNames: ['id'], + referencedTableName: 'users', + onDelete: 'CASCADE', + }) + ); + + await queryRunner.createForeignKey( + 'component_users', + new TableForeignKey({ + columnNames: ['permission_groups_id'], + referencedColumnNames: ['id'], + referencedTableName: 'permission_groups', + onDelete: 'CASCADE', + }) + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable('component_users'); + } +} From 2e3f52335ada81d7f23146b86ae604e363528b9e Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Thu, 29 May 2025 17:17:24 +0530 Subject: [PATCH 02/24] Added the required entities for component_permissions and component_users tables --- server/src/entities/component.entity.ts | 4 +++ .../entities/component_permissions.entity.ts | 29 ++++++++++++++++ server/src/entities/component_users.entity.ts | 34 +++++++++++++++++++ .../src/entities/group_permissions.entity.ts | 4 +++ server/src/entities/user.entity.ts | 4 +++ 5 files changed, 75 insertions(+) create mode 100644 server/src/entities/component_permissions.entity.ts create mode 100644 server/src/entities/component_users.entity.ts diff --git a/server/src/entities/component.entity.ts b/server/src/entities/component.entity.ts index bb22c7dcd4..6ef8d20b0c 100644 --- a/server/src/entities/component.entity.ts +++ b/server/src/entities/component.entity.ts @@ -11,6 +11,7 @@ import { } from 'typeorm'; import { Page } from './page.entity'; import { Layout } from './layout.entity'; +import { ComponentPermission } from './component_permissions.entity'; @Entity({ name: 'components' }) @Index('idx_component_page_id', ['pageId']) @@ -60,4 +61,7 @@ export class Component { @OneToMany(() => Layout, (layout) => layout.component) layouts: Layout[]; + + @OneToMany(() => ComponentPermission, (permission) => permission.component) + permissions: ComponentPermission[]; } diff --git a/server/src/entities/component_permissions.entity.ts b/server/src/entities/component_permissions.entity.ts new file mode 100644 index 0000000000..9ed804eb4e --- /dev/null +++ b/server/src/entities/component_permissions.entity.ts @@ -0,0 +1,29 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, OneToMany } from 'typeorm'; +import { Component } from './component.entity'; +import { PAGE_PERMISSION_TYPE } from '@modules/app-permissions/constants'; +import { ComponentUser } from './component_users.entity'; + +@Entity('component_permissions') +export class ComponentPermission { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'component_id', type: 'uuid', nullable: false }) + componentId: string; + + @Column({ + type: 'enum', + enum: PAGE_PERMISSION_TYPE, + }) + type: PAGE_PERMISSION_TYPE; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @ManyToOne(() => Component, (component) => component.permissions, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'component_id' }) + component: Component; + + @OneToMany(() => ComponentUser, (componentUser) => componentUser.componentPermission) + users: ComponentUser[]; +} diff --git a/server/src/entities/component_users.entity.ts b/server/src/entities/component_users.entity.ts new file mode 100644 index 0000000000..2728297e3b --- /dev/null +++ b/server/src/entities/component_users.entity.ts @@ -0,0 +1,34 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn } from 'typeorm'; +import { User } from './user.entity'; +import { ComponentPermission } from './component_permissions.entity'; +import { GroupPermissions } from './group_permissions.entity'; + +@Entity('component_users') +export class ComponentUser { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'component_permissions_id', type: 'uuid' }) + componentPermissionsId: string; + + @Column({ name: 'user_id', type: 'uuid', nullable: true }) + userId: string | null; + + @Column({ name: 'permission_groups_id', type: 'uuid', nullable: true }) + permissionGroupsId: string | null; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @ManyToOne(() => ComponentPermission, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'component_permissions_id' }) + componentPermission: ComponentPermission; + + @ManyToOne(() => User, { onDelete: 'CASCADE', nullable: true }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @ManyToOne(() => GroupPermissions, { onDelete: 'CASCADE', nullable: true }) + @JoinColumn({ name: 'permission_groups_id' }) + permissionGroup: GroupPermissions; +} diff --git a/server/src/entities/group_permissions.entity.ts b/server/src/entities/group_permissions.entity.ts index 0ff6e47b0b..dfc6256ebd 100644 --- a/server/src/entities/group_permissions.entity.ts +++ b/server/src/entities/group_permissions.entity.ts @@ -15,6 +15,7 @@ import { GranularPermissions } from './granular_permissions.entity'; import { GROUP_PERMISSIONS_TYPE } from '@modules/group-permissions/constants'; import { PageUser } from './page_users.entity'; import { QueryUser } from './query_users.entity'; +import { ComponentUser } from './component_users.entity'; @Entity({ name: 'permission_groups' }) export class GroupPermissions extends BaseEntity { @@ -70,5 +71,8 @@ export class GroupPermissions extends BaseEntity { @OneToMany(() => QueryUser, (queryUser) => queryUser.permissionGroup) queryUsers: QueryUser[]; + @OneToMany(() => ComponentUser, (componentUser) => componentUser.permissionGroup) + componentUsers: ComponentUser[]; + disabled?: boolean; } diff --git a/server/src/entities/user.entity.ts b/server/src/entities/user.entity.ts index 515b1f469e..f43396f0ed 100644 --- a/server/src/entities/user.entity.ts +++ b/server/src/entities/user.entity.ts @@ -31,6 +31,7 @@ import { AiResponseVote } from './ai_response_vote.entity'; import { USER_ROLE } from '@modules/group-permissions/constants'; import { PageUser } from './page_users.entity'; import { QueryUser } from './query_users.entity'; +import { ComponentUser } from './component_users.entity'; @Entity({ name: 'users' }) export class User extends BaseEntity { @@ -192,6 +193,9 @@ export class User extends BaseEntity { @OneToMany(() => QueryUser, (queryUser) => queryUser.user) queryUsers: QueryUser[]; + @OneToMany(() => ComponentUser, (componentUser) => componentUser.user) + componentUsers: ComponentUser[]; + organizationId: string; invitedOrganizationId: string; organizationIds?: Array; From 3a7f4be4bb223d727bad916f821e12591b2750bf Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Thu, 29 May 2025 17:50:04 +0530 Subject: [PATCH 03/24] Created base for API endpoint and feature keys --- .../modules/app-permissions/ability/index.ts | 9 ++++ .../app-permissions/constants/features.ts | 4 ++ .../app-permissions/constants/index.ts | 4 ++ .../src/modules/app-permissions/controller.ts | 46 +++++++++++++++++++ .../app-permissions/interfaces/IController.ts | 20 ++++++++ .../modules/app-permissions/types/index.ts | 4 ++ 6 files changed, 87 insertions(+) diff --git a/server/src/modules/app-permissions/ability/index.ts b/server/src/modules/app-permissions/ability/index.ts index 5a03f417d5..81b25601b1 100644 --- a/server/src/modules/app-permissions/ability/index.ts +++ b/server/src/modules/app-permissions/ability/index.ts @@ -42,6 +42,10 @@ export class FeatureAbilityFactory extends AbilityFactory FEATURE_KEY.CREATE_QUERY_PERMISSIONS, FEATURE_KEY.UPDATE_QUERY_PERMISSIONS, FEATURE_KEY.DELETE_QUERY_PERMISSIONS, + FEATURE_KEY.FETCH_COMPONENT_PERMISSIONS, + FEATURE_KEY.CREATE_COMPONENT_PERMISSIONS, + FEATURE_KEY.UPDATE_COMPONENT_PERMISSIONS, + FEATURE_KEY.DELETE_COMPONENT_PERMISSIONS, ], App ); @@ -64,6 +68,10 @@ export class FeatureAbilityFactory extends AbilityFactory FEATURE_KEY.CREATE_QUERY_PERMISSIONS, FEATURE_KEY.UPDATE_QUERY_PERMISSIONS, FEATURE_KEY.DELETE_QUERY_PERMISSIONS, + FEATURE_KEY.FETCH_COMPONENT_PERMISSIONS, + FEATURE_KEY.CREATE_COMPONENT_PERMISSIONS, + FEATURE_KEY.UPDATE_COMPONENT_PERMISSIONS, + FEATURE_KEY.DELETE_COMPONENT_PERMISSIONS, ], App ); @@ -80,6 +88,7 @@ export class FeatureAbilityFactory extends AbilityFactory FEATURE_KEY.FETCH_USER_GROUPS, FEATURE_KEY.FETCH_PAGE_PERMISSIONS, FEATURE_KEY.FETCH_QUERY_PERMISSIONS, + FEATURE_KEY.FETCH_COMPONENT_PERMISSIONS, ], App ); diff --git a/server/src/modules/app-permissions/constants/features.ts b/server/src/modules/app-permissions/constants/features.ts index 360b1cf4c9..cd992b88aa 100644 --- a/server/src/modules/app-permissions/constants/features.ts +++ b/server/src/modules/app-permissions/constants/features.ts @@ -14,5 +14,9 @@ export const FEATURES: FeaturesConfig = { [FEATURE_KEY.CREATE_QUERY_PERMISSIONS]: {}, [FEATURE_KEY.UPDATE_QUERY_PERMISSIONS]: {}, [FEATURE_KEY.DELETE_QUERY_PERMISSIONS]: {}, + [FEATURE_KEY.FETCH_COMPONENT_PERMISSIONS]: {}, + [FEATURE_KEY.CREATE_COMPONENT_PERMISSIONS]: {}, + [FEATURE_KEY.UPDATE_COMPONENT_PERMISSIONS]: {}, + [FEATURE_KEY.DELETE_COMPONENT_PERMISSIONS]: {}, }, }; diff --git a/server/src/modules/app-permissions/constants/index.ts b/server/src/modules/app-permissions/constants/index.ts index ff5063e948..14e7122e2f 100644 --- a/server/src/modules/app-permissions/constants/index.ts +++ b/server/src/modules/app-permissions/constants/index.ts @@ -21,4 +21,8 @@ export enum FEATURE_KEY { CREATE_QUERY_PERMISSIONS = 'create_query_permissions', UPDATE_QUERY_PERMISSIONS = 'update_query_permissions', DELETE_QUERY_PERMISSIONS = 'delete_query_permissions', + FETCH_COMPONENT_PERMISSIONS = 'fetch_component_permissions', + CREATE_COMPONENT_PERMISSIONS = 'create_component_permissions', + UPDATE_COMPONENT_PERMISSIONS = 'update_component_permissions', + DELETE_COMPONENT_PERMISSIONS = 'delete_component_permissions', } diff --git a/server/src/modules/app-permissions/controller.ts b/server/src/modules/app-permissions/controller.ts index d317e3115b..65bdaf017d 100644 --- a/server/src/modules/app-permissions/controller.ts +++ b/server/src/modules/app-permissions/controller.ts @@ -127,4 +127,50 @@ export class AppPermissionsController implements IAppPermissionsController { ): Promise { throw new NotFoundException(); } + + @InitFeature(FEATURE_KEY.FETCH_COMPONENT_PERMISSIONS) + @Get(':appId/components/:componentId') + async fetchComponentPermissions( + @User() user, + @Param('appId') appId: string, + @Param('componentId') componentId: string, + @Res({ passthrough: true }) response: Response + ): Promise { + throw new NotFoundException(); + } + + @InitFeature(FEATURE_KEY.CREATE_COMPONENT_PERMISSIONS) + @Post(':appId/components/:componentId') + async createComponentPermissions( + @User() user, + @Param('appId') appId: string, + @Param('componentId') componentId: string, + @Body() body: CreatePermissionDto, + @Res({ passthrough: true }) response: Response + ): Promise { + throw new NotFoundException(); + } + + @InitFeature(FEATURE_KEY.UPDATE_COMPONENT_PERMISSIONS) + @Put(':appId/components/:componentId') + async updateComponentPermissions( + @User() user, + @Param('appId') appId: string, + @Param('componentId') componentId: string, + @Body() body: CreatePermissionDto, + @Res({ passthrough: true }) response: Response + ): Promise { + throw new NotFoundException(); + } + + @InitFeature(FEATURE_KEY.DELETE_COMPONENT_PERMISSIONS) + @Delete(':appId/components/:componentId') + async deleteComponentPermissions( + @User() user, + @Param('appId') appId: string, + @Param('componentId') componentId: string, + @Res({ passthrough: true }) response: Response + ): Promise { + throw new NotFoundException(); + } } diff --git a/server/src/modules/app-permissions/interfaces/IController.ts b/server/src/modules/app-permissions/interfaces/IController.ts index a7808d280d..e9b9cdc24e 100644 --- a/server/src/modules/app-permissions/interfaces/IController.ts +++ b/server/src/modules/app-permissions/interfaces/IController.ts @@ -46,4 +46,24 @@ export interface IAppPermissionsController { ): Promise; deleteQueryPermissions(user: User, appId: string, queryId: string, response: Response): Promise; + + fetchComponentPermissions(user: User, appId: string, componentId: string, response: Response): Promise; + + createComponentPermissions( + user: User, + appId: string, + componentId: string, + body: CreatePermissionDto, + response: Response + ): Promise; + + updateComponentPermissions( + user: User, + appId: string, + componentId: string, + body: CreatePermissionDto, + response: Response + ): Promise; + + deleteComponentPermissions(user: User, appId: string, componentId: string, response: Response): Promise; } diff --git a/server/src/modules/app-permissions/types/index.ts b/server/src/modules/app-permissions/types/index.ts index d377f5a08f..5e3acf64b4 100644 --- a/server/src/modules/app-permissions/types/index.ts +++ b/server/src/modules/app-permissions/types/index.ts @@ -13,6 +13,10 @@ interface Features { [FEATURE_KEY.CREATE_QUERY_PERMISSIONS]: FeatureConfig; [FEATURE_KEY.UPDATE_QUERY_PERMISSIONS]: FeatureConfig; [FEATURE_KEY.DELETE_QUERY_PERMISSIONS]: FeatureConfig; + [FEATURE_KEY.FETCH_COMPONENT_PERMISSIONS]: FeatureConfig; + [FEATURE_KEY.CREATE_COMPONENT_PERMISSIONS]: FeatureConfig; + [FEATURE_KEY.UPDATE_COMPONENT_PERMISSIONS]: FeatureConfig; + [FEATURE_KEY.DELETE_COMPONENT_PERMISSIONS]: FeatureConfig; } export interface FeaturesConfig { From 5423bbe687abbad3f3186beb01c8e74ddd0e68ec Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Fri, 30 May 2025 02:27:53 +0530 Subject: [PATCH 04/24] Implemented business logic for Component Permissions API --- server/ee | 2 +- .../interfaces/IUtilService.ts | 4 + server/src/modules/app-permissions/module.ts | 17 +++- .../component-permissions.repository.ts | 58 +++++++++++++ .../component-users.repository.ts | 83 +++++++++++++++++++ .../modules/app-permissions/util.service.ts | 8 ++ 6 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 server/src/modules/app-permissions/repositories/component-permissions.repository.ts create mode 100644 server/src/modules/app-permissions/repositories/component-users.repository.ts diff --git a/server/ee b/server/ee index dd0462d3c3..5865a18fe8 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit dd0462d3c3acf4b8e6856209eee52bc3cc78a02e +Subproject commit 5865a18fe87e545b9b6a8f40f998117edc403c3f diff --git a/server/src/modules/app-permissions/interfaces/IUtilService.ts b/server/src/modules/app-permissions/interfaces/IUtilService.ts index dbd390982a..a38d845076 100644 --- a/server/src/modules/app-permissions/interfaces/IUtilService.ts +++ b/server/src/modules/app-permissions/interfaces/IUtilService.ts @@ -14,4 +14,8 @@ export interface IUtilService { createQueryPermission(queryId: string, body: CreatePermissionDto): Promise; updateQueryPermission(queryId: string, body: CreatePermissionDto): Promise; + + createComponentPermission(componentId: string, body: CreatePermissionDto): Promise; + + updateComponentPermission(componentId: string, body: CreatePermissionDto): Promise; } diff --git a/server/src/modules/app-permissions/module.ts b/server/src/modules/app-permissions/module.ts index 5e3e3db107..b290bd5dbc 100644 --- a/server/src/modules/app-permissions/module.ts +++ b/server/src/modules/app-permissions/module.ts @@ -9,10 +9,14 @@ import { PageUsersRepository } from './repositories/page-users.repository'; import { PagePermissionsRepository } from './repositories/page-permissions.repository'; import { QueryUsersRepository } from './repositories/query-users.repository'; import { QueryPermissionsRepository } from './repositories/query-permissions.repository'; +import { ComponentUsersRepository } from './repositories/component-users.repository'; +import { ComponentPermissionsRepository } from './repositories/component-permissions.repository'; import { PageUser } from '@entities/page_users.entity'; import { PagePermission } from '@entities/page_permissions.entity'; import { QueryUser } from '@entities/query_users.entity'; import { QueryPermission } from '@entities/query_permissions.entity'; +import { ComponentUser } from '@entities/component_users.entity'; +import { ComponentPermission } from '@entities/component_permissions.entity'; export class AppPermissionsModule { static async register(configs: { IS_GET_CONTEXT: boolean }): Promise { @@ -24,7 +28,16 @@ export class AppPermissionsModule { return { module: AppPermissionsModule, imports: [ - TypeOrmModule.forFeature([GroupPermissions, User, PageUser, PagePermission, QueryUser, QueryPermission]), + TypeOrmModule.forFeature([ + GroupPermissions, + User, + PageUser, + PagePermission, + QueryUser, + QueryPermission, + ComponentUser, + ComponentPermission, + ]), ], controllers: [AppPermissionsController], providers: [ @@ -35,6 +48,8 @@ export class AppPermissionsModule { PagePermissionsRepository, QueryUsersRepository, QueryPermissionsRepository, + ComponentUsersRepository, + ComponentPermissionsRepository, FeatureAbilityFactory, ], exports: [AppPermissionsUtilService, AppPermissionsService], diff --git a/server/src/modules/app-permissions/repositories/component-permissions.repository.ts b/server/src/modules/app-permissions/repositories/component-permissions.repository.ts new file mode 100644 index 0000000000..802dd12868 --- /dev/null +++ b/server/src/modules/app-permissions/repositories/component-permissions.repository.ts @@ -0,0 +1,58 @@ +import { ComponentPermission } from '@entities/component_permissions.entity'; +import { Injectable } from '@nestjs/common'; +import { DataSource, EntityManager, Repository } from 'typeorm'; +import { ComponentUsersRepository } from './component-users.repository'; +import { dbTransactionWrap } from '@helpers/database.helper'; +import { PAGE_PERMISSION_TYPE } from '../constants'; + +@Injectable() +export class ComponentPermissionsRepository extends Repository { + constructor(private dataSource: DataSource, private readonly componentUsersRepository: ComponentUsersRepository) { + super(ComponentPermission, dataSource.createEntityManager()); + } + + async getComponentPermissions(componentId: string, manager?: EntityManager): Promise { + return dbTransactionWrap(async (manager: EntityManager) => { + const componentPermissions = await manager.find(ComponentPermission, { + where: { componentId }, + relations: ['users', 'users.user', 'users.permissionGroup'], + }); + + return componentPermissions.map((permission) => { + if (permission.type === PAGE_PERMISSION_TYPE.GROUP) { + return { + ...permission, + groups: permission.users, + users: undefined, + }; + } + return permission; + }); + }, manager || this.manager); + } + + async createComponentPermissions( + componentId: string, + type: PAGE_PERMISSION_TYPE, + manager?: EntityManager + ): Promise { + return dbTransactionWrap(async (manager: EntityManager) => { + const existingPermission = await manager.findOne(ComponentPermission, { where: { componentId } }); + if (existingPermission) { + throw new Error(`Component permission already exists for Component id: ${componentId}`); + } + + const componentPermission = manager.create(ComponentPermission, { + componentId, + type, + }); + return manager.save(componentPermission); + }, manager || this.manager); + } + + async deleteComponentPermissions(componentId: string, manager?: EntityManager): Promise { + return dbTransactionWrap(async (manager: EntityManager) => { + await manager.delete(ComponentPermission, { componentId }); + }, manager || this.manager); + } +} diff --git a/server/src/modules/app-permissions/repositories/component-users.repository.ts b/server/src/modules/app-permissions/repositories/component-users.repository.ts new file mode 100644 index 0000000000..98478b4334 --- /dev/null +++ b/server/src/modules/app-permissions/repositories/component-users.repository.ts @@ -0,0 +1,83 @@ +import { ComponentUser } from '@entities/component_users.entity'; +import { Injectable } from '@nestjs/common'; +import { DataSource, EntityManager, Repository } from 'typeorm'; +import { dbTransactionWrap } from '@helpers/database.helper'; +import { ComponentPermission } from '@entities/component_permissions.entity'; + +@Injectable() +export class ComponentUsersRepository extends Repository { + constructor(private dataSource: DataSource) { + super(ComponentUser, dataSource.createEntityManager()); + } + + async createComponentUsersWithSingle( + componentPermissionsId: string, + users: string[], + manager?: EntityManager + ): Promise { + return dbTransactionWrap(async (manager: EntityManager) => { + const componentUsers = users.map((userId) => { + return manager.create(ComponentUser, { + componentPermissionsId, + userId, + permissionGroupsId: null, + }); + }); + return manager.save(componentUsers); + }, manager || this.manager); + } + + async createComponentUsersWithGroup( + componentPermissionsId: string, + groups: string[], + manager?: EntityManager + ): Promise { + return dbTransactionWrap(async (manager: EntityManager) => { + const componentUsers = groups.map((permissionGroupsId) => { + return manager.create(ComponentUser, { + componentPermissionsId, + permissionGroupsId, + userId: null, + }); + }); + return manager.save(componentUsers); + }, manager || this.manager); + } + + async checkComponentUserWithGroup( + componentPermission: ComponentPermission, + userId: string, + manager?: EntityManager + ): Promise { + return dbTransactionWrap(async (manager: EntityManager) => { + const result = await manager + .createQueryBuilder(ComponentUser, 'component_users') + .innerJoin('component_users.permissionGroup', 'group') + .innerJoin('group.groupUsers', 'groupUser') + .where('component_users.componentPermission = :permissionId', { + permissionId: componentPermission.id, + }) + .andWhere('groupUser.userId = :userId', { userId }) + .getOne(); + + return !!result; + }, manager || this.manager); + } + + async checkComponentUserWithSingle( + componentPermission: ComponentPermission, + userId: string, + manager?: EntityManager + ): Promise { + return dbTransactionWrap(async (manager: EntityManager) => { + const componentUser = await manager.findOne(ComponentUser, { + where: { + componentPermission: { id: componentPermission.id }, + userId, + }, + }); + + return !!componentUser; + }, manager || this.manager); + } +} diff --git a/server/src/modules/app-permissions/util.service.ts b/server/src/modules/app-permissions/util.service.ts index 7ff894a058..55d927eebb 100644 --- a/server/src/modules/app-permissions/util.service.ts +++ b/server/src/modules/app-permissions/util.service.ts @@ -31,4 +31,12 @@ export class AppPermissionsUtilService implements IUtilService { async updateQueryPermission(queryId: string, body: CreatePermissionDto): Promise { throw new Error('Method not implemented.'); } + + async createComponentPermission(componentId: string, body: CreatePermissionDto): Promise { + throw new Error('Method not implemented.'); + } + + async updateComponentPermission(componentId: string, body: CreatePermissionDto): Promise { + throw new Error('Method not implemented.'); + } } From 827c1ba7697b42d783b595b3d75e2619336bba76 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Fri, 30 May 2025 11:39:56 +0530 Subject: [PATCH 05/24] Added filter logic for preview and released apps --- server/ee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/ee b/server/ee index 5865a18fe8..78d8d151ae 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit 5865a18fe87e545b9b6a8f40f998117edc403c3f +Subproject commit 78d8d151ae6be18fa76bf46fdf25ffa7dec7a643 From cf30360b3db083b71906cd638559ad7579274de8 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Fri, 30 May 2025 14:13:29 +0530 Subject: [PATCH 06/24] Implemented versioning logic for component permissions --- server/ee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/ee b/server/ee index 78d8d151ae..eabc2cc32a 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit 78d8d151ae6be18fa76bf46fdf25ffa7dec7a643 +Subproject commit eabc2cc32afac238c3f7c01198bf049af4cf3b60 From bc0d4b79d3d43073a207de53864d38bc6c2af68d Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Fri, 30 May 2025 15:38:55 +0530 Subject: [PATCH 07/24] Implemented import-export logic --- .../services/app-import-export.service.ts | 75 ++++++++++++++++++- .../import-export-resources/service.ts | 10 ++- 2 files changed, 81 insertions(+), 4 deletions(-) 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 6522690675..9b4c1be177 100644 --- a/server/src/modules/apps/services/app-import-export.service.ts +++ b/server/src/modules/apps/services/app-import-export.service.ts @@ -41,6 +41,8 @@ import { PageUser } from '@entities/page_users.entity'; import { UsersUtilService } from '@modules/users/util.service'; import { QueryPermission } from '@entities/query_permissions.entity'; import { QueryUser } from '@entities/query_users.entity'; +import { ComponentPermission } from '@entities/component_permissions.entity'; +import { ComponentUser } from '@entities/component_users.entity'; interface AppResourceMappings { defaultDataSourceIdMapping: Record; dataQueryMapping: Record; @@ -241,6 +243,9 @@ export class AppImportExportService { ? await manager .createQueryBuilder(Component, 'components') .leftJoinAndSelect('components.layouts', 'layouts') + .leftJoinAndSelect('components.permissions', 'permission') + .leftJoinAndSelect('permission.users', 'componentUser') + .leftJoinAndSelect('componentUser.permissionGroup', 'permissionGroup') .where('components.pageId IN(:...pageId)', { pageId: pages.map((v) => v.id), }) @@ -248,6 +253,21 @@ export class AppImportExportService { .getMany() : []; + const componentsWithPermissionGroups = components.map((component) => { + const groupPermission = component.permissions.find((perm) => perm.type === 'GROUP'); + + return { + ...component, + permissions: groupPermission + ? { + permissionGroup: groupPermission.users + .map((user) => user.permissionGroup?.name) + .filter((name): name is string => Boolean(name)), + } + : undefined, + }; + }); + const events = await manager .createQueryBuilder(EventHandler, 'event_handlers') .where('event_handlers.appVersionId IN(:...versionId)', { @@ -256,7 +276,7 @@ export class AppImportExportService { .orderBy('event_handlers.created_at', 'ASC') .getMany(); - appToExport['components'] = components; + appToExport['components'] = componentsWithPermissionGroups; appToExport['pages'] = pagesWithPermissionGroups; appToExport['events'] = events; appToExport['dataQueries'] = queriesWithPermissionGroups; @@ -950,6 +970,13 @@ export class AppImportExportService { await manager.save(newLayout); }); + if (component.permissions) { + savedComponent.permissions = component.permissions; + } + + //create component permissions of component if flag enabled in dto + await this.createComponentPermissionsForGroups(savedComponent, user.organizationId, manager); + const componentEvents = importingEvents.filter((event) => event.sourceId === component.id); if (componentEvents.length > 0) { @@ -1386,7 +1413,7 @@ export class AppImportExportService { return pageSettings; } - async checkIfGroupPermissionsExist(pages, queries, organizationId) { + async checkIfGroupPermissionsExist(pages, queries, components, organizationId) { const allGroupNames = new Set(); for (const page of pages) { @@ -1405,6 +1432,15 @@ export class AppImportExportService { } } + for (const component of components) { + const groupNames = component.permissions?.permissionGroup || []; + for (const name of groupNames) { + if (!allGroupNames.has(name)) { + allGroupNames.add(name); + } + } + } + if (!allGroupNames.size) return; return await dbTransactionWrap(async (manager: EntityManager) => { @@ -1500,6 +1536,41 @@ export class AppImportExportService { await manager.save(queryUsers); } + async createComponentPermissionsForGroups(component, organizationId: string, manager: EntityManager) { + const groupNames = component.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(ComponentPermission, { + componentId: component.id, + type: PAGE_PERMISSION_TYPE.GROUP, + }); + + const savedPermission = await manager.save(permission); + + const componentUsers = validGroupNames.map((name) => + manager.create(ComponentUser, { + componentPermissionsId: savedPermission.id, + permissionGroupsId: groupMap.get(name).id, + }) + ); + + await manager.save(componentUsers); + } + async createAppVersionsForImportedApp( manager: EntityManager, user: User, diff --git a/server/src/modules/import-export-resources/service.ts b/server/src/modules/import-export-resources/service.ts index 7b50ca6076..a83b3f766e 100644 --- a/server/src/modules/import-export-resources/service.ts +++ b/server/src/modules/import-export-resources/service.ts @@ -78,8 +78,14 @@ export class ImportExportResourcesService { appParams = { ...appParams.appV2 }; const pages = appParams?.pages; const queries = appParams?.dataQueries; - (pages?.length || queries?.length) && - (await this.appImportExportService.checkIfGroupPermissionsExist(pages, queries, user.organizationId)); + const components = appParams?.components; + (pages?.length || queries?.length || components?.length) && + (await this.appImportExportService.checkIfGroupPermissionsExist( + pages, + queries, + components, + user.organizationId + )); } } } From a79ffd58040987aed363a856d1f032fe0e84c4c7 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Fri, 30 May 2025 15:42:04 +0530 Subject: [PATCH 08/24] Create service functions for component permissions --- .../src/_services/appPermission.service.js | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/frontend/src/_services/appPermission.service.js b/frontend/src/_services/appPermission.service.js index fd2438f058..1aab6e97e1 100644 --- a/frontend/src/_services/appPermission.service.js +++ b/frontend/src/_services/appPermission.service.js @@ -11,6 +11,10 @@ export const appPermissionService = { createQueryPermission, updateQueryPermission, deleteQueryPermission, + getComponentPermission, + createComponentPermission, + updateComponentPermission, + deleteComponentPermission, }; function getPagePermission(appId, pageId) { @@ -89,3 +93,49 @@ function deleteQueryPermission(appId, queryId) { }; return fetch(`${config.apiUrl}/app-permissions/${appId}/queries/${queryId}`, requestOptions).then(handleResponse); } + +function getComponentPermission(appId, componentId) { + const requestOptions = { + method: 'GET', + headers: authHeader(), + credentials: 'include', + }; + return fetch(`${config.apiUrl}/app-permissions/${appId}/components/${componentId}`, requestOptions).then( + handleResponse + ); +} + +function createComponentPermission(appId, componentId, body) { + const requestOptions = { + method: 'POST', + headers: authHeader(), + credentials: 'include', + body: JSON.stringify(body), + }; + return fetch(`${config.apiUrl}/app-permissions/${appId}/components/${componentId}`, requestOptions).then( + handleResponse + ); +} + +function updateComponentPermission(appId, componentId, body) { + const requestOptions = { + method: 'PUT', + headers: authHeader(), + credentials: 'include', + body: JSON.stringify(body), + }; + return fetch(`${config.apiUrl}/app-permissions/${appId}/components/${componentId}`, requestOptions).then( + handleResponse + ); +} + +function deleteComponentPermission(appId, componentId) { + const requestOptions = { + method: 'DELETE', + headers: authHeader(), + credentials: 'include', + }; + return fetch(`${config.apiUrl}/app-permissions/${appId}/components/${componentId}`, requestOptions).then( + handleResponse + ); +} From 2261e96f2e27449c668914cd700c39f616facf08 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Fri, 30 May 2025 18:18:00 +0530 Subject: [PATCH 09/24] Get permissions data with components in Editor --- server/ee | 2 +- server/src/modules/apps/interfaces/services/IPageService.ts | 2 +- server/src/modules/apps/services/page.service.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/ee b/server/ee index eabc2cc32a..adc0f3471b 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit eabc2cc32afac238c3f7c01198bf049af4cf3b60 +Subproject commit adc0f3471bfef26be3ea5d69972bcf2e6c8c46cb diff --git a/server/src/modules/apps/interfaces/services/IPageService.ts b/server/src/modules/apps/interfaces/services/IPageService.ts index f8f8fed9eb..9f06d02719 100644 --- a/server/src/modules/apps/interfaces/services/IPageService.ts +++ b/server/src/modules/apps/interfaces/services/IPageService.ts @@ -4,7 +4,7 @@ import { EventHandler } from 'src/entities/event_handler.entity'; import { CreatePageDto, UpdatePageDto } from '@modules/apps/dto/page'; export interface IPageService { - findPagesForVersion(appVersionId: string): Promise; + findPagesForVersion(appVersionId: string, mode?: string): Promise; findOne(id: string): Promise; createPage(page: CreatePageDto, appVersionId: string): Promise; clonePage(pageId: string, appVersionId: string): Promise<{ pages: Page[]; events: EventHandler[] }>; diff --git a/server/src/modules/apps/services/page.service.ts b/server/src/modules/apps/services/page.service.ts index fa0b2864e0..3ead2d4a99 100644 --- a/server/src/modules/apps/services/page.service.ts +++ b/server/src/modules/apps/services/page.service.ts @@ -23,7 +23,7 @@ export class PageService implements IPageService { protected eventHandlerService: EventsService ) {} - async findPagesForVersion(appVersionId: string): Promise { + async findPagesForVersion(appVersionId: string, mode?: string): Promise { // const allPages = await this.pageRepository.find({ where: { appVersionId }, order: { index: 'ASC' } }); const allPages = await this.pageHelperService.fetchPages(appVersionId); const pagesWithComponents = await Promise.all( From b286834c7f47c7e8086e0c8f06d7286eff41a1a8 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Sun, 1 Jun 2025 20:12:22 +0530 Subject: [PATCH 10/24] Integrated App permissions modal for component permissions and updated state for permissions --- frontend/ee | 2 +- .../RightSideBar/Inspector/Inspector.jsx | 86 +++++++++++++++---- .../_stores/slices/componentsSlice.js | 22 +++++ server/ee | 2 +- 4 files changed, 93 insertions(+), 19 deletions(-) diff --git a/frontend/ee b/frontend/ee index 5c2787b498..d97f9b8c1c 160000 --- a/frontend/ee +++ b/frontend/ee @@ -1 +1 @@ -Subproject commit 5c2787b498202f4356b4598fb961ddbab04acbbf +Subproject commit d97f9b8c1c0dd54696d3c13fc9dc2227db6c9664 diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx index 07b53c4454..b661b74583 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx @@ -43,6 +43,9 @@ import useStore from '@/AppBuilder/_stores/store'; import { componentTypes } from '@/AppBuilder/WidgetManager/componentTypes'; import { copyComponents } from '@/AppBuilder/AppCanvas/appCanvasUtils.js'; import DatetimePickerV2 from './Components/DatetimePickerV2.jsx'; +import { ToolTip } from '@/_components/ToolTip'; +import AppPermissionsModal from '@/modules/Appbuilder/components/AppPermissionsModal'; +import { appPermissionService } from '@/_services'; const INSPECTOR_HEADER_OPTIONS = [ { @@ -60,6 +63,19 @@ const INSPECTOR_HEADER_OPTIONS = [ value: 'duplicate', icon: , }, + { + label: 'Component permission', + value: 'permission', + icon: ( + permission-icon + ), + trailingIcon: , + }, { label: 'Delete', value: 'delete', @@ -103,6 +119,11 @@ export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selecte const isVersionReleased = useStore((state) => state.isVersionReleased); const setWidgetDeleteConfirmation = useStore((state) => state.setWidgetDeleteConfirmation); const setComponentToInspect = useStore((state) => state.setComponentToInspect); + const featureAccess = useStore((state) => state?.license?.featureAccess, shallow); + const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid; + const showComponentPermissionModal = useStore((state) => state.showComponentPermissionModal); + const toggleComponentPermissionModal = useStore((state) => state.toggleComponentPermissionModal); + const setComponentPermission = useStore((state) => state.setComponentPermission); const dataQueries = useDataQueries(); const currentState = useCurrentState(); @@ -375,6 +396,9 @@ export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selecte if (value === 'delete') { setWidgetDeleteConfirmation(true); } + if (value === 'permission' && licenseValid) { + toggleComponentPermissionModal(true); + } if (value === 'duplicate') { copyComponents({ isCloning: true }); } @@ -490,26 +514,42 @@ export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selecte overlay={ - {INSPECTOR_HEADER_OPTIONS.map((option) => ( -
{ - e.stopPropagation(); - handleInspectorHeaderActions(option.value); - }} - > -
{option.icon}
+ {INSPECTOR_HEADER_OPTIONS.map((option) => { + const optionBody = (
{ + e.stopPropagation(); + handleInspectorHeaderActions(option.value); + }} > - {option?.label} +
{option.icon}
+
+ {option?.label} +
+ {option.value === 'permission' && !licenseValid && option.trailingIcon && option.trailingIcon}
-
- ))} + ); + + return option.value === 'permission' ? ( + + {optionBody} + + ) : ( + optionBody + ); + })}
} @@ -519,6 +559,18 @@ export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selecte + appPermissionService.getComponentPermission(appId, id)} + createPermission={(id, appId, body) => appPermissionService.createComponentPermission(appId, id, body)} + updatePermission={(id, appId, body) => appPermissionService.updateComponentPermission(appId, id, body)} + deletePermission={(id, appId) => appPermissionService.deleteComponentPermission(appId, id)} + onSuccess={(data) => setComponentPermission(selectedComponentId, data)} + />
diff --git a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js index b500a4d912..46ae6791e3 100644 --- a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js @@ -45,6 +45,7 @@ const initialState = { showWidgetDeleteConfirmation: false, focusedParentId: null, modalsOpenOnCanvas: [], + showComponentPermissionModal: false, }; export const createComponentsSlice = (set, get) => ({ @@ -1920,4 +1921,25 @@ export const createComponentsSlice = (set, get) => ({ setComponentProperty(componentId, `canvasHeight`, maxHeight, 'properties', 'value', false); }, + toggleComponentPermissionModal: (show) => { + set((state) => { + state.showComponentPermissionModal = show; + }); + }, + setComponentPermission: (componentId, data) => { + const { currentPageId, modules } = get(); + const currentPageIndex = modules.canvas.pages.findIndex((page) => page.id === currentPageId); + const component = modules.canvas.pages[currentPageIndex]?.components?.[componentId]; + + if (component) { + const updatedComponent = { + ...component, + permissions: data.length === 0 || data.length === undefined ? [] : [data[0]], + }; + + set((state) => { + state.modules.canvas.pages[currentPageIndex].components[componentId] = updatedComponent; + }); + } + }, }); diff --git a/server/ee b/server/ee index adc0f3471b..d32bbd96c6 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit adc0f3471bfef26be3ea5d69972bcf2e6c8c46cb +Subproject commit d32bbd96c6d81f7cfb4237fd92321448fc6f2e34 From 19b770abad3b18e80c8a5f750a6ceeae411b5641 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Sun, 1 Jun 2025 20:30:30 +0530 Subject: [PATCH 11/24] Remove component from containerChildrenMaping --- frontend/src/AppBuilder/_stores/slices/componentsSlice.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js index 46ae6791e3..8de2e30723 100644 --- a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js @@ -80,6 +80,7 @@ export const createComponentsSlice = (set, get) => ({ state.currentPageId = id; state.containerChildrenMapping = { canvas: [] }; Object.entries(currentPageComponents).forEach(([componentId, component]) => { + if (state.currentMode === 'view' && component.restricted) return; const parentId = component.component.parent || 'canvas'; if (!state.containerChildrenMapping[parentId]) { state.containerChildrenMapping[parentId] = []; From 15719ac1f288208c7f62fb4960e09abfac00f971 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Sun, 1 Jun 2025 22:27:32 +0530 Subject: [PATCH 12/24] Add permission icon in component config handle --- .../AppCanvas/ConfigHandle/ConfigHandle.jsx | 49 +++++++++++++++++++ frontend/src/_styles/theme.scss | 2 +- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx index 45835c39de..cdf864d8b5 100644 --- a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx +++ b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx @@ -4,6 +4,7 @@ import './configHandle.scss'; import useStore from '@/AppBuilder/_stores/store'; import { findHighestLevelofSelection } from '../Grid/gridUtils'; import SolidIcon from '@/_ui/Icon/solidIcons/index'; +import { ToolTip } from '@/_components/ToolTip'; const CONFIG_HANDLE_HEIGHT = 20; const BUFFER_HEIGHT = 1; @@ -47,7 +48,39 @@ export const ConfigHandle = ({ ); }, shallow); + const currentPageIndex = useStore((state) => state.currentPageIndex); + const component = useStore((state) => state.modules.canvas.pages[currentPageIndex].components[id]); + const featureAccess = useStore((state) => state?.license?.featureAccess, shallow); + const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid; + const isRestricted = component.permissions && component.permissions.length !== 0; + let height = visibility === false ? 10 : widgetHeight; + + const getTooltip = () => { + const permission = component.permissions?.[0]; + if (!permission) return null; + + const users = permission.groups || permission.users || []; + if (users.length === 0) return null; + + const isSingle = permission.type === 'SINGLE'; + const isGroup = permission.type === 'GROUP'; + + if (isSingle) { + return users.length === 1 + ? `Access restricted to ${users[0].user.email}` + : `Access restricted to ${users.length} users`; + } + + if (isGroup) { + return users.length === 1 + ? `Access restricted to ${users[0].permission_group?.name || users[0].permissionGroup?.name} group` + : `Access restricted to ${users.length} user groups`; + } + + return null; + }; + return (
+ {licenseValid && isRestricted && ( + + + + + + )} Date: Tue, 3 Jun 2025 22:24:47 +0530 Subject: [PATCH 13/24] Restrict child component for container components --- .../src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx | 3 ++- frontend/src/AppBuilder/_stores/slices/componentsSlice.js | 1 - server/ee | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx index cdf864d8b5..8e79d6f63c 100644 --- a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx +++ b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx @@ -53,6 +53,7 @@ export const ConfigHandle = ({ const featureAccess = useStore((state) => state?.license?.featureAccess, shallow); const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid; const isRestricted = component.permissions && component.permissions.length !== 0; + const draggingComponentId = useStore((state) => state.draggingComponentId); let height = visibility === false ? 10 : widgetHeight; @@ -105,7 +106,7 @@ export const ConfigHandle = ({ }} > {licenseValid && isRestricted && ( - + ({ state.currentPageId = id; state.containerChildrenMapping = { canvas: [] }; Object.entries(currentPageComponents).forEach(([componentId, component]) => { - if (state.currentMode === 'view' && component.restricted) return; const parentId = component.component.parent || 'canvas'; if (!state.containerChildrenMapping[parentId]) { state.containerChildrenMapping[parentId] = []; diff --git a/server/ee b/server/ee index d32bbd96c6..0047b9e0c6 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit d32bbd96c6d81f7cfb4237fd92321448fc6f2e34 +Subproject commit 0047b9e0c6d1ea6bed11de4333ac6961ec030eab From a3450b87f7d629437d9ddc51c40b88e25b04d782 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Tue, 3 Jun 2025 23:44:26 +0530 Subject: [PATCH 14/24] Disable button on free plan --- frontend/src/AppBuilder/QueryPanel/QueryCard.jsx | 1 + .../src/AppBuilder/RightSideBar/Inspector/Inspector.jsx | 7 +++++-- frontend/src/ToolJetUI/List/list.scss | 4 ++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/frontend/src/AppBuilder/QueryPanel/QueryCard.jsx b/frontend/src/AppBuilder/QueryPanel/QueryCard.jsx index 5236a529eb..318ac86736 100644 --- a/frontend/src/AppBuilder/QueryPanel/QueryCard.jsx +++ b/frontend/src/AppBuilder/QueryPanel/QueryCard.jsx @@ -276,6 +276,7 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
{option?.label} diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx index b661b74583..c6c788ecaf 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx @@ -396,12 +396,14 @@ export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selecte if (value === 'delete') { setWidgetDeleteConfirmation(true); } - if (value === 'permission' && licenseValid) { + if (value === 'permission') { + if (!licenseValid) return; toggleComponentPermissionModal(true); } if (value === 'duplicate') { copyComponents({ isCloning: true }); } + setShowHeaderActionsMenu(false); }; const buildGeneralStyle = () => { if (!componentMeta?.definition?.generalStyles) { @@ -467,7 +469,7 @@ export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selecte React.useEffect(() => { const handleClickOutside = (event) => { - if (showHeaderActionsMenu && event.target.closest('.list-menu') === null) { + if (showHeaderActionsMenu && event.target.closest('#list-menu') === null) { setShowHeaderActionsMenu(false); } }; @@ -529,6 +531,7 @@ export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selecte
{option?.label} diff --git a/frontend/src/ToolJetUI/List/list.scss b/frontend/src/ToolJetUI/List/list.scss index 786f4e88af..04258a0788 100644 --- a/frontend/src/ToolJetUI/List/list.scss +++ b/frontend/src/ToolJetUI/List/list.scss @@ -79,6 +79,10 @@ button:focus:not(:focus-visible) { .color-tomato9 { color: var(--tomato9) } + + .color-disabled { + color: var(--text-disabled); + } } } From 62b44532417662581a6473597224f5602b579109 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Thu, 5 Jun 2025 00:44:01 +0530 Subject: [PATCH 15/24] Fix: Query options menu --- .../src/AppBuilder/QueryPanel/QueryCard.jsx | 192 ++++-------------- .../AppBuilder/QueryPanel/QueryCardMenu.jsx | 172 ++++++++++++++++ .../AppBuilder/QueryPanel/QueryDataPane.jsx | 2 + .../_stores/slices/queryPanelSlice.js | 19 ++ 4 files changed, 231 insertions(+), 154 deletions(-) create mode 100644 frontend/src/AppBuilder/QueryPanel/QueryCardMenu.jsx diff --git a/frontend/src/AppBuilder/QueryPanel/QueryCard.jsx b/frontend/src/AppBuilder/QueryPanel/QueryCard.jsx index 318ac86736..2d192ec666 100644 --- a/frontend/src/AppBuilder/QueryPanel/QueryCard.jsx +++ b/frontend/src/AppBuilder/QueryPanel/QueryCard.jsx @@ -1,11 +1,10 @@ -import React, { useState, useCallback } from 'react'; +import React, { useState } from 'react'; import { Tooltip } from 'react-tooltip'; import { ToolTip } from '@/_components/ToolTip'; import { updateQuerySuggestions } from '@/_helpers/appUtils'; // import { Confirm } from '../Viewer/Confirm'; import { toast } from 'react-hot-toast'; import { shallow } from 'zustand/shallow'; -import Copy from '@/_ui/Icon/solidIcons/Copy'; import DataSourceIcon from '../QueryManager/Components/DataSourceIcon'; import { isQueryRunnable, decodeEntities } from '@/_helpers/utils'; import { canDeleteDataSource, canReadDataSource, canUpdateDataSource } from '@/_helpers'; @@ -13,17 +12,10 @@ import useStore from '@/AppBuilder/_stores/store'; //TODO: Remove this import { Confirm } from '@/Editor/Viewer/Confirm'; // TODO: enable delete query confirmation popup -import { debounce } from 'lodash'; import { Button as ButtonComponent } from '@/components/ui/Button/Button.jsx'; -import Edit from '@/_ui/Icon/bulkIcons/Edit'; -import Trash from '@/_ui/Icon/solidIcons/Trash'; -import { OverlayTrigger, Popover } from 'react-bootstrap'; -import classNames from 'classnames'; import SolidIcon from '@/_ui/Icon/SolidIcons'; export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => { - const appId = useStore((state) => state.app.appId); - const isQuerySelected = useStore((state) => state.queryPanel.isQuerySelected(dataQuery.id), shallow); const setSelectedQuery = useStore((state) => state.queryPanel.setSelectedQuery); const checkExistingQueryName = useStore((state) => state.dataQuery.checkExistingQueryName); @@ -31,11 +23,16 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => { const isDeletingQueryInProcess = useStore((state) => state.dataQuery.isDeletingQueryInProcess); const renameQuery = useStore((state) => state.dataQuery.renameQuery); const deleteDataQueries = useStore((state) => state.dataQuery.deleteDataQueries); - const duplicateQuery = useStore((state) => state.dataQuery.duplicateQuery); const setPreviewData = useStore((state) => state.queryPanel.setPreviewData); - const toggleQueryPermissionModal = useStore((state) => state.queryPanel.toggleQueryPermissionModal); - const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false); - const [showQueryMenu, setShowQueryMenu] = useState(false); + const shouldFreeze = useStore((state) => state.getShouldFreeze()); + + const renamingQueryId = useStore((state) => state.queryPanel.renamingQueryId); + const deletingQueryId = useStore((state) => state.queryPanel.deletingQueryId); + const setRenamingQuery = useStore((state) => state.queryPanel.setRenamingQuery); + const deleteDataQuery = useStore((state) => state.queryPanel.deleteDataQuery); + const isRenaming = renamingQueryId === dataQuery.id; + const isDeleting = deletingQueryId === dataQuery.id; + const hasPermissions = selectedDataSourceScope === 'global' ? canUpdateDataSource(dataQuery?.data_source_id) || @@ -43,104 +40,35 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => { canDeleteDataSource() : true; + const toggleQueryHandlerMenu = useStore((state) => state.queryPanel.toggleQueryHandlerMenu); const featureAccess = useStore((state) => state?.license?.featureAccess, shallow); const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid; const isRestricted = dataQuery.permissions && dataQuery.permissions.length !== 0; - const shouldFreeze = useStore((state) => state.getShouldFreeze()); - - const QUERY_MENU_OPTIONS = [ - { - label: 'Rename', - value: 'rename', - icon: , - showTooltip: false, - }, - { - label: 'Duplicate', - value: 'duplicate', - icon: , - showTooltip: false, - }, - { - label: 'Query permission', - value: 'permission', - icon: ( - permission-icon - ), - trailingIcon: !licenseValid ? : undefined, - tooltipText: 'Query permissions are available only in paid plans', - showTooltip: !licenseValid, - }, - { - label: 'Delete', - value: 'delete', - icon: , - showTooltip: false, - }, - ]; - - const handleQueryMenuActions = (value) => { - if (value === 'rename') { - setRenamingQuery(true); - } - if (value === 'duplicate') { - debouncedDuplicateQuery(dataQuery?.id, appId); - } - if (value === 'permission') { - if (!licenseValid) return; - toggleQueryPermissionModal(true); - } - if (value === 'delete') { - deleteDataQuery(); - } - setShowQueryMenu(false); - }; - - const [renamingQuery, setRenamingQuery] = useState(false); - - const deleteDataQuery = () => { - setShowDeleteConfirmation(true); - }; - const updateQueryName = (dataQuery, newName) => { const { name } = dataQuery; if (name === newName) { - return setRenamingQuery(false); + return setRenamingQuery(null); } const isNewQueryNameAlreadyExists = checkExistingQueryName(newName); if (newName && !isNewQueryNameAlreadyExists) { renameQuery(dataQuery?.id, newName); - setRenamingQuery(false); + setRenamingQuery(null); updateQuerySuggestions(name, newName); } else { if (isNewQueryNameAlreadyExists) { toast.error('Query name already exists'); } - setRenamingQuery(false); + setRenamingQuery(null); } }; const executeDataQueryDeletion = () => { - setShowDeleteConfirmation(false); + deleteDataQuery(null); deleteDataQueries(dataQuery?.id); setPreviewData(null); }; - // To prevent user clicking from continuous clicks - const debouncedDuplicateQuery = useCallback( - debounce((queryId, appId) => { - duplicateQuery(queryId, appId); - setPreviewData(null); - }, 500), - [duplicateQuery] - ); - const getTooltip = () => { const permission = dataQuery.permissions?.[0]; if (!permission) return null; @@ -171,10 +99,18 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
{ + onClick={(e) => { if (isQuerySelected) return; - setSelectedQuery(dataQuery?.id); - setPreviewData(null); + const menuBtn = document.getElementById(`query-handler-menu-${dataQuery?.id}`); + if (menuBtn.contains(e.target)) { + e.stopPropagation(); + } else { + toggleQueryHandlerMenu(false); + } + setTimeout(() => { + setSelectedQuery(dataQuery?.id); + setPreviewData(null); + }, 0); }} role="button" > @@ -182,7 +118,7 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
- {renamingQuery ? ( + {isRenaming ? ( { )}
- setShowQueryMenu(false)} - show={showQueryMenu && isQuerySelected} - popperConfig={{ - modifiers: [ - { - name: 'offset', - options: { - offset: [0, 3], - }, - }, - ], - }} - overlay={ - - - {QUERY_MENU_OPTIONS.map((option) => ( - -
{ - e.stopPropagation(); - handleQueryMenuActions(option.value); - }} - > -
{option.icon}
-
- {option?.label} -
- {option.trailingIcon && option.trailingIcon} -
-
- ))} -
-
- } - > - setShowQueryMenu(!showQueryMenu)} - size="small" - variant="outline" - className="" - /> -
+ toggleQueryHandlerMenu(true, `query-handler-menu-${dataQuery?.id}`)} + size="small" + variant="outline" + className="" + id={`query-handler-menu-${dataQuery?.id}`} + />
setShowDeleteConfirmation(false)} + onCancel={() => deleteDataQuery(null)} darkMode={darkMode} /> diff --git a/frontend/src/AppBuilder/QueryPanel/QueryCardMenu.jsx b/frontend/src/AppBuilder/QueryPanel/QueryCardMenu.jsx new file mode 100644 index 0000000000..760adb8075 --- /dev/null +++ b/frontend/src/AppBuilder/QueryPanel/QueryCardMenu.jsx @@ -0,0 +1,172 @@ +import React, { useCallback } from 'react'; +import { Overlay, Popover } from 'react-bootstrap'; +import useStore from '@/AppBuilder/_stores/store'; +import classNames from 'classnames'; +import Edit from '@/_ui/Icon/bulkIcons/Edit'; +import Trash from '@/_ui/Icon/solidIcons/Trash'; +import Copy from '@/_ui/Icon/solidIcons/Copy'; +import SolidIcon from '@/_ui/Icon/SolidIcons'; +import { shallow } from 'zustand/shallow'; +import { ToolTip } from '@/_components/ToolTip'; +import { debounce } from 'lodash'; +import usePopoverObserver from '@/AppBuilder/_hooks/usePopoverObserver'; + +const QueryCardMenu = ({ darkMode }) => { + const appId = useStore((state) => state.app.appId); + const selectedQuery = useStore((state) => state.queryPanel.selectedQuery); + const toggleQueryPermissionModal = useStore((state) => state.queryPanel.toggleQueryPermissionModal); + const featureAccess = useStore((state) => state?.license?.featureAccess, shallow); + const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid; + const targetBtnForMenu = useStore((state) => state.queryPanel.targetBtnForMenu); + const targetElement = document.getElementById(targetBtnForMenu); + const showQueryHandlerMenu = useStore((state) => state.queryPanel.showQueryHandlerMenu); + const toggleQueryHandlerMenu = useStore((state) => state.queryPanel.toggleQueryHandlerMenu); + const duplicateQuery = useStore((state) => state.dataQuery.duplicateQuery); + const setPreviewData = useStore((state) => state.queryPanel.setPreviewData); + const setRenamingQuery = useStore((state) => state.queryPanel.setRenamingQuery); + const deleteDataQuery = useStore((state) => state.queryPanel.deleteDataQuery); + + const QUERY_MENU_OPTIONS = [ + { + label: 'Rename', + value: 'rename', + icon: , + showTooltip: false, + }, + { + label: 'Duplicate', + value: 'duplicate', + icon: , + showTooltip: false, + }, + { + label: 'Query permission', + value: 'permission', + icon: ( + permission-icon + ), + trailingIcon: , + }, + { + label: 'Delete', + value: 'delete', + icon: , + showTooltip: false, + }, + ]; + + // To prevent user clicking from continuous clicks + const debouncedDuplicateQuery = useCallback( + debounce((queryId, appId) => { + duplicateQuery(queryId, appId); + setPreviewData(null); + }, 500), + [duplicateQuery] + ); + + const handleQueryMenuActions = (value) => { + if (value === 'rename') { + setRenamingQuery(selectedQuery?.id); + } + if (value === 'duplicate') { + debouncedDuplicateQuery(selectedQuery?.id, appId); + } + if (value === 'permission') { + if (!licenseValid) return; + toggleQueryPermissionModal(true); + } + if (value === 'delete') { + deleteDataQuery(selectedQuery?.id); + } + toggleQueryHandlerMenu(false); + }; + + usePopoverObserver( + document.getElementsByClassName('query-list')[0], + targetElement, + document.getElementById('query-list-menu'), + showQueryHandlerMenu, + () => (document.getElementById('query-list-menu').style.display = 'block'), + () => (document.getElementById('query-list-menu').style.display = 'none') + ); + + return ( + toggleQueryHandlerMenu(false)} + popperConfig={{ + modifiers: [ + { + name: 'flip', + options: { + fallbackPlacements: ['top-start'], + flipVariations: true, + allowedAutoPlacements: ['top', 'bottom'], + boundary: 'viewport', + }, + }, + { + name: 'offset', + options: { + offset: [0, 3], + }, + }, + ], + }} + > + {(props) => ( + + + {QUERY_MENU_OPTIONS.map((option) => { + const optionBody = ( +
{ + e.stopPropagation(); + handleQueryMenuActions(option.value); + }} + > +
{option.icon}
+
+ {option?.label} +
+ {option.value === 'permission' && !licenseValid && option.trailingIcon && option.trailingIcon} +
+ ); + + return option.value === 'permission' ? ( + + {optionBody} + + ) : ( + optionBody + ); + })} +
+
+ )} +
+ ); +}; + +export default QueryCardMenu; diff --git a/frontend/src/AppBuilder/QueryPanel/QueryDataPane.jsx b/frontend/src/AppBuilder/QueryPanel/QueryDataPane.jsx index 62f27197ba..12e8fb3171 100644 --- a/frontend/src/AppBuilder/QueryPanel/QueryDataPane.jsx +++ b/frontend/src/AppBuilder/QueryPanel/QueryDataPane.jsx @@ -19,6 +19,7 @@ import useStore from '@/AppBuilder/_stores/store'; import AppPermissionsModal from '@/modules/Appbuilder/components/AppPermissionsModal'; import { shallow } from 'zustand/shallow'; import { appPermissionService } from '@/_services'; +import QueryCardMenu from './QueryCardMenu'; export const QueryDataPane = ({ darkMode }) => { const { t } = useTranslation(); @@ -180,6 +181,7 @@ export const QueryDataPane = ({ darkMode }) => { {filteredQueries.map((query) => ( ))} + {licenseValid && ( ({ @@ -1141,5 +1146,19 @@ export const createQueryPanelSlice = (set, get) => ({ state.queryPanel.showQueryPermissionModal = show; }); }, + toggleQueryHandlerMenu: (show, id) => { + set((state) => { + if (show) state.queryPanel.targetBtnForMenu = id; + state.queryPanel.showQueryHandlerMenu = show; + }); + }, + setRenamingQuery: (queryId) => + set((state) => { + state.queryPanel.renamingQueryId = queryId; + }), + deleteDataQuery: (queryId) => + set((state) => { + state.queryPanel.deletingQueryId = queryId; + }), }, }); From 0dc73d891065bc761b56a5374d77bac2386dcb6e Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Thu, 5 Jun 2025 01:58:29 +0530 Subject: [PATCH 16/24] Fix: Foreign key issue in versioning Component permissions --- server/ee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/ee b/server/ee index 0047b9e0c6..c6e11564b5 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit 0047b9e0c6d1ea6bed11de4333ac6961ec030eab +Subproject commit c6e11564b5f7ad79aa34fc7bf9ac1b14c8098dad From 65ed3b87282310da42a57a66ec908884f0ed9341 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Thu, 5 Jun 2025 10:45:59 +0530 Subject: [PATCH 17/24] Fix: query not running on application load if query not accessible --- frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js | 5 ++++- server/ee | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js b/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js index 79acc2c461..7fd9c46af6 100644 --- a/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js +++ b/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js @@ -420,7 +420,10 @@ export const createDataQuerySlice = (set, get) => ({ const queries = get().dataQuery.queries.modules.canvas; try { for (const query of queries) { - if ((query.options.runOnPageLoad || query.options.run_on_page_load) && isQueryRunnable(query)) { + if ( + (query.options.runOnPageLoad || query.options.run_on_page_load) && + (query.restricted || isQueryRunnable(query)) + ) { await get().queryPanel.runQuery(query.id, query.name, undefined, undefined, {}, false, true, 'canvas'); } } diff --git a/server/ee b/server/ee index c6e11564b5..f59972c5c3 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit c6e11564b5f7ad79aa34fc7bf9ac1b14c8098dad +Subproject commit f59972c5c3c37179b467deeda0d4d3bf81b5c6f9 From 970cfe21c7804af5ca684573293bbe3a9acd5c70 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Thu, 5 Jun 2025 12:15:42 +0530 Subject: [PATCH 18/24] Add page, query or component name to the permissions modal --- frontend/ee | 2 +- frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx | 2 ++ frontend/src/AppBuilder/QueryPanel/QueryDataPane.jsx | 1 + frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/ee b/frontend/ee index d97f9b8c1c..743e06cd6e 160000 --- a/frontend/ee +++ b/frontend/ee @@ -1 +1 @@ -Subproject commit d97f9b8c1c0dd54696d3c13fc9dc2227db6c9664 +Subproject commit 743e06cd6e1233d0b98720354bb7b0d0df098f22 diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx index d55864e91c..68a54c2ca4 100644 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx @@ -29,6 +29,7 @@ export const PageMenu = ({ darkMode, switchPage, pinned, setPinned }) => { const enableReleasedVersionPopupState = useStore((state) => state.enableReleasedVersionPopupState); const closePageEditPopover = useStore((state) => state.closePageEditPopover); const editingPageId = useStore((state) => state.editingPage?.id); + const editingPageName = useStore((state) => state.editingPage?.name); const showPagePermissionModal = useStore((state) => state.showPagePermissionModal); const togglePagePermissionModal = useStore((state) => state.togglePagePermissionModal); const updatePageWithPermissions = useStore((state) => state.updatePageWithPermissions); @@ -105,6 +106,7 @@ export const PageMenu = ({ darkMode, switchPage, pinned, setPinned }) => { { Date: Thu, 5 Jun 2025 14:50:03 +0530 Subject: [PATCH 19/24] Added duplicating logic for query --- .../_stores/slices/dataQuerySlice.js | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js b/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js index 7fd9c46af6..3a8dba58c6 100644 --- a/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js +++ b/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js @@ -1,4 +1,4 @@ -import { dataqueryService } from '@/_services'; +import { dataqueryService, appPermissionService } from '@/_services'; import { getDefaultOptions } from '@/_stores/storeHelper'; import { v4 as uuidv4 } from 'uuid'; import _, { isEmpty, throttle } from 'lodash'; @@ -256,7 +256,6 @@ export const createDataQuerySlice = (set, get) => ({ ) .then((data) => { set((state) => { - state.dataQuery.creatingQueryInProcessId = null; state.dataQuery.queries.modules[moduleId] = [ { ...data, @@ -290,6 +289,42 @@ export const createDataQuerySlice = (set, get) => ({ }; createAppVersionEventHandlers(newEvent, moduleId); }); + + if (queryToClone.permissions && queryToClone.permissions.length !== 0) { + const body = { + type: queryToClone.permissions[0]?.type, + ...(queryToClone.permissions[0]?.type === 'GROUP' + ? { + groups: (queryToClone.permissions[0]?.groups || queryToClone.permissions[0]?.users || []).map( + (group) => group.permissionGroupsId || group.permission_groups_id + ), + } + : { users: queryToClone.permissions[0]?.users.map((user) => user.userId || user.user_id) }), + }; + appPermissionService + .createQueryPermission(appId, data.id, body) + .then((newQuery) => { + const dataQueries = get().dataQuery.queries.modules[moduleId]; + const updatedDataQueries = dataQueries.map((query) => { + if (query.id === data.id) { + return { + ...query, + permissions: newQuery.length === 0 || newQuery.length === undefined ? [] : [newQuery[0]], + }; + } + return query; + }); + get().dataQuery.setQueries(updatedDataQueries); + }) + .catch(() => { + toast.error('Permission could not be created. Please try again!', { + className: 'text-nowrap w-auto mw-100', + }); + }); + } + set((state) => { + state.dataQuery.creatingQueryInProcessId = null; + }); }) .catch((error) => { console.error('error', error); From 5a3170b2d04e458c08471f447861bf090b98860c Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Wed, 11 Jun 2025 10:01:42 +0530 Subject: [PATCH 20/24] Fixed component vanishing issue on version creation and promotion --- .../AppBuilder/Header/CreateVersionModal.jsx | 4 ++- frontend/src/AppBuilder/_hooks/useAppData.js | 4 +-- .../slices/environmentsAndVersionsSlice.js | 2 +- frontend/src/_services/appVersion.service.js | 4 +-- server/ee | 2 +- server/src/modules/versions/controller.v2.ts | 6 ++-- .../versions/interfaces/IControllerV2.ts | 2 +- .../modules/versions/interfaces/IService.ts | 2 +- server/src/modules/versions/repository.ts | 28 +++++++++++++++++++ server/src/modules/versions/service.ts | 2 +- 10 files changed, 43 insertions(+), 13 deletions(-) diff --git a/frontend/src/AppBuilder/Header/CreateVersionModal.jsx b/frontend/src/AppBuilder/Header/CreateVersionModal.jsx index 3add0e6074..689965691a 100644 --- a/frontend/src/AppBuilder/Header/CreateVersionModal.jsx +++ b/frontend/src/AppBuilder/Header/CreateVersionModal.jsx @@ -27,6 +27,7 @@ const CreateVersionModal = ({ appId, setCurrentVersionId, selectedVersion, + currentMode, } = useStore( (state) => ({ createNewVersionAction: state.createNewVersionAction, @@ -39,6 +40,7 @@ const CreateVersionModal = ({ currentVersionId: state.currentVersionId, setCurrentVersionId: state.setCurrentVersionId, selectedVersion: state.selectedVersion, + currentMode: state.currentMode, }), shallow ); @@ -88,7 +90,7 @@ const CreateVersionModal = ({ setIsCreatingVersion(false); setShowCreateAppVersion(false); appVersionService - .getAppVersionData(appId, newVersion.id) + .getAppVersionData(appId, newVersion.id, currentMode) .then((data) => { setCurrentVersionId(newVersion.id); handleCommitOnVersionCreation(data); diff --git a/frontend/src/AppBuilder/_hooks/useAppData.js b/frontend/src/AppBuilder/_hooks/useAppData.js index 7b74639870..c0cd9124e9 100644 --- a/frontend/src/AppBuilder/_hooks/useAppData.js +++ b/frontend/src/AppBuilder/_hooks/useAppData.js @@ -211,7 +211,7 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v appDataPromise = appService.fetchAppBySlug(slug); } else { appDataPromise = isPreviewForVersion - ? appVersionService.getAppVersionData(appId, versionId) + ? appVersionService.getAppVersionData(appId, versionId, mode) : appService.fetchApp(appId); } @@ -488,7 +488,7 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v if (isEnvChanged) { setEnvironmentLoadingState('loading'); } - appVersionService.getAppVersionData(appId, selectedVersion?.id).then(async (appData) => { + appVersionService.getAppVersionData(appId, selectedVersion?.id, mode).then(async (appData) => { cleanUpStore(); const { should_freeze_editor } = appData; setIsEditorFreezed(should_freeze_editor); diff --git a/frontend/src/AppBuilder/_stores/slices/environmentsAndVersionsSlice.js b/frontend/src/AppBuilder/_stores/slices/environmentsAndVersionsSlice.js index 1edd3994c5..75f89f1e06 100644 --- a/frontend/src/AppBuilder/_stores/slices/environmentsAndVersionsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/environmentsAndVersionsSlice.js @@ -196,7 +196,7 @@ export const createEnvironmentsAndVersionsSlice = (set, get) => ({ }, changeEditorVersionAction: async (appId, versionId, onSuccess, onFailure) => { try { - const data = await appVersionService.getAppVersionData(appId, versionId); + const data = await appVersionService.getAppVersionData(appId, versionId, get().currentMode); const selectedVersion = { id: data.editing_version.id, name: data.editing_version.name, diff --git a/frontend/src/_services/appVersion.service.js b/frontend/src/_services/appVersion.service.js index bda0b7a192..e510fb67d0 100644 --- a/frontend/src/_services/appVersion.service.js +++ b/frontend/src/_services/appVersion.service.js @@ -36,9 +36,9 @@ function promoteEnvironment(appId, versionId, currentEnvironmentId) { }; return fetch(`${config.apiUrl}/v2/apps/${appId}/versions/${versionId}/promote`, requestOptions).then(handleResponse); } -function getAppVersionData(appId, versionId) { +function getAppVersionData(appId, versionId, mode) { const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' }; - return fetch(`${config.apiUrl}/v2/apps/${appId}/versions/${versionId}`, requestOptions).then(handleResponse); + return fetch(`${config.apiUrl}/v2/apps/${appId}/versions/${versionId}?mode=${mode}`, requestOptions).then(handleResponse); } function create(appId, versionName, versionFromId, currentEnvironmentId) { diff --git a/server/ee b/server/ee index f59972c5c3..1c46b1b5f0 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit f59972c5c3c37179b467deeda0d4d3bf81b5c6f9 +Subproject commit 1c46b1b5f0150c273adad875a6e8553a00c0f343 diff --git a/server/src/modules/versions/controller.v2.ts b/server/src/modules/versions/controller.v2.ts index 7a3f006883..02e9f91268 100644 --- a/server/src/modules/versions/controller.v2.ts +++ b/server/src/modules/versions/controller.v2.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, Put, UseGuards } from '@nestjs/common'; +import { Body, Controller, Get, Put, Query, UseGuards } from '@nestjs/common'; import { VersionService } from './service'; import { InitModule } from '@modules/app/decorators/init-module'; import { MODULES } from '@modules/app/constants/modules'; @@ -26,8 +26,8 @@ export class VersionControllerV2 implements IVersionControllerV2 { @InitFeature(FEATURE_KEY.GET_ONE) @UseGuards(JwtAuthGuard, ValidAppGuard, FeatureAbilityGuard) @Get(':id/versions/:versionId') - getVersion(@User() user: UserEntity, @App() app: AppEntity) { - return this.versionService.getVersion(app, user); + getVersion(@User() user: UserEntity, @App() app: AppEntity, @Query('mode') mode?: string) { + return this.versionService.getVersion(app, user, mode); } @InitFeature(FEATURE_KEY.UPDATE) diff --git a/server/src/modules/versions/interfaces/IControllerV2.ts b/server/src/modules/versions/interfaces/IControllerV2.ts index 2d86353981..f432e9d3dd 100644 --- a/server/src/modules/versions/interfaces/IControllerV2.ts +++ b/server/src/modules/versions/interfaces/IControllerV2.ts @@ -4,7 +4,7 @@ import { App as AppEntity } from '@entities/app.entity'; import { PromoteVersionDto } from '../dto'; export interface IVersionControllerV2 { - getVersion(user: UserEntity, app: AppEntity): Promise; + getVersion(user: UserEntity, app: AppEntity, mode?: string): Promise; updateVersion(user: UserEntity, app: AppEntity, appVersionUpdateDto: AppVersionUpdateDto): Promise; updateGlobalSettings(user: UserEntity, app: AppEntity, appVersionUpdateDto: AppVersionUpdateDto): Promise; promoteVersion(user: UserEntity, app: AppEntity, promoteVersionDto: PromoteVersionDto): Promise; diff --git a/server/src/modules/versions/interfaces/IService.ts b/server/src/modules/versions/interfaces/IService.ts index dc39061efa..5c6ed2d1b4 100644 --- a/server/src/modules/versions/interfaces/IService.ts +++ b/server/src/modules/versions/interfaces/IService.ts @@ -11,7 +11,7 @@ export interface IVersionService { deleteVersion(app: App, user: User): Promise; - getVersion(app: App, user: User): Promise; + getVersion(app: App, user: User, mode?: string): Promise; update(app: App, user: User, appVersionUpdateDto: AppVersionUpdateDto): Promise; diff --git a/server/src/modules/versions/repository.ts b/server/src/modules/versions/repository.ts index 4cd26d0202..ae9a0b2160 100644 --- a/server/src/modules/versions/repository.ts +++ b/server/src/modules/versions/repository.ts @@ -147,6 +147,34 @@ export class VersionRepository extends Repository { }, manager || this.manager); } + async findVersionWithQueryPermissions(id: string, manager?: EntityManager): Promise { + return await dbTransactionWrap(async (manager: EntityManager) => { + const appVersion = await manager + .createQueryBuilder(AppVersion, 'appVersion') + .where('appVersion.id = :id', { id }) + .leftJoinAndSelect('appVersion.app', 'app') + .leftJoinAndSelect('appVersion.dataQueries', 'dataQueries') + .leftJoinAndSelect('dataQueries.dataSource', 'dataSource') + .leftJoinAndSelect('dataQueries.plugins', 'plugins') + .leftJoinAndSelect('plugins.manifestFile', 'manifestFile') + .leftJoinAndSelect('dataQueries.permissions', 'permission') + .leftJoinAndSelect('permission.users', 'queryUser') + .leftJoinAndSelect('queryUser.user', 'user') + .leftJoinAndSelect('queryUser.permissionGroup', 'group') + .getOneOrFail(); + + if (appVersion?.dataQueries) { + for (const query of appVersion?.dataQueries) { + if (query?.plugin) { + query.plugin.manifestFile.data = JSON.parse(decode(query.plugin.manifestFile.data.toString('utf8'))); + } + } + } + + return appVersion; + }, manager || this.manager); + } + getVersionsInApp(appId: string, manager?: EntityManager): Promise { return dbTransactionWrap((manager: EntityManager) => { return manager.find(AppVersion, { diff --git a/server/src/modules/versions/service.ts b/server/src/modules/versions/service.ts index 5ab17efd5c..65ef4e3b56 100644 --- a/server/src/modules/versions/service.ts +++ b/server/src/modules/versions/service.ts @@ -107,7 +107,7 @@ export class VersionService implements IVersionService { }, manager); } - async getVersion(app: App, user: User): Promise { + async getVersion(app: App, user: User, mode?: string): Promise { const versionId = app.appVersions[0].id; const appVersion = await this.versionRepository.findVersion(versionId); From 3e463227588962d247fb47d6af61f4f3e9883716 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Thu, 12 Jun 2025 21:49:12 +0530 Subject: [PATCH 21/24] Update submodule reference --- server/ee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/ee b/server/ee index 1c46b1b5f0..e54ae1f5ca 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit 1c46b1b5f0150c273adad875a6e8553a00c0f343 +Subproject commit e54ae1f5cabf52e9631f0d2e4cda7f7be3dd1f4f From 5f740a64f73030f7ce186a71c3c830deb542e731 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Wed, 25 Jun 2025 17:19:51 +0530 Subject: [PATCH 22/24] Update submodule ref with feature branch itself --- frontend/ee | 2 +- server/ee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/ee b/frontend/ee index 743e06cd6e..51d0a7fbe9 160000 --- a/frontend/ee +++ b/frontend/ee @@ -1 +1 @@ -Subproject commit 743e06cd6e1233d0b98720354bb7b0d0df098f22 +Subproject commit 51d0a7fbe974919786c938304e2214d46396c033 diff --git a/server/ee b/server/ee index e54ae1f5ca..213bba9801 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit e54ae1f5cabf52e9631f0d2e4cda7f7be3dd1f4f +Subproject commit 213bba98018d82fe2fee0689e5b7bf1a19a85ade From 412f5ad837fb5b95a4df415ca6bd7ec3be6f102d Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Wed, 25 Jun 2025 18:30:12 +0530 Subject: [PATCH 23/24] Fix: App crashing on component drop --- frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx index 05306724fa..f25d815159 100644 --- a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx +++ b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx @@ -53,7 +53,7 @@ export const ConfigHandle = ({ ); }, shallow); - const currentPageIndex = useStore((state) => state.currentPageIndex); + const currentPageIndex = useStore((state) => state.modules.canvas.currentPageIndex); const component = useStore((state) => state.modules.canvas.pages[currentPageIndex].components[id]); const featureAccess = useStore((state) => state?.license?.featureAccess, shallow); const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid; From ba5f51888d69f8a7338e6d7c314dec9c3d1e23dc Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Thu, 26 Jun 2025 00:18:37 +0530 Subject: [PATCH 24/24] Fix: permission icon not updating in component config handle --- frontend/src/AppBuilder/_stores/slices/componentsSlice.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js index 15efdc31dc..9d7c1b77f5 100644 --- a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js @@ -1996,8 +1996,8 @@ export const createComponentsSlice = (set, get) => ({ }); }, setComponentPermission: (componentId, data) => { - const { currentPageId, modules } = get(); - const currentPageIndex = modules.canvas.pages.findIndex((page) => page.id === currentPageId); + const { modules } = get(); + const currentPageIndex = modules.canvas.currentPageIndex; const component = modules.canvas.pages[currentPageIndex]?.components?.[componentId]; if (component) {