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;
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<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();
Mono<ApplicationTemplate> getTemplateDetails(String templateId);
Mono<Application> importApplicationFromTemplate(String templateId, String organizationId);
Mono<Application> mergeTemplateWithApplication(String templateId, String applicationId, String organizationId, String branchName, List<String> pagesToImport);
Mono<ApplicationTemplate> getFilters();
}

View File

@ -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<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.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<Theme, String> {
Mono<Theme> updateName(String id, Theme theme);
Mono<Theme> getOrSaveTheme(Theme theme, Application destApplication);
Mono<Application> archiveApplicationThemes(Application application);
Mono<Application> importThemesToApplication(Application destinationApp, ApplicationJson sourceJson);
}

View File

@ -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<ThemeRepositoryCE, Theme, St
return repository.getSystemThemeByName(theme.getName())
.switchIfEmpty(repository.getSystemThemeByName(Theme.DEFAULT_THEME_NAME));
} else {
theme.setApplicationId(null);
theme.setOrganizationId(null);
theme.setPolicies(policyGenerator.getAllChildPolicies(
// create a new theme
Theme newTheme = new Theme();
newTheme.setPolicies(policyGenerator.getAllChildPolicies(
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()))
.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;
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

View File

@ -32,6 +32,8 @@ public interface ImportExportApplicationServiceCE {
*/
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
*

View File

@ -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<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
*
* @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<Application> importApplicationInWorkspace(String workspaceId,
ApplicationJson applicationJson,
String applicationId,
String branchName) {
private Mono<Application> 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<String, String> pluginMap = new HashMap<>();
Map<String, String> datasourceMap = new HashMap<>();
Map<String, NewPage> 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<Datasource>());
})
.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<List<NewPage>> existingPagesMono = newPageService
.findNewPagesByApplicationId(importedApplication.getId(), MANAGE_PAGES)
.collectList()
.cache();
return importAndSavePages(
Flux<NewPage> importNewPageFlux = importAndSavePages(
importedNewPageList,
importedApplication,
savedApp,
importedDoc.getPublishedLayoutmongoEscapedWidgets(),
importedDoc.getUnpublishedLayoutmongoEscapedWidgets(),
branchName,
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 -> {
// 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<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;
return reorderPages(applicationJson, importedDoc, pageNameMap);
})
.flatMap(applicationPages -> {
if (!StringUtils.isEmpty(applicationId)) {
if (!StringUtils.isEmpty(applicationId) && !appendToApp) {
Set<String> 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<String> 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<String> 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<String> 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<String> 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<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
*
@ -1922,22 +2009,11 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
return srcTheme;
}
private Mono<Application> importThemes(Application application, ApplicationJson importedApplicationJson) {
Mono<Theme> importedEditModeTheme = themeService.getOrSaveTheme(importedApplicationJson.getEditModeTheme(), application);
Mono<Theme> 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<Application> 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<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.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<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.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<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
@WithUserDetails(value = "api_user")
public void exportApplicationById_WhenThemeDoesNotExist_ExportedWithDefaultTheme() {