diff --git a/frontend/src/Editor/EditorFunc.jsx b/frontend/src/Editor/EditorFunc.jsx index a0c856156e..5542793b67 100644 --- a/frontend/src/Editor/EditorFunc.jsx +++ b/frontend/src/Editor/EditorFunc.jsx @@ -1161,6 +1161,8 @@ const EditorComponent = (props) => { return; } + setCurrentPageId(pageId); + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); copyOfAppDefinition.pages[pageId].name = newName; @@ -1192,12 +1194,17 @@ const EditorComponent = (props) => { name, handle: newHandle, components: {}, - index: Object.keys(copyOfAppDefinition.pages).length, + index: Object.keys(copyOfAppDefinition.pages).length + 1, }; setCurrentPageId(newPageId); - appDefinitionChanged(copyOfAppDefinition, { pageDefinitionChanged: true, switchPage: true, pageId: newPageId }); + appDefinitionChanged(copyOfAppDefinition, { + pageDefinitionChanged: true, + addNewPage: true, + switchPage: true, + pageId: newPageId, + }); }; const switchPage = (pageId, queryParams = []) => { @@ -1288,6 +1295,7 @@ const EditorComponent = (props) => { appDefinitionChanged(newAppDefinition, { pageDefinitionChanged: true, + deletePageRequest: true, }); toast.success(`${toBeDeletedPage.name} page deleted.`); @@ -1447,7 +1455,7 @@ const EditorComponent = (props) => { const pagesObj = newSortedPages.reduce((acc, page, index) => { acc[page.id] = { ...page, - index: index, + index: index + 1, }; return acc; }, {}); @@ -1458,6 +1466,7 @@ const EditorComponent = (props) => { appDefinitionChanged(newAppDefinition, { pageDefinitionChanged: true, + pageSortingChanged: true, }); }; diff --git a/frontend/src/_services/appVersion.service.js b/frontend/src/_services/appVersion.service.js index 9af120a865..2fca17429a 100644 --- a/frontend/src/_services/appVersion.service.js +++ b/frontend/src/_services/appVersion.service.js @@ -66,7 +66,15 @@ function autoSaveApp(appId, versionId, diff, type, pageId, operation, isUserSwit delete: 'DELETE', }); - const body = { is_user_switched_version: isUserSwitchedVersion, pageId, diff: diff }; + let body = {}; + + if ((type === 'pages' && operation === 'create') || operation === 'delete') { + body = { + ...diff, + }; + } else { + body = { is_user_switched_version: isUserSwitchedVersion, pageId, diff: diff }; + } const requestOptions = { method: OPERATION[operation], @@ -74,5 +82,5 @@ function autoSaveApp(appId, versionId, diff, type, pageId, operation, isUserSwit credentials: 'include', body: JSON.stringify(body), }; - return fetch(`${config.apiUrl}/apps/${appId}/versions/${versionId}/${type}`, requestOptions).then(handleResponse); + return fetch(`${config.apiUrl}/v2/apps/${appId}/versions/${versionId}/${type}`, requestOptions).then(handleResponse); } diff --git a/frontend/src/_stores/utils.js b/frontend/src/_stores/utils.js index 428fda719d..921d1b3515 100644 --- a/frontend/src/_stores/utils.js +++ b/frontend/src/_stores/utils.js @@ -48,12 +48,31 @@ export const computeAppDiff = (appDiff, currentPageId, opts) => { let updateDiff; let operation = 'update'; - console.log('---piku [computeAppDiff]', { appDiff, currentPageId, opts }); + if (opts?.deletePageRequest) { + const deletePageId = _.keys(appDiff?.pages).map((pageId) => { + if (appDiff?.pages[pageId]?.pageId === undefined) { + return pageId; + } + })[0]; - if (opts?.pageDefinitionChanged) { + updateDiff = { + pageId: deletePageId, + }; + + type = updateType.pageDefinitionChanged; + operation = 'delete'; + } else if (opts?.pageSortingChanged) { + updateDiff = appDiff?.pages; + + type = updateType.pageDefinitionChanged; + } else if (opts?.pageDefinitionChanged) { updateDiff = appDiff?.pages[currentPageId]; type = updateType.pageDefinitionChanged; + + if (opts?.addNewPage) { + operation = 'create'; + } } else if (opts?.componentDeleted) { const currentPageComponents = appDiff?.pages[currentPageId]?.components; diff --git a/server/migrations/1691004576333-CreatePageTable.ts b/server/migrations/1691004576333-CreatePageTable.ts index 98694a5e34..729cc11349 100644 --- a/server/migrations/1691004576333-CreatePageTable.ts +++ b/server/migrations/1691004576333-CreatePageTable.ts @@ -17,6 +17,11 @@ export class CreatePageTable1691004576333 implements MigrationInterface { type: 'varchar', isNullable: false, }, + { + name: 'index', + type: 'int', + isNullable: false, + }, { name: 'page_handle', type: 'varchar', diff --git a/server/src/controllers/apps.controller.ts b/server/src/controllers/apps.controller.ts index f449fb572e..af89025946 100644 --- a/server/src/controllers/apps.controller.ts +++ b/server/src/controllers/apps.controller.ts @@ -28,7 +28,6 @@ import { EntityManager } from 'typeorm'; import { ValidAppInterceptor } from 'src/interceptors/valid.app.interceptor'; import { AppDecorator } from 'src/decorators/app.decorator'; -import { ComponentsService } from '@services/components.service'; import { PageService } from '@services/page.service'; @Controller('apps') @@ -36,7 +35,6 @@ export class AppsController { constructor( private appsService: AppsService, private foldersService: FoldersService, - private componentsService: ComponentsService, private pageService: PageService, private appsAbilityFactory: AppsAbilityFactory ) {} @@ -323,100 +321,6 @@ export class AppsController { await this.appsService.updateVersion(version, versionEditDto, app.organizationId); return; } - @UseGuards(JwtAuthGuard) - @UseInterceptors(ValidAppInterceptor) - @Post(':id/versions/:versionId/components') - async createComponent( - @User() user, - @Param('id') id, - @Param('versionId') versionId, - @Body() versionEditDto: VersionEditDto - ) { - const version = await this.appsService.findVersion(versionId); - const app = version.app; - - if (app.id !== id) { - throw new BadRequestException(); - } - const ability = await this.appsAbilityFactory.appsActions(user, id); - - if (!ability.can('updateVersions', app)) { - throw new ForbiddenException('You do not have permissions to perform this action'); - } - - await this.componentsService.create(versionEditDto.diff, versionEditDto.pageId); - } - @UseGuards(JwtAuthGuard) - @UseInterceptors(ValidAppInterceptor) - @Put(':id/versions/:versionId/components') - async updateComponent( - @User() user, - @Param('id') id, - @Param('versionId') versionId, - @Body() versionEditDto: VersionEditDto - ) { - const version = await this.appsService.findVersion(versionId); - const app = version.app; - - if (app.id !== id) { - throw new BadRequestException(); - } - const ability = await this.appsAbilityFactory.appsActions(user, id); - - if (!ability.can('updateVersions', app)) { - throw new ForbiddenException('You do not have permissions to perform this action'); - } - - await this.componentsService.update(versionEditDto.diff); - } - - @UseGuards(JwtAuthGuard) - @UseInterceptors(ValidAppInterceptor) - @Delete(':id/versions/:versionId/components') - async deleteComponents( - @User() user, - @Param('id') id, - @Param('versionId') versionId, - @Body() versionEditDto: VersionEditDto - ) { - const version = await this.appsService.findVersion(versionId); - const app = version.app; - - if (app.id !== id) { - throw new BadRequestException(); - } - const ability = await this.appsAbilityFactory.appsActions(user, id); - - if (!ability.can('updateVersions', app)) { - throw new ForbiddenException('You do not have permissions to perform this action'); - } - - await this.componentsService.delete(versionEditDto.diff); - } - - @UseGuards(JwtAuthGuard) - @UseInterceptors(ValidAppInterceptor) - @Put(':id/versions/:versionId/components/layout') - async updateComponentLayput( - @User() user, - @Param('id') id, - @Param('versionId') versionId, - @Body() versionEditDto: VersionEditDto - ) { - const version = await this.appsService.findVersion(versionId); - const app = version.app; - - if (app.id !== id) { - throw new BadRequestException(); - } - const ability = await this.appsAbilityFactory.appsActions(user, id); - - if (!ability.can('updateVersions', app)) { - throw new ForbiddenException('You do not have permissions to perform this action'); - } - - await this.componentsService.componentLayoutChange(versionEditDto.diff); - } @UseGuards(JwtAuthGuard) @UseInterceptors(ValidAppInterceptor) diff --git a/server/src/controllers/apps.controller.v2.ts b/server/src/controllers/apps.controller.v2.ts index 786bda3135..f3247c0bd5 100644 --- a/server/src/controllers/apps.controller.v2.ts +++ b/server/src/controllers/apps.controller.v2.ts @@ -1,12 +1,262 @@ -import { Controller } from '@nestjs/common'; -// import { JwtAuthGuard } from '../../src/modules/auth/jwt-auth.guard'; +import { + Controller, + ForbiddenException, + Get, + Param, + Post, + Put, + Delete, + Query, + UseGuards, + Body, + BadRequestException, + UseInterceptors, +} from '@nestjs/common'; +import { JwtAuthGuard } from '../../src/modules/auth/jwt-auth.guard'; +import { AppsService } from '../services/apps.service'; +import { camelizeKeys, decamelizeKeys } from 'humps'; +import { AppsAbilityFactory } from 'src/modules/casl/abilities/apps-ability.factory'; +// import { AppAuthGuard } from 'src/modules/auth/app-auth.guard'; +import { App } from 'src/entities/app.entity'; +import { User } from 'src/decorators/user.decorator'; + +import { VersionEditDto } from '@dto/version-edit.dto'; +import { CreatePageDto, UpdatePageDto } from '@dto/pages.dto'; + +import { ValidAppInterceptor } from 'src/interceptors/valid.app.interceptor'; +import { AppDecorator } from 'src/decorators/app.decorator'; + +import { ComponentsService } from '@services/components.service'; +import { PageService } from '@services/page.service'; @Controller({ path: 'apps', - version: '2', // Set the version to '2' + version: '2', }) export class AppsControllerV2 { - constructor(/* Add your services and dependencies here */) {} + constructor( + private appsService: AppsService, + private componentsService: ComponentsService, + private pageService: PageService, + private appsAbilityFactory: AppsAbilityFactory + ) {} - // Add your new version 2 methods here + @UseGuards(JwtAuthGuard) + @UseInterceptors(ValidAppInterceptor) + @Get(':id') + async show(@User() user, @AppDecorator() app: App, @Query('access_type') accessType: string) { + const ability = await this.appsAbilityFactory.appsActions(user, app.id); + if (!ability.can('viewApp', app)) { + throw new ForbiddenException( + JSON.stringify({ + organizationId: app.organizationId, + }) + ); + } + + if (accessType === 'edit' && !ability.can('editApp', app)) { + throw new ForbiddenException( + JSON.stringify({ + organizationId: app.organizationId, + }) + ); + } + + const response = decamelizeKeys(app); + + const seralizedQueries = []; + const dataQueriesForVersion = app.editingVersion + ? await this.appsService.findDataQueriesForVersion(app.editingVersion.id) + : []; + + const pagesForVersion = app.editingVersion ? await this.pageService.findPagesForVersion(app.editingVersion.id) : []; + + // serialize queries + for (const query of dataQueriesForVersion) { + const decamelizedQuery = decamelizeKeys(query); + decamelizedQuery['options'] = query.options; + seralizedQueries.push(decamelizedQuery); + } + + response['data_queries'] = seralizedQueries; + response['definition'] = app.editingVersion?.definition; + response['pages'] = pagesForVersion; + + //! if editing version exists, camelize the definition + if (app.editingVersion && app.editingVersion.definition) { + response['editing_version'] = { + ...response['editing_version'], + definition: camelizeKeys(app.editingVersion.definition), + }; + } + return response; + } + + //components api + @UseGuards(JwtAuthGuard) + @UseInterceptors(ValidAppInterceptor) + @Post(':id/versions/:versionId/components') + async createComponent( + @User() user, + @Param('id') id, + @Param('versionId') versionId, + @Body() versionEditDto: VersionEditDto + ) { + const version = await this.appsService.findVersion(versionId); + const app = version.app; + + if (app.id !== id) { + throw new BadRequestException(); + } + const ability = await this.appsAbilityFactory.appsActions(user, id); + + if (!ability.can('updateVersions', app)) { + throw new ForbiddenException('You do not have permissions to perform this action'); + } + + await this.componentsService.create(versionEditDto.diff, versionEditDto.pageId); + } + + @UseGuards(JwtAuthGuard) + @UseInterceptors(ValidAppInterceptor) + @Put(':id/versions/:versionId/components') + async updateComponent( + @User() user, + @Param('id') id, + @Param('versionId') versionId, + @Body() versionEditDto: VersionEditDto + ) { + const version = await this.appsService.findVersion(versionId); + const app = version.app; + + if (app.id !== id) { + throw new BadRequestException(); + } + const ability = await this.appsAbilityFactory.appsActions(user, id); + + if (!ability.can('updateVersions', app)) { + throw new ForbiddenException('You do not have permissions to perform this action'); + } + + await this.componentsService.update(versionEditDto.diff); + } + + @UseGuards(JwtAuthGuard) + @UseInterceptors(ValidAppInterceptor) + @Delete(':id/versions/:versionId/components') + async deleteComponents( + @User() user, + @Param('id') id, + @Param('versionId') versionId, + @Body() versionEditDto: VersionEditDto + ) { + const version = await this.appsService.findVersion(versionId); + const app = version.app; + + if (app.id !== id) { + throw new BadRequestException(); + } + const ability = await this.appsAbilityFactory.appsActions(user, id); + + if (!ability.can('updateVersions', app)) { + throw new ForbiddenException('You do not have permissions to perform this action'); + } + + await this.componentsService.delete(versionEditDto.diff); + } + + @UseGuards(JwtAuthGuard) + @UseInterceptors(ValidAppInterceptor) + @Put(':id/versions/:versionId/components/layout') + async updateComponentLayout( + @User() user, + @Param('id') id, + @Param('versionId') versionId, + @Body() versionEditDto: VersionEditDto + ) { + const version = await this.appsService.findVersion(versionId); + const app = version.app; + + if (app.id !== id) { + throw new BadRequestException(); + } + const ability = await this.appsAbilityFactory.appsActions(user, id); + + if (!ability.can('updateVersions', app)) { + throw new ForbiddenException('You do not have permissions to perform this action'); + } + + await this.componentsService.componentLayoutChange(versionEditDto.diff); + } + + // pages api + @UseGuards(JwtAuthGuard) + @UseInterceptors(ValidAppInterceptor) + @Post(':id/versions/:versionId/pages') + async createPages( + @User() user, + @Param('id') id, + @Param('versionId') versionId, + @Body() createPageDto: CreatePageDto + ) { + const version = await this.appsService.findVersion(versionId); + const app = version.app; + + if (app.id !== id) { + throw new BadRequestException(); + } + const ability = await this.appsAbilityFactory.appsActions(user, id); + + if (!ability.can('updateVersions', app)) { + throw new ForbiddenException('You do not have permissions to perform this action'); + } + + await this.pageService.createPage(createPageDto, versionId); + } + @UseGuards(JwtAuthGuard) + @UseInterceptors(ValidAppInterceptor) + @Put(':id/versions/:versionId/pages') + async updatePages( + @User() user, + @Param('id') id, + @Param('versionId') versionId, + @Body() updatePageDto: Partial + ) { + const version = await this.appsService.findVersion(versionId); + const app = version.app; + + if (app.id !== id) { + throw new BadRequestException(); + } + const ability = await this.appsAbilityFactory.appsActions(user, id); + + if (!ability.can('updateVersions', app)) { + throw new ForbiddenException('You do not have permissions to perform this action'); + } + + await this.pageService.updatePage({ pageId: updatePageDto.pageId, diff: updatePageDto.diff }); + } + + @UseGuards(JwtAuthGuard) + @UseInterceptors(ValidAppInterceptor) + @Delete(':id/versions/:versionId/pages') + async deletePage(@User() user, @Param('id') id, @Param('versionId') versionId, @Body() body) { + const version = await this.appsService.findVersion(versionId); + const app = version.app; + + if (app.id !== id) { + throw new BadRequestException(); + } + const ability = await this.appsAbilityFactory.appsActions(user, id); + + if (!ability.can('updateVersions', app)) { + throw new ForbiddenException('You do not have permissions to perform this action'); + } + + const { pageId } = body; + + if (pageId) { + await this.pageService.deletePage(pageId, versionId); + } + } } diff --git a/server/src/dto/pages.dto.ts b/server/src/dto/pages.dto.ts new file mode 100644 index 0000000000..f72dbe6d6a --- /dev/null +++ b/server/src/dto/pages.dto.ts @@ -0,0 +1,17 @@ +import { IsNumber, IsString } from 'class-validator'; + +export class CreatePageDto { + @IsString() + name: string; + + @IsString() + handle: string; + + @IsNumber() + index: number; +} + +export class UpdatePageDto { + pageId: string; + diff: Partial; +} diff --git a/server/src/entities/page.entity.ts b/server/src/entities/page.entity.ts index 247ae1178a..ee029c6049 100644 --- a/server/src/entities/page.entity.ts +++ b/server/src/entities/page.entity.ts @@ -11,7 +11,10 @@ export class Page { name: string; @Column({ name: 'page_handle' }) - pageHandle: string; + handle: string; + + @Column() + index: number; @Column({ name: 'app_version_id' }) appVersionId: string; diff --git a/server/src/modules/apps/apps.module.ts b/server/src/modules/apps/apps.module.ts index 6923858b8b..695a52d25b 100644 --- a/server/src/modules/apps/apps.module.ts +++ b/server/src/modules/apps/apps.module.ts @@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { App } from '../../entities/app.entity'; import { File } from '../../entities/file.entity'; import { AppsController } from '../../controllers/apps.controller'; +import { AppsControllerV2 } from '../../controllers/apps.controller.v2'; import { AppsService } from '../../services/apps.service'; import { AppVersion } from '../../../src/entities/app_version.entity'; import { DataQuery } from '../../../src/entities/data_query.entity'; @@ -83,6 +84,6 @@ import { PageService } from '@services/page.service'; ComponentsService, PageService, ], - controllers: [AppsController, AppUsersController, AppsImportExportController], + controllers: [AppsController, AppsControllerV2, AppUsersController, AppsImportExportController], }) export class AppsModule {} diff --git a/server/src/services/apps.service.ts b/server/src/services/apps.service.ts index b4a53a5cde..0dceab44f2 100644 --- a/server/src/services/apps.service.ts +++ b/server/src/services/apps.service.ts @@ -123,8 +123,9 @@ export class AppsService { const defaultHomePage = await manager.save( manager.create(Page, { name: 'Defualt Page', - pageHandle: 'defaultpage', + handle: 'defaultpage', appVersionId: appVersion.id, + index: 1, }) ); diff --git a/server/src/services/page.service.ts b/server/src/services/page.service.ts index 38574285f2..27c1966d4b 100644 --- a/server/src/services/page.service.ts +++ b/server/src/services/page.service.ts @@ -4,15 +4,18 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Page } from 'src/entities/page.entity'; import { ComponentsService } from './components.service'; +import { CreatePageDto, UpdatePageDto } from '@dto/pages.dto'; +import { AppsService } from './apps.service'; +import { dbTransactionWrap } from 'src/helpers/utils.helper'; @Injectable() export class PageService { constructor( - private readonly manager: EntityManager, @InjectRepository(Page) private readonly pageRepository: Repository, - private componentsService: ComponentsService + private componentsService: ComponentsService, + private appService: AppsService ) {} async findPagesForVersion(appVersionId: string): Promise { @@ -32,4 +35,83 @@ export class PageService { async findOne(id: string): Promise { return this.pageRepository.findOne(id); } + + async createPage(page: CreatePageDto, appVersionId: string): Promise { + const newPage = { + ...page, + appVersionId: appVersionId, + }; + + return this.pageRepository.save(newPage); + } + + async updatePage(pageUpdates: UpdatePageDto) { + if (Object.keys(pageUpdates.diff).length > 1) { + return this.updatePagesOrder(pageUpdates.diff); + } + + const currentPage = await this.pageRepository.findOne(pageUpdates.pageId); + + if (!currentPage) { + throw new Error('Page not found'); + } + return this.pageRepository.update(pageUpdates.pageId, pageUpdates.diff); + } + + async updatePagesOrder(pages) { + const pagesToPage = Object.keys(pages).map((pageId) => { + return { + id: pageId, + index: pages[pageId].index, + }; + }); + + return await dbTransactionWrap(async (manager: EntityManager) => { + await Promise.all( + pagesToPage.map(async (page) => { + await manager.update(Page, page.id, page); + }) + ); + }); + } + + async deletePage(pageId: string, appVersionId: string) { + const pageExists = await this.pageRepository.findOne(pageId); + const { editingVersion } = await this.appService.findAppFromVersion(appVersionId); + + if (!pageExists) { + throw new Error('Page not found'); + } + + if (editingVersion?.homePageId === pageId) { + throw new Error('Cannot delete home page'); + } + const pageDeletedIndex = pageExists.index; + const pageDeleted = await this.pageRepository.delete(pageId); + + if (pageDeleted.affected === 0) { + throw new Error('Page not deleted'); + } + + const pages = await this.pageRepository.find({ appVersionId: pageExists.appVersionId }); + + const rearrangedPages = this.rearrangePagesOnDelete(pages, pageDeletedIndex); + + await this.pageRepository.save(rearrangedPages); + } + + rearrangePagesOnDelete(pages: Page[], pageDeletedIndex: number) { + const rearrangedPages = pages.map((page, index) => { + if (index + 1 >= pageDeletedIndex) { + return { + ...page, + index: page.index - 1, + }; + } + + return page; + }); + + return rearrangedPages; + } }