diff --git a/frontend/src/Editor/EditorFunc.jsx b/frontend/src/Editor/EditorFunc.jsx index 39ecc0bb99..33775c9d15 100644 --- a/frontend/src/Editor/EditorFunc.jsx +++ b/frontend/src/Editor/EditorFunc.jsx @@ -1262,74 +1262,31 @@ const EditorComponent = (props) => { }); }; - const clonePage = (pageId) => { - const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); + const clonePage = async (pageId) => { + await appVersionService.clonePage(appId, editingVersion?.id, pageId).then((data) => { + const copyOfAppDefinition = JSON.parse(JSON.stringify(appDefinition)); - updateEditorState({ - isUpdatingEditorStateInProcess: true, - }); + const pages = data.pages.reduce((acc, page) => { + const currentComponents = buildComponentMetaDefinition(_.cloneDeep(page?.components)); - const currentPage = copyOfAppDefinition.pages[pageId]; - const newPageId = uuid(); - let newPageName = `${currentPage.name} (copy)`; - let newPageHandle = `${currentPage.handle}-copy`; - let i = 1; - while ( - !isNull(copyOfAppDefinition.pages[pageId]?.pages) && - !isUndefined(copyOfAppDefinition.pages[pageId]?.pages) && - Object.values(copyOfAppDefinition.pages[pageId]?.pages)?.some((page) => page.handle === newPageHandle) - ) { - newPageName = `${currentPage.name} (copy ${i})`; - newPageHandle = `${currentPage.handle}-copy-${i}`; - i++; - } + page.components = currentComponents; + + acc[page.id] = page; - const newPageData = cloneDeep(currentPage); - const oldToNewIdMapping = {}; - if (!isEmpty(currentPage?.components)) { - newPageData.components = Object.keys(newPageData.components).reduce((acc, key) => { - const newComponentId = uuid(); - acc[newComponentId] = newPageData.components[key]; - acc[newComponentId].id = newComponentId; - oldToNewIdMapping[key] = newComponentId; return acc; }, {}); - Object.values(newPageData.components).map((comp) => { - if (comp.parent) { - let newParentId = oldToNewIdMapping[comp.parent]; - if (newParentId) { - comp.parent = newParentId; - } else { - const oldParentId = Object.keys(oldToNewIdMapping).find( - (parentId) => - comp.parent.startsWith(parentId) && - ['Tabs', 'Calendar'].includes(currentPage?.components[parentId]?.component?.component) - ); - const childTabId = comp.parent.split('-').at(-1); - comp.parent = `${oldToNewIdMapping[oldParentId]}-${childTabId}`; - } - } - return comp; + const newAppDefinition = { + ...copyOfAppDefinition, + pages: { + ...copyOfAppDefinition.pages, + ...pages, + }, + }; + updateState({ + events: data.events, }); - } - - const newPage = { - ...newPageData, - name: newPageName, - handle: newPageHandle, - }; - - const newAppDefinition = { - ...copyOfAppDefinition, - pages: { - ...copyOfAppDefinition.pages, - [newPageId]: newPage, - }, - }; - - appDefinitionChanged(newAppDefinition, { - pageDefinitionChanged: true, + appDefinitionChanged(newAppDefinition); }); }; diff --git a/frontend/src/_services/appVersion.service.js b/frontend/src/_services/appVersion.service.js index 6a41cd96cb..90b13eab0e 100644 --- a/frontend/src/_services/appVersion.service.js +++ b/frontend/src/_services/appVersion.service.js @@ -11,6 +11,7 @@ export const appVersionService = { saveAppVersionEventHandlers, createAppVersionEventHandler, deleteAppVersionEventHandler, + clonePage, }; function getAll(appId) { @@ -142,3 +143,14 @@ function deleteAppVersionEventHandler(appId, versionId, eventId) { handleResponse ); } + +function clonePage(appId, versionId, pageId) { + const requestOptions = { + method: 'POST', + headers: authHeader(), + credentials: 'include', + }; + return fetch(`${config.apiUrl}/v2/apps/${appId}/versions/${versionId}/pages/${pageId}/clone`, requestOptions).then( + handleResponse + ); +} diff --git a/server/src/controllers/apps.controller.v2.ts b/server/src/controllers/apps.controller.v2.ts index b7b7d51ac9..3cc5fb86db 100644 --- a/server/src/controllers/apps.controller.v2.ts +++ b/server/src/controllers/apps.controller.v2.ts @@ -263,6 +263,22 @@ export class AppsControllerV2 { await this.pageService.createPage(createPageDto, versionId); } + + @UseGuards(JwtAuthGuard) + @UseInterceptors(ValidAppInterceptor) + @Post(':id/versions/:versionId/pages/:pageId/clone') + async clonePage(@User() user, @Param('id') id, @Param('versionId') versionId, @Param('pageId') pageId) { + const version = await this.appsService.findVersion(versionId); + const app = version.app; + 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'); + } + + return await this.pageService.clonePage(pageId, versionId); + } + @UseGuards(JwtAuthGuard) @UseInterceptors(ValidAppInterceptor) @Put(':id/versions/:versionId/pages') diff --git a/server/src/services/page.service.ts b/server/src/services/page.service.ts index c2a5661598..12a7ad0074 100644 --- a/server/src/services/page.service.ts +++ b/server/src/services/page.service.ts @@ -8,6 +8,9 @@ import { CreatePageDto, UpdatePageDto } from '@dto/pages.dto'; import { AppsService } from './apps.service'; import { dbTransactionWrap } from 'src/helpers/utils.helper'; import { EventsService } from './events_handler.service'; +import { Component } from 'src/entities/component.entity'; +import { Layout } from 'src/entities/layout.entity'; +import { EventHandler } from 'src/entities/event_handler.entity'; @Injectable() export class PageService { @@ -49,6 +52,74 @@ export class PageService { return this.pageRepository.save(newPage); } + async clonePage(pageId: string, appVersionId: string) { + const pageToClone = await this.pageRepository.findOne(pageId); + + if (!pageToClone) { + throw new Error('Page not found'); + } + + const newPage = new Page(); + newPage.name = `${pageToClone.name} copy`; + newPage.handle = `${pageToClone.handle}-copy`; + newPage.index = pageToClone.index + 1; + newPage.appVersionId = appVersionId; + + const clonedpage = await this.pageRepository.save(newPage); + + await this.clonePageEventsAndComponents(pageId, clonedpage.id, appVersionId); + + const pages = await this.findPagesForVersion(appVersionId); + const events = await this.eventHandlerService.findEventsForVersion(appVersionId); + + return { pages, events }; + } + + async clonePageEventsAndComponents(pageId: string, clonePageId: string, appVersionId: string) { + return dbTransactionWrap(async (manager: EntityManager) => { + const pageComponents = await manager.find(Component, { pageId }); + const pageEvents = await this.eventHandlerService.findAllEventsWithSourceId(pageId); + + // Clone events + await Promise.all( + pageEvents.map(async (event) => { + const clonedEvent = { ...event, id: undefined, sourceId: clonePageId }; + await manager.save(EventHandler, clonedEvent); + }) + ); + + // Clone components + const clonedComponents = await Promise.all( + pageComponents.map(async (component) => { + const clonedComponent = { ...component, id: undefined, pageId: clonePageId }; + const newComponent = await manager.save(Component, clonedComponent); + + const componentLayouts = await manager.find(Layout, { componentId: component.id }); + const clonedLayouts = componentLayouts.map((layout) => ({ + ...layout, + id: undefined, + componentId: newComponent.id, + })); + + // Clone component events + const clonedComponentEvents = await this.eventHandlerService.findAllEventsWithSourceId(component.id); + const clonedEvents = clonedComponentEvents.map((event) => ({ + ...event, + id: undefined, + sourceId: newComponent.id, + })); + + await manager.save(Layout, clonedLayouts); + await manager.save(EventHandler, clonedEvents); + + return newComponent; + }) + ); + + return clonedComponents; + }); + } + async updatePage(pageUpdates: UpdatePageDto) { if (Object.keys(pageUpdates.diff).length > 1) { return this.updatePagesOrder(pageUpdates.diff);