diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/ApplicationTemplateControllerCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/ApplicationTemplateControllerCE.java index 62ff1ed57a..e4a5b490ba 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/ApplicationTemplateControllerCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/ApplicationTemplateControllerCE.java @@ -1,5 +1,6 @@ package com.appsmith.server.controllers.ce; +import com.appsmith.server.constants.FieldName; import com.appsmith.server.domains.Application; import com.appsmith.server.dtos.ApplicationTemplate; import com.appsmith.server.dtos.ResponseDTO; @@ -9,6 +10,8 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import reactor.core.publisher.Mono; import java.util.List; @@ -58,4 +61,14 @@ public class ApplicationTemplateControllerCE { return applicationTemplateService.getRecentlyUsedTemplates().collectList() .map(templates -> new ResponseDTO<>(HttpStatus.OK.value(), templates, null)); } + + @PostMapping("{templateId}/merge/{applicationId}/{organizationId}") + public Mono> mergeTemplateWithApplication(@PathVariable String templateId, + @PathVariable String applicationId, + @PathVariable String organizationId, + @RequestHeader(name = FieldName.BRANCH_NAME, required = false) String branchName, + @RequestBody(required = false) List pagesToImport) { + return applicationTemplateService.mergeTemplateWithApplication(templateId, applicationId, organizationId, branchName, pagesToImport) + .map(importedApp -> new ResponseDTO<>(HttpStatus.OK.value(), importedApp, null)); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ApplicationTemplateServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ApplicationTemplateServiceCE.java index 0654a39e46..914bcdb7c4 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ApplicationTemplateServiceCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ApplicationTemplateServiceCE.java @@ -13,5 +13,6 @@ public interface ApplicationTemplateServiceCE { Flux getRecentlyUsedTemplates(); Mono getTemplateDetails(String templateId); Mono importApplicationFromTemplate(String templateId, String organizationId); + Mono mergeTemplateWithApplication(String templateId, String applicationId, String organizationId, String branchName, List pagesToImport); Mono getFilters(); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ApplicationTemplateServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ApplicationTemplateServiceCEImpl.java index 606da44bd1..1e84852a22 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ApplicationTemplateServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ApplicationTemplateServiceCEImpl.java @@ -31,6 +31,7 @@ import java.util.Comparator; import java.util.List; import java.util.HashMap; import java.util.Map; +import java.util.List; @Service public class ApplicationTemplateServiceCEImpl implements ApplicationTemplateServiceCE { @@ -205,4 +206,13 @@ public class ApplicationTemplateServiceCEImpl implements ApplicationTemplateServ super.setEncodingMode(EncodingMode.NONE); } } + + @Override + public Mono mergeTemplateWithApplication(String templateId, String applicationId, String organizationId, String branchName, List pagesToImport) { + return getApplicationJsonFromTemplate(templateId).flatMap(applicationJson -> + importExportApplicationService.mergeApplicationJsonWithApplication( + organizationId, applicationId, null, applicationJson, pagesToImport + ) + ); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ThemeServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ThemeServiceCE.java index e607455d62..c54c61765f 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ThemeServiceCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ThemeServiceCE.java @@ -2,6 +2,7 @@ package com.appsmith.server.services.ce; import com.appsmith.server.acl.AclPermission; import com.appsmith.server.domains.Application; +import com.appsmith.server.domains.ApplicationJson; import com.appsmith.server.domains.ApplicationMode; import com.appsmith.server.domains.Theme; import com.appsmith.server.services.CrudService; @@ -38,4 +39,5 @@ public interface ThemeServiceCE extends CrudService { Mono updateName(String id, Theme theme); Mono getOrSaveTheme(Theme theme, Application destApplication); Mono archiveApplicationThemes(Application application); + Mono importThemesToApplication(Application destinationApp, ApplicationJson sourceJson); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ThemeServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ThemeServiceCEImpl.java index 134e9def83..90dae873e2 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ThemeServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ThemeServiceCEImpl.java @@ -1,10 +1,11 @@ package com.appsmith.server.services.ce; +import com.appsmith.external.constants.AnalyticsEvents; import com.appsmith.server.acl.AclPermission; import com.appsmith.server.acl.PolicyGenerator; -import com.appsmith.external.constants.AnalyticsEvents; import com.appsmith.server.constants.FieldName; import com.appsmith.server.domains.Application; +import com.appsmith.server.domains.ApplicationJson; import com.appsmith.server.domains.ApplicationMode; import com.appsmith.server.domains.Theme; import com.appsmith.server.exceptions.AppsmithError; @@ -391,12 +392,18 @@ public class ThemeServiceCEImpl extends BaseService importThemesToApplication(Application destinationApp, ApplicationJson sourceJson) { + Mono editModeTheme = updateExistingAppThemeFromJSON( + destinationApp, destinationApp.getEditModeThemeId(), sourceJson.getEditModeTheme() + ); + + Mono publishedModeTheme = updateExistingAppThemeFromJSON( + destinationApp, destinationApp.getPublishedModeThemeId(), sourceJson.getPublishedTheme() + ); + + return Mono.zip(editModeTheme, publishedModeTheme).flatMap(importedThemesTuple -> { + String editModeThemeId = importedThemesTuple.getT1().getId(); + String publishedModeThemeId = importedThemesTuple.getT2().getId(); + + destinationApp.setEditModeThemeId(editModeThemeId); + destinationApp.setPublishedModeThemeId(publishedModeThemeId); + // this will update the theme id in DB + // also returning the updated application object so that theme id are available to the next pipeline + return applicationService.setAppTheme( + destinationApp.getId(), editModeThemeId, publishedModeThemeId, MANAGE_APPLICATIONS + ).thenReturn(destinationApp); + }); + } + + private Mono updateExistingAppThemeFromJSON(Application destinationApp, String existingThemeId, Theme themeFromJson) { + if(!StringUtils.hasLength(existingThemeId)) { + return getOrSaveTheme(themeFromJson, destinationApp); + } + return repository.findById(existingThemeId, READ_THEMES).flatMap(existingTheme -> { + if(existingTheme.isSystemTheme()) { + return getOrSaveTheme(themeFromJson, destinationApp); + } else { + if(themeFromJson.isSystemTheme()) { + return getOrSaveTheme(themeFromJson, destinationApp).flatMap(importedTheme -> { + // need to delete the old existingTheme + return repository.archiveById(existingThemeId).thenReturn(importedTheme); + }); + } else { + return repository.updateById(existingThemeId, themeFromJson, MANAGE_THEMES); + } + } + }); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ImportExportApplicationServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ImportExportApplicationServiceImpl.java index 47a83c2835..9fe923ff9f 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ImportExportApplicationServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ImportExportApplicationServiceImpl.java @@ -1,6 +1,10 @@ package com.appsmith.server.solutions; import com.appsmith.server.helpers.PolicyUtils; +import com.appsmith.server.domains.Application; +import com.appsmith.server.domains.ApplicationJson; +import com.appsmith.server.exceptions.AppsmithError; +import com.appsmith.server.exceptions.AppsmithException; import com.appsmith.server.repositories.ActionCollectionRepository; import com.appsmith.server.repositories.DatasourceRepository; import com.appsmith.server.repositories.NewActionRepository; @@ -19,7 +23,11 @@ import com.appsmith.server.services.SessionUserService; import com.appsmith.server.services.ThemeService; import com.appsmith.server.solutions.ce.ImportExportApplicationServiceCEImpl; import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.http.MediaType; +import org.springframework.http.codec.multipart.Part; import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; @Slf4j @Component diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ImportExportApplicationServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ImportExportApplicationServiceCE.java index f20aba6919..cd41e4974c 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ImportExportApplicationServiceCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ImportExportApplicationServiceCE.java @@ -32,6 +32,8 @@ public interface ImportExportApplicationServiceCE { */ Mono extractFileAndSaveApplication(String orgId, Part filePart); + Mono mergeApplicationJsonWithApplication(String organizationId, String applicationId, String branchName, ApplicationJson applicationJson, List pagesToImport); + /** * This function will save the application to workspace from the application resource * diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ImportExportApplicationServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ImportExportApplicationServiceCEImpl.java index a9362f573f..952c683f8c 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ImportExportApplicationServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ImportExportApplicationServiceCEImpl.java @@ -34,6 +34,7 @@ import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; import com.appsmith.server.helpers.DefaultResourcesUtils; import com.appsmith.server.helpers.PolicyUtils; +import com.appsmith.server.helpers.TextUtils; import com.appsmith.server.migrations.ApplicationVersion; import com.appsmith.server.migrations.JsonSchemaMigration; import com.appsmith.server.migrations.JsonSchemaVersions; @@ -631,19 +632,48 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica return importApplicationInWorkspace(workspaceId, importedDoc, null, null); } + public Mono importApplicationInWorkspace(String workspaceId, + ApplicationJson applicationJson, + String applicationId, + String branchName) { + return importApplicationInWorkspace(workspaceId, applicationJson, applicationId, branchName, false); + } + + /** + * validates whether a ApplicationJSON contains the required fields or not. + * @param importedDoc ApplicationJSON object that needs to be validated + * @return Name of the field that have error. Empty string otherwise + */ + private String validateApplicationJson(ApplicationJson importedDoc) { + String errorField = ""; + if (CollectionUtils.isEmpty(importedDoc.getPageList())) { + errorField = FieldName.PAGES; + } else if (importedDoc.getExportedApplication() == null) { + errorField = FieldName.APPLICATION; + } else if (importedDoc.getActionList() == null) { + errorField = FieldName.ACTIONS; + } else if (importedDoc.getDatasourceList() == null) { + errorField = FieldName.DATASOURCE; + } + + return errorField; + } + /** * This function will take the application reference object to hydrate the application in mongoDB * * @param workspaceId workspace to which application is going to be stored * @param applicationJson application resource which contains necessary information to import the application * @param applicationId application which needs to be saved with the updated resources + * @param branchName name of the branch of application with applicationId + * @param appendToApp whether applicationJson will be appended to the existing app or not * @return Updated application */ - public Mono importApplicationInWorkspace(String workspaceId, - ApplicationJson applicationJson, - String applicationId, - String branchName) { - + private Mono importApplicationInWorkspace(String workspaceId, + ApplicationJson applicationJson, + String applicationId, + String branchName, + boolean appendToApp) { /* 1. Migrate resource to latest schema 2. Fetch workspace by id @@ -653,10 +683,18 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica 6. Extract and save pages in the application 7. Extract and save actions in the application */ + // Start the stopwatch to log the execution time + Stopwatch processStopwatch = new Stopwatch("Import application"); ApplicationJson importedDoc = JsonSchemaMigration.migrateApplicationToLatestSchema(applicationJson); + // check for validation error and raise exception if error found + String errorField = validateApplicationJson(importedDoc); + if (!errorField.isEmpty()) { + return Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, errorField, INVALID_JSON_FILE)); + } + Map pluginMap = new HashMap<>(); Map datasourceMap = new HashMap<>(); Map pageNameMap = new HashMap<>(); @@ -681,20 +719,6 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica .findAllByOrganizationId(workspaceId, MANAGE_DATASOURCES) .cache(); - String errorField = ""; - if (CollectionUtils.isEmpty(importedNewPageList)) { - errorField = FieldName.PAGES; - } else if (importedApplication == null) { - errorField = FieldName.APPLICATION; - } else if (importedNewActionList == null) { - errorField = FieldName.ACTIONS; - } else if (importedDatasourceList == null) { - errorField = FieldName.DATASOURCE; - } - - if (!errorField.isEmpty()) { - return Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, errorField, INVALID_JSON_FILE)); - } assert importedApplication != null: "Received invalid application object!"; if(importedApplication.getApplicationVersion() == null) { importedApplication.setApplicationVersion(ApplicationVersion.EARLIEST_VERSION); @@ -722,7 +746,6 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica return Mono.just(new ArrayList()); }) .flatMapMany(existingDatasources -> { - if (CollectionUtils.isEmpty(importedDatasourceList)) { return Mono.empty(); } @@ -810,6 +833,9 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica applicationId)) ) .flatMap(existingApplication -> { + if(StringUtils.isEmpty(branchName)) { + return Mono.just(existingApplication); + } importedApplication.setId(existingApplication.getId()); // For the existing application we don't need to default value of the flag // The isPublic flag has a default value as false and this would be confusing to user @@ -845,7 +871,7 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica return applicationPageService.createOrUpdateSuffixedApplication(application, application.getName(), 0); }) ) - .flatMap(savedApp -> importThemes(savedApp, importedDoc)) + .flatMap(savedApp -> importThemes(savedApp, importedDoc, appendToApp)) .flatMap(savedApp -> { importedApplication.setId(savedApp.getId()); if (savedApp.getGitApplicationMetadata() != null) { @@ -855,20 +881,49 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica // Import and save pages, also update the pages related fields in saved application assert importedNewPageList != null: "Unable to find pages in the imported application"; + if(appendToApp) { + // add existing pages to importedApplication so that they are not lost + // when we update application from importedApplication + importedApplication.setPages(savedApp.getPages()); + } + // For git-sync this will not be empty Mono> existingPagesMono = newPageService .findNewPagesByApplicationId(importedApplication.getId(), MANAGE_PAGES) .collectList() .cache(); - return importAndSavePages( + Flux importNewPageFlux = importAndSavePages( importedNewPageList, - importedApplication, + savedApp, importedDoc.getPublishedLayoutmongoEscapedWidgets(), importedDoc.getUnpublishedLayoutmongoEscapedWidgets(), branchName, existingPagesMono - ) + ); + Flux importedNewPagesMono; + + if(appendToApp) { + // we need to rename page if there is a conflict + // also need to remap the renamed page + importedNewPagesMono = updateNewPagesBeforeMerge(existingPagesMono, importedNewPageList) + .flatMapMany(newToOldNameMap-> + importNewPageFlux.map(newPage -> { + // we need to map the newly created page with old name + // because other related resources e.g. actions will refer the page with old name + String newPageName = newPage.getUnpublishedPage().getName(); + String oldPageName = newToOldNameMap.get(newPageName); + if(!newPageName.equals(oldPageName)) { + renamePageInActions(importedNewActionList, oldPageName, newPageName); + renamePageInActionCollections(importedActionCollectionList, oldPageName, newPageName); + } + return newPage; + }) + ); + } else { + importedNewPagesMono = importNewPageFlux; + } + importedNewPagesMono = importedNewPagesMono .map(newPage -> { // Save the map of pageName and NewPage if (newPage.getUnpublishedPage() != null && newPage.getUnpublishedPage().getName() != null) { @@ -878,75 +933,15 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica pageNameMap.put(newPage.getPublishedPage().getName(), newPage); } return newPage; - }) + }); + + return importedNewPagesMono .collectList() .map(newPageList -> { - Map> applicationPages = Map.of( - PublishType.UNPUBLISHED, new ArrayList<>(), - PublishType.PUBLISHED, new ArrayList<>() - ); - // Reorder the pages based on edit mode page sequence - List pageOrderList; - if(!CollectionUtils.isEmpty(applicationJson.getPageOrder())) { - pageOrderList = applicationJson.getPageOrder(); - } else { - pageOrderList = pageNameMap.values() - .stream() - .map(newPage -> newPage.getUnpublishedPage().getName()) - .collect(Collectors.toList()); - } - for(String pageName : pageOrderList) { - NewPage newPage = pageNameMap.get(pageName); - ApplicationPage unpublishedAppPage = new ApplicationPage(); - if (newPage.getUnpublishedPage() != null && newPage.getUnpublishedPage().getName() != null) { - unpublishedAppPage.setIsDefault( - StringUtils.equals( - newPage.getUnpublishedPage().getName(), importedDoc.getUnpublishedDefaultPageName() - ) - ); - unpublishedAppPage.setId(newPage.getId()); - if (newPage.getDefaultResources() != null) { - unpublishedAppPage.setDefaultPageId(newPage.getDefaultResources().getPageId()); - } - } - if (unpublishedAppPage.getId() != null && newPage.getUnpublishedPage().getDeletedAt() == null) { - applicationPages.get(PublishType.UNPUBLISHED).add(unpublishedAppPage); - } - } - - // Reorder the pages based on view mode page sequence - List publishedPageOrderList; - if(!CollectionUtils.isEmpty(applicationJson.getPublishedPageOrder())) { - publishedPageOrderList = applicationJson.getPublishedPageOrder(); - } else { - publishedPageOrderList = pageNameMap.values() - .stream() - .filter(newPage -> Optional.ofNullable(newPage.getPublishedPage()).isPresent()) - .map(newPage -> newPage.getPublishedPage().getName()).collect(Collectors.toList()); - } - for(String pageName : publishedPageOrderList) { - NewPage newPage = pageNameMap.get(pageName); - ApplicationPage publishedAppPage = new ApplicationPage(); - if (newPage.getPublishedPage() != null && newPage.getPublishedPage().getName() != null) { - publishedAppPage.setIsDefault( - StringUtils.equals( - newPage.getPublishedPage().getName(), importedDoc.getPublishedDefaultPageName() - ) - ); - publishedAppPage.setId(newPage.getId()); - if (newPage.getDefaultResources() != null) { - publishedAppPage.setDefaultPageId(newPage.getDefaultResources().getPageId()); - } - } - - if (publishedAppPage.getId() != null && newPage.getPublishedPage().getDeletedAt() == null) { - applicationPages.get(PublishType.PUBLISHED).add(publishedAppPage); - } - } - return applicationPages; + return reorderPages(applicationJson, importedDoc, pageNameMap); }) .flatMap(applicationPages -> { - if (!StringUtils.isEmpty(applicationId)) { + if (!StringUtils.isEmpty(applicationId) && !appendToApp) { Set validPageIds = applicationPages.get(PublishType.UNPUBLISHED).stream() .map(ApplicationPage::getId) .collect(Collectors.toSet()); @@ -984,9 +979,19 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica }) .flatMap(applicationPageMap -> { // set it based on the order for published and unublished pages - importedApplication.setPages(applicationPageMap.get(PublishType.UNPUBLISHED)); - importedApplication.setPublishedPages(applicationPageMap.get(PublishType.PUBLISHED)); - + if(appendToApp) { + if(importedApplication.getPages() == null) { + importedApplication.setPages(new ArrayList<>()); + } + // new pages should not be a default one, existing default page should not change + applicationPageMap.get(PublishType.UNPUBLISHED) + .forEach(applicationPage -> applicationPage.setIsDefault(false)); + // append the new pages so that existing pages not lost when we update later + importedApplication.getPages().addAll(applicationPageMap.get(PublishType.UNPUBLISHED)); + } else { + importedApplication.setPages(applicationPageMap.get(PublishType.UNPUBLISHED)); + importedApplication.setPublishedPages(applicationPageMap.get(PublishType.PUBLISHED)); + } // This will be non-empty for GIT sync return newActionRepository.findByApplicationId(importedApplication.getId()) .collectList(); @@ -1005,34 +1010,34 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica publishedCollectionIdToActionIdsMap, applicationJson.getInvisibleActionFields() ) - .map(NewAction::getId) - .collectList() - .flatMap(savedActionIds -> { - // Updating the existing application for git-sync - if (!StringUtils.isEmpty(applicationId)) { - // Remove unwanted actions - Set invalidActionIds = new HashSet<>(); - for (NewAction action : existingActions) { - if (!savedActionIds.contains(action.getId())) { - invalidActionIds.add(action.getId()); + .map(NewAction::getId) + .collectList() + .flatMap(savedActionIds -> { + // Updating the existing application for git-sync + if (!StringUtils.isEmpty(applicationId) && !appendToApp) { + // Remove unwanted actions + Set invalidActionIds = new HashSet<>(); + for (NewAction action : existingActions) { + if (!savedActionIds.contains(action.getId())) { + invalidActionIds.add(action.getId()); + } + } + return Flux.fromIterable(invalidActionIds) + .flatMap(actionId -> newActionService.deleteUnpublishedAction(actionId) + // return an empty action so that the filter can remove it from the list + .onErrorResume(throwable -> { + log.debug("Failed to delete action with id {} during import", actionId); + log.error(throwable.getMessage()); + return Mono.empty(); + }) + ) + .then() + .thenReturn(savedActionIds); } - } - return Flux.fromIterable(invalidActionIds) - .flatMap(actionId -> newActionService.deleteUnpublishedAction(actionId) - // return an empty action so that the filter can remove it from the list - .onErrorResume(throwable -> { - log.debug("Failed to delete action with id {} during import", actionId); - log.error(throwable.getMessage()); - return Mono.empty(); - }) - ) - .then() - .thenReturn(savedActionIds); - } - return Mono.just(savedActionIds); - }) - .thenMany(actionCollectionRepository.findByApplicationId(importedApplication.getId())) - .collectList() + return Mono.just(savedActionIds); + }) + .thenMany(actionCollectionRepository.findByApplicationId(importedApplication.getId())) + .collectList() ) .flatMap(existingActionCollections -> { if (importedActionCollectionList == null) { @@ -1047,46 +1052,46 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica pageNameMap, pluginMap, unpublishedCollectionIdToActionIdsMap, publishedCollectionIdToActionIdsMap - ) - .flatMap(tuple -> { - final String importedActionCollectionId = tuple.getT1(); - ActionCollection savedActionCollection = tuple.getT2(); - savedCollectionIds.add(savedActionCollection.getId()); - return updateActionsWithImportedCollectionIds( - importedActionCollectionId, - savedActionCollection, - unpublishedCollectionIdToActionIdsMap, - publishedCollectionIdToActionIdsMap, - unpublishedActionIdToCollectionIdMap, - publishedActionIdToCollectionIdMap - ); - }) - .collectList() - .flatMap(ignore -> { - // Updating the existing application for git-sync - if (!StringUtils.isEmpty(applicationId)) { - // Remove unwanted actions - Set invalidCollectionIds = new HashSet<>(); - for (ActionCollection collection : existingActionCollections) { - if (!savedCollectionIds.contains(collection.getId())) { - invalidCollectionIds.add(collection.getId()); + ) + .flatMap(tuple -> { + final String importedActionCollectionId = tuple.getT1(); + ActionCollection savedActionCollection = tuple.getT2(); + savedCollectionIds.add(savedActionCollection.getId()); + return updateActionsWithImportedCollectionIds( + importedActionCollectionId, + savedActionCollection, + unpublishedCollectionIdToActionIdsMap, + publishedCollectionIdToActionIdsMap, + unpublishedActionIdToCollectionIdMap, + publishedActionIdToCollectionIdMap + ); + }) + .collectList() + .flatMap(ignore -> { + // Updating the existing application for git-sync + if (!StringUtils.isEmpty(applicationId) && !appendToApp) { + // Remove unwanted action collections + Set invalidCollectionIds = new HashSet<>(); + for (ActionCollection collection : existingActionCollections) { + if (!savedCollectionIds.contains(collection.getId())) { + invalidCollectionIds.add(collection.getId()); + } } + return Flux.fromIterable(invalidCollectionIds) + .flatMap(collectionId -> actionCollectionService.deleteUnpublishedActionCollection(collectionId) + // return an empty collection so that the filter can remove it from the list + .onErrorResume(throwable -> { + log.debug("Failed to delete collection with id {} during import", collectionId); + log.error(throwable.getMessage()); + return Mono.empty(); + }) + ) + .then() + .thenReturn(savedCollectionIds); } - return Flux.fromIterable(invalidCollectionIds) - .flatMap(collectionId -> actionCollectionService.deleteUnpublishedActionCollection(collectionId) - // return an empty collection so that the filter can remove it from the list - .onErrorResume(throwable -> { - log.debug("Failed to delete collection with id {} during import", collectionId); - log.error(throwable.getMessage()); - return Mono.empty(); - }) - ) - .then() - .thenReturn(savedCollectionIds); - } - return Mono.just(savedCollectionIds); - }) - .thenReturn(true); + return Mono.just(savedCollectionIds); + }) + .thenReturn(true); }) .flatMap(ignored -> { // Don't update gitAuth as we are using @Encrypted for private key @@ -1133,6 +1138,88 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica ); } + private void renamePageInActions(List newActionList, String oldPageName, String newPageName) { + for(NewAction newAction : newActionList) { + if(newAction.getUnpublishedAction().getPageId().equals(oldPageName)) { + newAction.getUnpublishedAction().setPageId(newPageName); + } + } + } + + private void renamePageInActionCollections(List actionCollectionList, String oldPageName, String newPageName) { + for(ActionCollection actionCollection : actionCollectionList) { + if(actionCollection.getUnpublishedCollection().getPageId().equals(oldPageName)) { + actionCollection.getUnpublishedCollection().setPageId(newPageName); + } + } + } + + private Map> reorderPages(ApplicationJson applicationJson, ApplicationJson importedDoc, Map pageNameMap) { + Map> applicationPages = Map.of( + PublishType.UNPUBLISHED, new ArrayList<>(), + PublishType.PUBLISHED, new ArrayList<>() + ); + // Reorder the pages based on edit mode page sequence + List pageOrderList; + if(!CollectionUtils.isEmpty(applicationJson.getPageOrder())) { + pageOrderList = applicationJson.getPageOrder(); + } else { + pageOrderList = pageNameMap.values() + .stream() + .map(newPage -> newPage.getUnpublishedPage().getName()) + .collect(Collectors.toList()); + } + for(String pageName : pageOrderList) { + NewPage newPage = pageNameMap.get(pageName); + ApplicationPage unpublishedAppPage = new ApplicationPage(); + if (newPage.getUnpublishedPage() != null && newPage.getUnpublishedPage().getName() != null) { + unpublishedAppPage.setIsDefault( + StringUtils.equals( + newPage.getUnpublishedPage().getName(), importedDoc.getUnpublishedDefaultPageName() + ) + ); + unpublishedAppPage.setId(newPage.getId()); + if (newPage.getDefaultResources() != null) { + unpublishedAppPage.setDefaultPageId(newPage.getDefaultResources().getPageId()); + } + } + if (unpublishedAppPage.getId() != null && newPage.getUnpublishedPage().getDeletedAt() == null) { + applicationPages.get(PublishType.UNPUBLISHED).add(unpublishedAppPage); + } + } + + // Reorder the pages based on view mode page sequence + List publishedPageOrderList; + if(!CollectionUtils.isEmpty(applicationJson.getPublishedPageOrder())) { + publishedPageOrderList = applicationJson.getPublishedPageOrder(); + } else { + publishedPageOrderList = pageNameMap.values() + .stream() + .filter(newPage -> Optional.ofNullable(newPage.getPublishedPage()).isPresent()) + .map(newPage -> newPage.getPublishedPage().getName()).collect(Collectors.toList()); + } + for(String pageName : publishedPageOrderList) { + NewPage newPage = pageNameMap.get(pageName); + ApplicationPage publishedAppPage = new ApplicationPage(); + if (newPage.getPublishedPage() != null && newPage.getPublishedPage().getName() != null) { + publishedAppPage.setIsDefault( + StringUtils.equals( + newPage.getPublishedPage().getName(), importedDoc.getPublishedDefaultPageName() + ) + ); + publishedAppPage.setId(newPage.getId()); + if (newPage.getDefaultResources() != null) { + publishedAppPage.setDefaultPageId(newPage.getDefaultResources().getPageId()); + } + } + + if (publishedAppPage.getId() != null && newPage.getPublishedPage().getDeletedAt() == null) { + applicationPages.get(PublishType.PUBLISHED).add(publishedAppPage); + } + } + return applicationPages; + } + /** * This function will respond with unique suffixed number for the entity to avoid duplicate names * @@ -1922,22 +2009,11 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica return srcTheme; } - private Mono importThemes(Application application, ApplicationJson importedApplicationJson) { - Mono importedEditModeTheme = themeService.getOrSaveTheme(importedApplicationJson.getEditModeTheme(), application); - Mono importedPublishedModeTheme = themeService.getOrSaveTheme(importedApplicationJson.getPublishedTheme(), application); - - return Mono.zip(importedEditModeTheme, importedPublishedModeTheme).flatMap(importedThemesTuple -> { - String editModeThemeId = importedThemesTuple.getT1().getId(); - String publishedModeThemeId = importedThemesTuple.getT2().getId(); - - application.setEditModeThemeId(editModeThemeId); - application.setPublishedModeThemeId(publishedModeThemeId); - // this will update the theme id in DB - // also returning the updated application object so that theme id are available to the next pipeline - return applicationService.setAppTheme( - application.getId(), editModeThemeId, publishedModeThemeId, MANAGE_APPLICATIONS - ).thenReturn(application); - }); + private Mono importThemes(Application application, ApplicationJson importedApplicationJson, boolean appendToApp) { + if(appendToApp) { // appending to existing app, theme should not change + return Mono.just(application); + } + return themeService.importThemesToApplication(application, importedApplicationJson); } /** @@ -2011,4 +2087,93 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica application.setServerSchemaVersion(null); application.setIsManualUpdate(false); } + + /** + * + * @param applicationId default ID of the application where this ApplicationJSON is going to get merged with + * @param branchName name of the branch of the application where this ApplicationJSON is going to get merged with + * @param applicationJson ApplicationJSON of the application that will be merged to + * @param pagesToImport Name of the pages that should be merged from the ApplicationJSON. + * If null or empty, all pages will be merged. + * @return Merged Application + */ + @Override + public Mono mergeApplicationJsonWithApplication(String workspaceId, String applicationId, String branchName, ApplicationJson applicationJson, List pagesToImport) { + // Update the application JSON to prepare it for merging inside an existing application + if(applicationJson.getExportedApplication() != null) { + // setting some properties to null so that target application is not updated by these properties + applicationJson.getExportedApplication().setName(null); + applicationJson.getExportedApplication().setSlug(null); + applicationJson.getExportedApplication().setApplicationVersion(null); + applicationJson.getExportedApplication().setForkingEnabled(null); + applicationJson.getExportedApplication().setClonedFromApplicationId(null); + } + + // need to remove git sync id. Also filter pages if pageToImport is not empty + if(applicationJson.getPageList() != null) { + List importedNewPageList = applicationJson.getPageList().stream() + .filter(newPage -> newPage.getUnpublishedPage() != null && + (CollectionUtils.isEmpty(pagesToImport) || + pagesToImport.contains(newPage.getUnpublishedPage().getName())) + ).collect(Collectors.toList()); + applicationJson.setPageList(importedNewPageList); + } + if(applicationJson.getActionList() != null) { + List importedNewActionList = applicationJson.getActionList().stream() + .filter(newAction -> + newAction.getUnpublishedAction() != null && + (CollectionUtils.isEmpty(pagesToImport) || + pagesToImport.contains(newAction.getUnpublishedAction().getPageId())) + ).peek(newAction -> newAction.setGitSyncId(null)) // setting this null so that this action can be imported again + .collect(Collectors.toList()); + applicationJson.setActionList(importedNewActionList); + } + if(applicationJson.getActionCollectionList() != null) { + List importedActionCollectionList = applicationJson.getActionCollectionList().stream() + .filter(actionCollection -> + (CollectionUtils.isEmpty(pagesToImport) || + pagesToImport.contains(actionCollection.getUnpublishedCollection().getPageId())) + ).peek(actionCollection -> actionCollection.setGitSyncId(null)) // setting this null so that this action collection can be imported again + .collect(Collectors.toList()); + applicationJson.setActionCollectionList(importedActionCollectionList); + } + + return importApplicationInWorkspace(workspaceId, applicationJson, applicationId, branchName, true); + } + + private Mono> updateNewPagesBeforeMerge(Mono> existingPagesMono, List newPagesList) { + return existingPagesMono.map(newPages -> { + Map newToOldToPageNameMap = new HashMap<>(); // maps new names with old names + + // get a list of unpublished page names that already exists + List unpublishedPageNames = newPages.stream() + .filter(newPage -> newPage.getUnpublishedPage() != null) + .map(newPage -> newPage.getUnpublishedPage().getName()) + .collect(Collectors.toList()); + + // modify each new page + for (NewPage newPage : newPagesList) { + newPage.setPublishedPage(null); // we'll not merge published pages so removing this + /* Need to remove gitSyncId from imported new pages. + * Same template or page can be merged with same application multiple times. + * As gitSyncId will be same for each import, new page will not be created + */ + newPage.setGitSyncId(null); + + // let's check if page name conflicts, rename in that case + String oldPageName = newPage.getUnpublishedPage().getName(), + newPageName = newPage.getUnpublishedPage().getName(); + + int i = 1; + while (unpublishedPageNames.contains(newPageName)) { + i++; + newPageName = oldPageName + i; + } + newPage.getUnpublishedPage().setName(newPageName); // set new name. may be same as before or not + newPage.getUnpublishedPage().setSlug(TextUtils.makeSlug(newPageName)); // set the slug also + newToOldToPageNameMap.put(newPageName, oldPageName); // map: new name -> old name + } + return newToOldToPageNameMap; + }); + } } diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ThemeServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ThemeServiceTest.java index fb0d26ad9f..3a5a0bcf0b 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ThemeServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ThemeServiceTest.java @@ -4,6 +4,7 @@ import com.appsmith.external.models.Policy; import com.appsmith.server.acl.AclPermission; import com.appsmith.server.constants.FieldName; import com.appsmith.server.domains.Application; +import com.appsmith.server.domains.ApplicationJson; import com.appsmith.server.domains.ApplicationMode; import com.appsmith.server.domains.Theme; import com.appsmith.server.dtos.ApplicationAccessDTO; @@ -25,6 +26,7 @@ import reactor.util.function.Tuples; import java.util.Collection; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.UUID; @@ -745,4 +747,37 @@ public class ThemeServiceTest { }).verifyComplete(); } + @WithUserDetails("api_user") + @Test + public void importThemesToApplication_WhenBothImportedThemesAreCustom_NewThemesCreated() { + Application application = createApplication("api_user", Set.of(MANAGE_APPLICATIONS)); + application.setOrganizationId("test-org"); + + // create a application json with a custom theme set as both edit mode and published mode + ApplicationJson applicationJson = new ApplicationJson(); + Theme customTheme = new Theme(); + customTheme.setName("Custom theme name"); + customTheme.setDisplayName("Custom theme display name"); + applicationJson.setEditModeTheme(customTheme); + applicationJson.setPublishedTheme(customTheme); + + Mono applicationMono = themeService.getDefaultThemeId() + .flatMap(defaultThemeId -> { + application.setEditModeThemeId(defaultThemeId); + application.setPublishedModeThemeId(defaultThemeId); + return applicationRepository.save(application); + }) + .flatMap(savedApplication -> + themeService.importThemesToApplication(savedApplication, applicationJson) + .thenReturn(Objects.requireNonNull(savedApplication.getId())) + ) + .flatMap(applicationId -> + applicationRepository.findById(applicationId, MANAGE_APPLICATIONS) + ); + + StepVerifier.create(applicationMono).assertNext(app -> { + assertThat(app.getEditModeThemeId().equals(app.getPublishedModeThemeId())).isFalse(); + }).verifyComplete(); + } + } \ No newline at end of file diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ImportExportApplicationServiceTests.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ImportExportApplicationServiceTests.java index 7198c4d94b..d790be75c7 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ImportExportApplicationServiceTests.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ImportExportApplicationServiceTests.java @@ -13,28 +13,31 @@ import com.appsmith.server.constants.SerialiseApplicationObjective; import com.appsmith.server.domains.ActionCollection; import com.appsmith.server.domains.Application; import com.appsmith.server.domains.ApplicationJson; +import com.appsmith.server.domains.ApplicationMode; import com.appsmith.server.domains.ApplicationPage; import com.appsmith.server.domains.GitApplicationMetadata; import com.appsmith.server.domains.Layout; import com.appsmith.server.domains.NewAction; import com.appsmith.server.domains.NewPage; -import com.appsmith.server.domains.Workspace; import com.appsmith.server.domains.Plugin; import com.appsmith.server.domains.PluginType; import com.appsmith.server.domains.Theme; +import com.appsmith.server.domains.Workspace; import com.appsmith.server.dtos.ActionCollectionDTO; import com.appsmith.server.dtos.ActionDTO; import com.appsmith.server.dtos.ApplicationAccessDTO; import com.appsmith.server.dtos.ApplicationImportDTO; +import com.appsmith.server.dtos.ApplicationPagesDTO; import com.appsmith.server.dtos.PageDTO; +import com.appsmith.server.dtos.PageNameIdDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; import com.appsmith.server.helpers.MockPluginExecutor; import com.appsmith.server.helpers.PluginExecutorHelper; +import com.appsmith.server.migrations.ApplicationVersion; import com.appsmith.server.migrations.JsonSchemaMigration; import com.appsmith.server.migrations.JsonSchemaVersions; import com.appsmith.server.repositories.ApplicationRepository; -import com.appsmith.server.repositories.NewPageRepository; import com.appsmith.server.repositories.PluginRepository; import com.appsmith.server.repositories.ThemeRepository; import com.appsmith.server.services.ActionCollectionService; @@ -46,9 +49,6 @@ import com.appsmith.server.services.LayoutCollectionService; import com.appsmith.server.services.NewActionService; import com.appsmith.server.services.NewPageService; import com.appsmith.server.services.WorkspaceService; -import com.appsmith.server.services.SessionUserService; -import com.appsmith.server.services.ThemeService; -import com.appsmith.server.services.UserService; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @@ -82,6 +82,7 @@ import org.springframework.util.LinkedMultiValueMap; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import reactor.util.function.Tuple3; import java.lang.reflect.Type; import java.time.Duration; @@ -120,9 +121,6 @@ public class ImportExportApplicationServiceTests { @Autowired ApplicationPageService applicationPageService; - @Autowired - UserService userService; - @Autowired PluginRepository pluginRepository; @@ -141,15 +139,9 @@ public class ImportExportApplicationServiceTests { @Autowired WorkspaceService workspaceService; - @Autowired - SessionUserService sessionUserService; - @Autowired LayoutActionService layoutActionService; - @Autowired - NewPageRepository newPageRepository; - @Autowired LayoutCollectionService layoutCollectionService; @@ -162,9 +154,6 @@ public class ImportExportApplicationServiceTests { @Autowired ThemeRepository themeRepository; - @Autowired - ThemeService themeService; - @Autowired ApplicationService applicationService; @@ -2334,7 +2323,7 @@ public class ImportExportApplicationServiceTests { }) .verifyComplete(); } - + @Test @WithUserDetails(value = "api_user") public void exportAndImportApplication_withMultiplePagesOrderSameInDeployAndEditMode_PagesOrderIsMaintainedInEditAndViewMode() { @@ -2525,6 +2514,155 @@ public class ImportExportApplicationServiceTests { } + private ApplicationJson createApplicationJSON(List pageNames) { + ApplicationJson applicationJson = new ApplicationJson(); + + // set the application data + Application application = new Application(); + application.setName("Template Application"); + application.setSlug("template-application"); + application.setForkingEnabled(true); + application.setIsPublic(true); + application.setApplicationVersion(ApplicationVersion.LATEST_VERSION); + applicationJson.setExportedApplication(application); + + Datasource sampleDatasource = new Datasource(); + sampleDatasource.setName("SampleDS"); + sampleDatasource.setPluginId("restapi-plugin"); + + applicationJson.setDatasourceList(List.of(sampleDatasource)); + + // add pages and actions + List newPageList = new ArrayList<>(pageNames.size()); + List actionList = new ArrayList<>(); + List actionCollectionList = new ArrayList<>(); + + for(String pageName : pageNames) { + NewPage newPage = new NewPage(); + newPage.setUnpublishedPage(new PageDTO()); + newPage.getUnpublishedPage().setName(pageName); + newPage.getUnpublishedPage().setLayouts(List.of()); + newPageList.add(newPage); + + NewAction action = new NewAction(); + action.setId(pageName + "_SampleQuery"); + action.setPluginType(PluginType.API); + action.setPluginId("restapi-plugin"); + action.setUnpublishedAction(new ActionDTO()); + action.getUnpublishedAction().setName("SampleQuery"); + action.getUnpublishedAction().setPageId(pageName); + action.getUnpublishedAction().setDatasource(new Datasource()); + action.getUnpublishedAction().getDatasource().setId("SampleDS"); + action.getUnpublishedAction().getDatasource().setPluginId("restapi-plugin"); + actionList.add(action); + + ActionCollection actionCollection = new ActionCollection(); + actionCollection.setId(pageName + "_SampleJS"); + actionCollection.setUnpublishedCollection(new ActionCollectionDTO()); + actionCollection.getUnpublishedCollection().setName("SampleJS"); + actionCollection.getUnpublishedCollection().setPageId(pageName); + actionCollection.getUnpublishedCollection().setPluginId("js-plugin"); + actionCollection.getUnpublishedCollection().setPluginType(PluginType.JS); + actionCollection.getUnpublishedCollection().setBody("export default {\\n\\t\\n}"); + actionCollectionList.add(actionCollection); + } + + applicationJson.setPageList(newPageList); + applicationJson.setActionList(actionList); + applicationJson.setActionCollectionList(actionCollectionList); + return applicationJson; + } + + @Test + @WithUserDetails("api_user") + public void mergeApplicationJsonWithApplication_WhenPageNameConflicts_PageNamesRenamed() { + String uniqueString = UUID.randomUUID().toString(); + + Application destApplication = new Application(); + destApplication.setName("App_" + uniqueString); + destApplication.setSlug("my-slug"); + destApplication.setIsPublic(false); + destApplication.setForkingEnabled(false); + Mono createAppAndPageMono = applicationPageService.createApplication(destApplication, workspaceId) + .flatMap(application -> { + PageDTO pageDTO = new PageDTO(); + pageDTO.setName("Home"); + pageDTO.setApplicationId(application.getId()); + return applicationPageService.createPage(pageDTO).thenReturn(application); + }); + + // let's create an ApplicationJSON which we'll merge with application created by createAppAndPageMono + ApplicationJson applicationJson = createApplicationJSON(List.of("Home", "About")); + + Mono, List>> tuple2Mono = createAppAndPageMono.flatMap(application -> + // merge the application json with the application we've created + importExportApplicationService.mergeApplicationJsonWithApplication(application.getOrganizationId(), application.getId(), null, applicationJson, null) + .thenReturn(application) + ).flatMap(application -> + // fetch the application pages, this should contain pages from application json + Mono.zip( + newPageService.findApplicationPages(application.getId(), null, null, ApplicationMode.EDIT), + newActionService.findAllByApplicationIdAndViewMode(application.getId(), false, MANAGE_ACTIONS, null).collectList(), + actionCollectionService.findAllByApplicationIdAndViewMode(application.getId(), false, MANAGE_ACTIONS, null).collectList() + ) + ); + + StepVerifier.create(tuple2Mono).assertNext(objects -> { + ApplicationPagesDTO applicationPagesDTO = objects.getT1(); + List newActionList = objects.getT2(); + List actionCollectionList = objects.getT3(); + + assertThat(applicationPagesDTO.getApplication().getName()).isEqualTo(destApplication.getName()); + assertThat(applicationPagesDTO.getApplication().getSlug()).isEqualTo(destApplication.getSlug()); + assertThat(applicationPagesDTO.getApplication().getIsPublic()).isFalse(); + assertThat(applicationPagesDTO.getApplication().getForkingEnabled()).isFalse(); + assertThat(applicationPagesDTO.getPages().size()).isEqualTo(4); + List pageNames = applicationPagesDTO.getPages().stream() + .map(PageNameIdDTO::getName) + .collect(Collectors.toList()); + assertThat(pageNames).contains("Home", "Home2", "About"); + assertThat(newActionList.size()).isEqualTo(2); // we imported two pages and each page has one action + assertThat(actionCollectionList.size()).isEqualTo(2); // we imported two pages and each page has one Collection + }).verifyComplete(); + } + + @Test + @WithUserDetails("api_user") + public void mergeApplicationJsonWithApplication_WhenPageListIProvided_OnlyListedPagesAreMerged() { + String uniqueString = UUID.randomUUID().toString(); + + Application destApplication = new Application(); + destApplication.setName("App_" + uniqueString); + Mono createAppAndPageMono = applicationPageService.createApplication(destApplication, workspaceId) + .flatMap(application -> { + PageDTO pageDTO = new PageDTO(); + pageDTO.setName("Home"); + pageDTO.setApplicationId(application.getId()); + return applicationPageService.createPage(pageDTO).thenReturn(application); + }); + + // let's create an ApplicationJSON which we'll merge with application created by createAppAndPageMono + ApplicationJson applicationJson = createApplicationJSON(List.of("Profile", "About", "Contact US")); + + Mono applicationPagesDTOMono = createAppAndPageMono.flatMap(application -> + // merge the application json with the application we've created + importExportApplicationService.mergeApplicationJsonWithApplication(application.getOrganizationId(), application.getId(), null, applicationJson, List.of("About", "Contact US")) + .thenReturn(application) + ).flatMap(application -> + // fetch the application pages, this should contain pages from application json + newPageService.findApplicationPages(application.getId(), null, null, ApplicationMode.EDIT) + ); + + StepVerifier.create(applicationPagesDTOMono).assertNext(applicationPagesDTO -> { + assertThat(applicationPagesDTO.getPages().size()).isEqualTo(4); + List pageNames = applicationPagesDTO.getPages().stream() + .map(PageNameIdDTO::getName) + .collect(Collectors.toList()); + assertThat(pageNames).contains("Home", "About", "Contact US"); + assertThat(pageNames).doesNotContain("Profile"); + }).verifyComplete(); + } + @Test @WithUserDetails(value = "api_user") public void exportApplicationById_WhenThemeDoesNotExist_ExportedWithDefaultTheme() {