feat: Import an template inside an existing Application (#12507)

This PR adds the feature to import a template inside an application.
This commit is contained in:
Nayan 2022-06-08 13:24:37 +06:00 committed by GitHub
parent 144c999d1c
commit da2455bb5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 635 additions and 196 deletions

View File

@ -1,5 +1,6 @@
package com.appsmith.server.controllers.ce; package com.appsmith.server.controllers.ce;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Application; import com.appsmith.server.domains.Application;
import com.appsmith.server.dtos.ApplicationTemplate; import com.appsmith.server.dtos.ApplicationTemplate;
import com.appsmith.server.dtos.ResponseDTO; 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.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; 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 reactor.core.publisher.Mono;
import java.util.List; import java.util.List;
@ -58,4 +61,14 @@ public class ApplicationTemplateControllerCE {
return applicationTemplateService.getRecentlyUsedTemplates().collectList() return applicationTemplateService.getRecentlyUsedTemplates().collectList()
.map(templates -> new ResponseDTO<>(HttpStatus.OK.value(), templates, null)); .map(templates -> new ResponseDTO<>(HttpStatus.OK.value(), templates, null));
} }
@PostMapping("{templateId}/merge/{applicationId}/{organizationId}")
public Mono<ResponseDTO<Application>> mergeTemplateWithApplication(@PathVariable String templateId,
@PathVariable String applicationId,
@PathVariable String organizationId,
@RequestHeader(name = FieldName.BRANCH_NAME, required = false) String branchName,
@RequestBody(required = false) List<String> pagesToImport) {
return applicationTemplateService.mergeTemplateWithApplication(templateId, applicationId, organizationId, branchName, pagesToImport)
.map(importedApp -> new ResponseDTO<>(HttpStatus.OK.value(), importedApp, null));
}
} }

View File

@ -13,5 +13,6 @@ public interface ApplicationTemplateServiceCE {
Flux<ApplicationTemplate> getRecentlyUsedTemplates(); Flux<ApplicationTemplate> getRecentlyUsedTemplates();
Mono<ApplicationTemplate> getTemplateDetails(String templateId); Mono<ApplicationTemplate> getTemplateDetails(String templateId);
Mono<Application> importApplicationFromTemplate(String templateId, String organizationId); Mono<Application> importApplicationFromTemplate(String templateId, String organizationId);
Mono<Application> mergeTemplateWithApplication(String templateId, String applicationId, String organizationId, String branchName, List<String> pagesToImport);
Mono<ApplicationTemplate> getFilters(); Mono<ApplicationTemplate> getFilters();
} }

View File

@ -31,6 +31,7 @@ import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.List;
@Service @Service
public class ApplicationTemplateServiceCEImpl implements ApplicationTemplateServiceCE { public class ApplicationTemplateServiceCEImpl implements ApplicationTemplateServiceCE {
@ -205,4 +206,13 @@ public class ApplicationTemplateServiceCEImpl implements ApplicationTemplateServ
super.setEncodingMode(EncodingMode.NONE); super.setEncodingMode(EncodingMode.NONE);
} }
} }
@Override
public Mono<Application> mergeTemplateWithApplication(String templateId, String applicationId, String organizationId, String branchName, List<String> pagesToImport) {
return getApplicationJsonFromTemplate(templateId).flatMap(applicationJson ->
importExportApplicationService.mergeApplicationJsonWithApplication(
organizationId, applicationId, null, applicationJson, pagesToImport
)
);
}
} }

View File

@ -2,6 +2,7 @@ package com.appsmith.server.services.ce;
import com.appsmith.server.acl.AclPermission; import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.domains.Application; import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.ApplicationJson;
import com.appsmith.server.domains.ApplicationMode; import com.appsmith.server.domains.ApplicationMode;
import com.appsmith.server.domains.Theme; import com.appsmith.server.domains.Theme;
import com.appsmith.server.services.CrudService; import com.appsmith.server.services.CrudService;
@ -38,4 +39,5 @@ public interface ThemeServiceCE extends CrudService<Theme, String> {
Mono<Theme> updateName(String id, Theme theme); Mono<Theme> updateName(String id, Theme theme);
Mono<Theme> getOrSaveTheme(Theme theme, Application destApplication); Mono<Theme> getOrSaveTheme(Theme theme, Application destApplication);
Mono<Application> archiveApplicationThemes(Application application); Mono<Application> archiveApplicationThemes(Application application);
Mono<Application> importThemesToApplication(Application destinationApp, ApplicationJson sourceJson);
} }

View File

@ -1,10 +1,11 @@
package com.appsmith.server.services.ce; package com.appsmith.server.services.ce;
import com.appsmith.external.constants.AnalyticsEvents;
import com.appsmith.server.acl.AclPermission; import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.acl.PolicyGenerator; import com.appsmith.server.acl.PolicyGenerator;
import com.appsmith.external.constants.AnalyticsEvents;
import com.appsmith.server.constants.FieldName; import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Application; import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.ApplicationJson;
import com.appsmith.server.domains.ApplicationMode; import com.appsmith.server.domains.ApplicationMode;
import com.appsmith.server.domains.Theme; import com.appsmith.server.domains.Theme;
import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithError;
@ -391,12 +392,18 @@ public class ThemeServiceCEImpl extends BaseService<ThemeRepositoryCE, Theme, St
return repository.getSystemThemeByName(theme.getName()) return repository.getSystemThemeByName(theme.getName())
.switchIfEmpty(repository.getSystemThemeByName(Theme.DEFAULT_THEME_NAME)); .switchIfEmpty(repository.getSystemThemeByName(Theme.DEFAULT_THEME_NAME));
} else { } else {
theme.setApplicationId(null); // create a new theme
theme.setOrganizationId(null); Theme newTheme = new Theme();
theme.setPolicies(policyGenerator.getAllChildPolicies( newTheme.setPolicies(policyGenerator.getAllChildPolicies(
destApplication.getPolicies(), Application.class, Theme.class destApplication.getPolicies(), Application.class, Theme.class
)); ));
return repository.save(theme); newTheme.setStylesheet(theme.getStylesheet());
newTheme.setProperties(theme.getProperties());
newTheme.setConfig(theme.getConfig());
newTheme.setName(theme.getName());
newTheme.setDisplayName(theme.getDisplayName());
newTheme.setSystemTheme(false);
return repository.save(newTheme);
} }
} }
@ -412,4 +419,62 @@ public class ThemeServiceCEImpl extends BaseService<ThemeRepositoryCE, Theme, St
.then(repository.archiveDraftThemesById(application.getEditModeThemeId(), application.getPublishedModeThemeId())) .then(repository.archiveDraftThemesById(application.getEditModeThemeId(), application.getPublishedModeThemeId()))
.thenReturn(application); .thenReturn(application);
} }
/**
* This method imports a theme from a JSON file to an application. The destination application can already have
* a theme set or not. If no theme is set, it means the application is being created from a JSON import, git import.
* In that case, we'll import the edit mode theme and published mode theme from the JSON file and update the application.
* If the destination application already has a theme, it means we're doing any of these Git operations -
* pull, merge, discard. In this case, we'll decide based on this decision tree:
* - If current theme is a customized one and source theme is also customized, replace the current theme properties with source theme properties
* - If current theme is a customized one and source theme is system theme, set the current theme to system and delete the old one
* - If current theme is system theme, update the current theme as per source theme
* @param destinationApp Application object
* @param sourceJson ApplicationJSON from file or Git
* @return Updated application that has editModeThemeId and publishedModeThemeId set
*/
@Override
public Mono<Application> importThemesToApplication(Application destinationApp, ApplicationJson sourceJson) {
Mono<Theme> editModeTheme = updateExistingAppThemeFromJSON(
destinationApp, destinationApp.getEditModeThemeId(), sourceJson.getEditModeTheme()
);
Mono<Theme> 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<Theme> 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);
}
}
});
}
} }

View File

@ -1,6 +1,10 @@
package com.appsmith.server.solutions; package com.appsmith.server.solutions;
import com.appsmith.server.helpers.PolicyUtils; 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.ActionCollectionRepository;
import com.appsmith.server.repositories.DatasourceRepository; import com.appsmith.server.repositories.DatasourceRepository;
import com.appsmith.server.repositories.NewActionRepository; 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.services.ThemeService;
import com.appsmith.server.solutions.ce.ImportExportApplicationServiceCEImpl; import com.appsmith.server.solutions.ce.ImportExportApplicationServiceCEImpl;
import lombok.extern.slf4j.Slf4j; 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 org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@Slf4j @Slf4j
@Component @Component

View File

@ -32,6 +32,8 @@ public interface ImportExportApplicationServiceCE {
*/ */
Mono<ApplicationImportDTO> extractFileAndSaveApplication(String orgId, Part filePart); Mono<ApplicationImportDTO> extractFileAndSaveApplication(String orgId, Part filePart);
Mono<Application> mergeApplicationJsonWithApplication(String organizationId, String applicationId, String branchName, ApplicationJson applicationJson, List<String> pagesToImport);
/** /**
* This function will save the application to workspace from the application resource * This function will save the application to workspace from the application resource
* *

View File

@ -34,6 +34,7 @@ import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException; import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.DefaultResourcesUtils; import com.appsmith.server.helpers.DefaultResourcesUtils;
import com.appsmith.server.helpers.PolicyUtils; import com.appsmith.server.helpers.PolicyUtils;
import com.appsmith.server.helpers.TextUtils;
import com.appsmith.server.migrations.ApplicationVersion; import com.appsmith.server.migrations.ApplicationVersion;
import com.appsmith.server.migrations.JsonSchemaMigration; import com.appsmith.server.migrations.JsonSchemaMigration;
import com.appsmith.server.migrations.JsonSchemaVersions; import com.appsmith.server.migrations.JsonSchemaVersions;
@ -631,19 +632,48 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
return importApplicationInWorkspace(workspaceId, importedDoc, null, null); return importApplicationInWorkspace(workspaceId, importedDoc, null, null);
} }
public Mono<Application> 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 * 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 workspaceId workspace to which application is going to be stored
* @param applicationJson application resource which contains necessary information to import the application * @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 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 * @return Updated application
*/ */
public Mono<Application> importApplicationInWorkspace(String workspaceId, private Mono<Application> importApplicationInWorkspace(String workspaceId,
ApplicationJson applicationJson, ApplicationJson applicationJson,
String applicationId, String applicationId,
String branchName) { String branchName,
boolean appendToApp) {
/* /*
1. Migrate resource to latest schema 1. Migrate resource to latest schema
2. Fetch workspace by id 2. Fetch workspace by id
@ -653,10 +683,18 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
6. Extract and save pages in the application 6. Extract and save pages in the application
7. Extract and save actions 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); 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<String, String> pluginMap = new HashMap<>(); Map<String, String> pluginMap = new HashMap<>();
Map<String, String> datasourceMap = new HashMap<>(); Map<String, String> datasourceMap = new HashMap<>();
Map<String, NewPage> pageNameMap = new HashMap<>(); Map<String, NewPage> pageNameMap = new HashMap<>();
@ -681,20 +719,6 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
.findAllByOrganizationId(workspaceId, MANAGE_DATASOURCES) .findAllByOrganizationId(workspaceId, MANAGE_DATASOURCES)
.cache(); .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!"; assert importedApplication != null: "Received invalid application object!";
if(importedApplication.getApplicationVersion() == null) { if(importedApplication.getApplicationVersion() == null) {
importedApplication.setApplicationVersion(ApplicationVersion.EARLIEST_VERSION); importedApplication.setApplicationVersion(ApplicationVersion.EARLIEST_VERSION);
@ -722,7 +746,6 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
return Mono.just(new ArrayList<Datasource>()); return Mono.just(new ArrayList<Datasource>());
}) })
.flatMapMany(existingDatasources -> { .flatMapMany(existingDatasources -> {
if (CollectionUtils.isEmpty(importedDatasourceList)) { if (CollectionUtils.isEmpty(importedDatasourceList)) {
return Mono.empty(); return Mono.empty();
} }
@ -810,6 +833,9 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
applicationId)) applicationId))
) )
.flatMap(existingApplication -> { .flatMap(existingApplication -> {
if(StringUtils.isEmpty(branchName)) {
return Mono.just(existingApplication);
}
importedApplication.setId(existingApplication.getId()); importedApplication.setId(existingApplication.getId());
// For the existing application we don't need to default value of the flag // 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 // 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); return applicationPageService.createOrUpdateSuffixedApplication(application, application.getName(), 0);
}) })
) )
.flatMap(savedApp -> importThemes(savedApp, importedDoc)) .flatMap(savedApp -> importThemes(savedApp, importedDoc, appendToApp))
.flatMap(savedApp -> { .flatMap(savedApp -> {
importedApplication.setId(savedApp.getId()); importedApplication.setId(savedApp.getId());
if (savedApp.getGitApplicationMetadata() != null) { 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 // Import and save pages, also update the pages related fields in saved application
assert importedNewPageList != null: "Unable to find pages in the imported 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 // For git-sync this will not be empty
Mono<List<NewPage>> existingPagesMono = newPageService Mono<List<NewPage>> existingPagesMono = newPageService
.findNewPagesByApplicationId(importedApplication.getId(), MANAGE_PAGES) .findNewPagesByApplicationId(importedApplication.getId(), MANAGE_PAGES)
.collectList() .collectList()
.cache(); .cache();
return importAndSavePages( Flux<NewPage> importNewPageFlux = importAndSavePages(
importedNewPageList, importedNewPageList,
importedApplication, savedApp,
importedDoc.getPublishedLayoutmongoEscapedWidgets(), importedDoc.getPublishedLayoutmongoEscapedWidgets(),
importedDoc.getUnpublishedLayoutmongoEscapedWidgets(), importedDoc.getUnpublishedLayoutmongoEscapedWidgets(),
branchName, branchName,
existingPagesMono existingPagesMono
) );
Flux<NewPage> 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 -> { .map(newPage -> {
// Save the map of pageName and NewPage // Save the map of pageName and NewPage
if (newPage.getUnpublishedPage() != null && newPage.getUnpublishedPage().getName() != null) { if (newPage.getUnpublishedPage() != null && newPage.getUnpublishedPage().getName() != null) {
@ -878,75 +933,15 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
pageNameMap.put(newPage.getPublishedPage().getName(), newPage); pageNameMap.put(newPage.getPublishedPage().getName(), newPage);
} }
return newPage; return newPage;
}) });
return importedNewPagesMono
.collectList() .collectList()
.map(newPageList -> { .map(newPageList -> {
Map<PublishType, List<ApplicationPage>> applicationPages = Map.of( return reorderPages(applicationJson, importedDoc, pageNameMap);
PublishType.UNPUBLISHED, new ArrayList<>(),
PublishType.PUBLISHED, new ArrayList<>()
);
// Reorder the pages based on edit mode page sequence
List<String> 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<String> 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;
}) })
.flatMap(applicationPages -> { .flatMap(applicationPages -> {
if (!StringUtils.isEmpty(applicationId)) { if (!StringUtils.isEmpty(applicationId) && !appendToApp) {
Set<String> validPageIds = applicationPages.get(PublishType.UNPUBLISHED).stream() Set<String> validPageIds = applicationPages.get(PublishType.UNPUBLISHED).stream()
.map(ApplicationPage::getId) .map(ApplicationPage::getId)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
@ -984,9 +979,19 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
}) })
.flatMap(applicationPageMap -> { .flatMap(applicationPageMap -> {
// set it based on the order for published and unublished pages // set it based on the order for published and unublished pages
importedApplication.setPages(applicationPageMap.get(PublishType.UNPUBLISHED)); if(appendToApp) {
importedApplication.setPublishedPages(applicationPageMap.get(PublishType.PUBLISHED)); 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 // This will be non-empty for GIT sync
return newActionRepository.findByApplicationId(importedApplication.getId()) return newActionRepository.findByApplicationId(importedApplication.getId())
.collectList(); .collectList();
@ -1005,34 +1010,34 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
publishedCollectionIdToActionIdsMap, publishedCollectionIdToActionIdsMap,
applicationJson.getInvisibleActionFields() applicationJson.getInvisibleActionFields()
) )
.map(NewAction::getId) .map(NewAction::getId)
.collectList() .collectList()
.flatMap(savedActionIds -> { .flatMap(savedActionIds -> {
// Updating the existing application for git-sync // Updating the existing application for git-sync
if (!StringUtils.isEmpty(applicationId)) { if (!StringUtils.isEmpty(applicationId) && !appendToApp) {
// Remove unwanted actions // Remove unwanted actions
Set<String> invalidActionIds = new HashSet<>(); Set<String> invalidActionIds = new HashSet<>();
for (NewAction action : existingActions) { for (NewAction action : existingActions) {
if (!savedActionIds.contains(action.getId())) { if (!savedActionIds.contains(action.getId())) {
invalidActionIds.add(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 Mono.just(savedActionIds);
return Flux.fromIterable(invalidActionIds) })
.flatMap(actionId -> newActionService.deleteUnpublishedAction(actionId) .thenMany(actionCollectionRepository.findByApplicationId(importedApplication.getId()))
// return an empty action so that the filter can remove it from the list .collectList()
.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()
) )
.flatMap(existingActionCollections -> { .flatMap(existingActionCollections -> {
if (importedActionCollectionList == null) { if (importedActionCollectionList == null) {
@ -1047,46 +1052,46 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
pageNameMap, pluginMap, pageNameMap, pluginMap,
unpublishedCollectionIdToActionIdsMap, unpublishedCollectionIdToActionIdsMap,
publishedCollectionIdToActionIdsMap publishedCollectionIdToActionIdsMap
) )
.flatMap(tuple -> { .flatMap(tuple -> {
final String importedActionCollectionId = tuple.getT1(); final String importedActionCollectionId = tuple.getT1();
ActionCollection savedActionCollection = tuple.getT2(); ActionCollection savedActionCollection = tuple.getT2();
savedCollectionIds.add(savedActionCollection.getId()); savedCollectionIds.add(savedActionCollection.getId());
return updateActionsWithImportedCollectionIds( return updateActionsWithImportedCollectionIds(
importedActionCollectionId, importedActionCollectionId,
savedActionCollection, savedActionCollection,
unpublishedCollectionIdToActionIdsMap, unpublishedCollectionIdToActionIdsMap,
publishedCollectionIdToActionIdsMap, publishedCollectionIdToActionIdsMap,
unpublishedActionIdToCollectionIdMap, unpublishedActionIdToCollectionIdMap,
publishedActionIdToCollectionIdMap publishedActionIdToCollectionIdMap
); );
}) })
.collectList() .collectList()
.flatMap(ignore -> { .flatMap(ignore -> {
// Updating the existing application for git-sync // Updating the existing application for git-sync
if (!StringUtils.isEmpty(applicationId)) { if (!StringUtils.isEmpty(applicationId) && !appendToApp) {
// Remove unwanted actions // Remove unwanted action collections
Set<String> invalidCollectionIds = new HashSet<>(); Set<String> invalidCollectionIds = new HashSet<>();
for (ActionCollection collection : existingActionCollections) { for (ActionCollection collection : existingActionCollections) {
if (!savedCollectionIds.contains(collection.getId())) { if (!savedCollectionIds.contains(collection.getId())) {
invalidCollectionIds.add(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) return Mono.just(savedCollectionIds);
.flatMap(collectionId -> actionCollectionService.deleteUnpublishedActionCollection(collectionId) })
// return an empty collection so that the filter can remove it from the list .thenReturn(true);
.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);
}) })
.flatMap(ignored -> { .flatMap(ignored -> {
// Don't update gitAuth as we are using @Encrypted for private key // 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<NewAction> newActionList, String oldPageName, String newPageName) {
for(NewAction newAction : newActionList) {
if(newAction.getUnpublishedAction().getPageId().equals(oldPageName)) {
newAction.getUnpublishedAction().setPageId(newPageName);
}
}
}
private void renamePageInActionCollections(List<ActionCollection> actionCollectionList, String oldPageName, String newPageName) {
for(ActionCollection actionCollection : actionCollectionList) {
if(actionCollection.getUnpublishedCollection().getPageId().equals(oldPageName)) {
actionCollection.getUnpublishedCollection().setPageId(newPageName);
}
}
}
private Map<PublishType, List<ApplicationPage>> reorderPages(ApplicationJson applicationJson, ApplicationJson importedDoc, Map<String, NewPage> pageNameMap) {
Map<PublishType, List<ApplicationPage>> applicationPages = Map.of(
PublishType.UNPUBLISHED, new ArrayList<>(),
PublishType.PUBLISHED, new ArrayList<>()
);
// Reorder the pages based on edit mode page sequence
List<String> 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<String> 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 * 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; return srcTheme;
} }
private Mono<Application> importThemes(Application application, ApplicationJson importedApplicationJson) { private Mono<Application> importThemes(Application application, ApplicationJson importedApplicationJson, boolean appendToApp) {
Mono<Theme> importedEditModeTheme = themeService.getOrSaveTheme(importedApplicationJson.getEditModeTheme(), application); if(appendToApp) { // appending to existing app, theme should not change
Mono<Theme> importedPublishedModeTheme = themeService.getOrSaveTheme(importedApplicationJson.getPublishedTheme(), application); return Mono.just(application);
}
return Mono.zip(importedEditModeTheme, importedPublishedModeTheme).flatMap(importedThemesTuple -> { return themeService.importThemesToApplication(application, importedApplicationJson);
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);
});
} }
/** /**
@ -2011,4 +2087,93 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
application.setServerSchemaVersion(null); application.setServerSchemaVersion(null);
application.setIsManualUpdate(false); 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<Application> mergeApplicationJsonWithApplication(String workspaceId, String applicationId, String branchName, ApplicationJson applicationJson, List<String> 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<NewPage> 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<NewAction> 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<ActionCollection> 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<Map<String, String>> updateNewPagesBeforeMerge(Mono<List<NewPage>> existingPagesMono, List<NewPage> newPagesList) {
return existingPagesMono.map(newPages -> {
Map<String, String> newToOldToPageNameMap = new HashMap<>(); // maps new names with old names
// get a list of unpublished page names that already exists
List<String> 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;
});
}
} }

View File

@ -4,6 +4,7 @@ import com.appsmith.external.models.Policy;
import com.appsmith.server.acl.AclPermission; import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.constants.FieldName; import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Application; import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.ApplicationJson;
import com.appsmith.server.domains.ApplicationMode; import com.appsmith.server.domains.ApplicationMode;
import com.appsmith.server.domains.Theme; import com.appsmith.server.domains.Theme;
import com.appsmith.server.dtos.ApplicationAccessDTO; import com.appsmith.server.dtos.ApplicationAccessDTO;
@ -25,6 +26,7 @@ import reactor.util.function.Tuples;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
@ -745,4 +747,37 @@ public class ThemeServiceTest {
}).verifyComplete(); }).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<Application> 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();
}
} }

View File

@ -13,28 +13,31 @@ import com.appsmith.server.constants.SerialiseApplicationObjective;
import com.appsmith.server.domains.ActionCollection; import com.appsmith.server.domains.ActionCollection;
import com.appsmith.server.domains.Application; import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.ApplicationJson; import com.appsmith.server.domains.ApplicationJson;
import com.appsmith.server.domains.ApplicationMode;
import com.appsmith.server.domains.ApplicationPage; import com.appsmith.server.domains.ApplicationPage;
import com.appsmith.server.domains.GitApplicationMetadata; import com.appsmith.server.domains.GitApplicationMetadata;
import com.appsmith.server.domains.Layout; import com.appsmith.server.domains.Layout;
import com.appsmith.server.domains.NewAction; import com.appsmith.server.domains.NewAction;
import com.appsmith.server.domains.NewPage; import com.appsmith.server.domains.NewPage;
import com.appsmith.server.domains.Workspace;
import com.appsmith.server.domains.Plugin; import com.appsmith.server.domains.Plugin;
import com.appsmith.server.domains.PluginType; import com.appsmith.server.domains.PluginType;
import com.appsmith.server.domains.Theme; import com.appsmith.server.domains.Theme;
import com.appsmith.server.domains.Workspace;
import com.appsmith.server.dtos.ActionCollectionDTO; import com.appsmith.server.dtos.ActionCollectionDTO;
import com.appsmith.server.dtos.ActionDTO; import com.appsmith.server.dtos.ActionDTO;
import com.appsmith.server.dtos.ApplicationAccessDTO; import com.appsmith.server.dtos.ApplicationAccessDTO;
import com.appsmith.server.dtos.ApplicationImportDTO; import com.appsmith.server.dtos.ApplicationImportDTO;
import com.appsmith.server.dtos.ApplicationPagesDTO;
import com.appsmith.server.dtos.PageDTO; import com.appsmith.server.dtos.PageDTO;
import com.appsmith.server.dtos.PageNameIdDTO;
import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException; import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.MockPluginExecutor; import com.appsmith.server.helpers.MockPluginExecutor;
import com.appsmith.server.helpers.PluginExecutorHelper; import com.appsmith.server.helpers.PluginExecutorHelper;
import com.appsmith.server.migrations.ApplicationVersion;
import com.appsmith.server.migrations.JsonSchemaMigration; import com.appsmith.server.migrations.JsonSchemaMigration;
import com.appsmith.server.migrations.JsonSchemaVersions; import com.appsmith.server.migrations.JsonSchemaVersions;
import com.appsmith.server.repositories.ApplicationRepository; import com.appsmith.server.repositories.ApplicationRepository;
import com.appsmith.server.repositories.NewPageRepository;
import com.appsmith.server.repositories.PluginRepository; import com.appsmith.server.repositories.PluginRepository;
import com.appsmith.server.repositories.ThemeRepository; import com.appsmith.server.repositories.ThemeRepository;
import com.appsmith.server.services.ActionCollectionService; 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.NewActionService;
import com.appsmith.server.services.NewPageService; import com.appsmith.server.services.NewPageService;
import com.appsmith.server.services.WorkspaceService; 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.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@ -82,6 +82,7 @@ import org.springframework.util.LinkedMultiValueMap;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.test.StepVerifier; import reactor.test.StepVerifier;
import reactor.util.function.Tuple3;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.time.Duration; import java.time.Duration;
@ -120,9 +121,6 @@ public class ImportExportApplicationServiceTests {
@Autowired @Autowired
ApplicationPageService applicationPageService; ApplicationPageService applicationPageService;
@Autowired
UserService userService;
@Autowired @Autowired
PluginRepository pluginRepository; PluginRepository pluginRepository;
@ -141,15 +139,9 @@ public class ImportExportApplicationServiceTests {
@Autowired @Autowired
WorkspaceService workspaceService; WorkspaceService workspaceService;
@Autowired
SessionUserService sessionUserService;
@Autowired @Autowired
LayoutActionService layoutActionService; LayoutActionService layoutActionService;
@Autowired
NewPageRepository newPageRepository;
@Autowired @Autowired
LayoutCollectionService layoutCollectionService; LayoutCollectionService layoutCollectionService;
@ -162,9 +154,6 @@ public class ImportExportApplicationServiceTests {
@Autowired @Autowired
ThemeRepository themeRepository; ThemeRepository themeRepository;
@Autowired
ThemeService themeService;
@Autowired @Autowired
ApplicationService applicationService; ApplicationService applicationService;
@ -2334,7 +2323,7 @@ public class ImportExportApplicationServiceTests {
}) })
.verifyComplete(); .verifyComplete();
} }
@Test @Test
@WithUserDetails(value = "api_user") @WithUserDetails(value = "api_user")
public void exportAndImportApplication_withMultiplePagesOrderSameInDeployAndEditMode_PagesOrderIsMaintainedInEditAndViewMode() { public void exportAndImportApplication_withMultiplePagesOrderSameInDeployAndEditMode_PagesOrderIsMaintainedInEditAndViewMode() {
@ -2525,6 +2514,155 @@ public class ImportExportApplicationServiceTests {
} }
private ApplicationJson createApplicationJSON(List<String> 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<NewPage> newPageList = new ArrayList<>(pageNames.size());
List<NewAction> actionList = new ArrayList<>();
List<ActionCollection> 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<Application> 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<Tuple3<ApplicationPagesDTO, List<NewAction>, List<ActionCollection>>> 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<NewAction> newActionList = objects.getT2();
List<ActionCollection> 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<String> 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<Application> 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<ApplicationPagesDTO> 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<String> pageNames = applicationPagesDTO.getPages().stream()
.map(PageNameIdDTO::getName)
.collect(Collectors.toList());
assertThat(pageNames).contains("Home", "About", "Contact US");
assertThat(pageNames).doesNotContain("Profile");
}).verifyComplete();
}
@Test @Test
@WithUserDetails(value = "api_user") @WithUserDetails(value = "api_user")
public void exportApplicationById_WhenThemeDoesNotExist_ExportedWithDefaultTheme() { public void exportApplicationById_WhenThemeDoesNotExist_ExportedWithDefaultTheme() {