feat: granular widgets for canvas (#24764)
## Description Today if we have more than one collaborator on a page, merging is an absolute nightmare. All of the widget config for a page is represented in one single giant JSON blob in canvas.json this makes it very hard to make isolated changes. This PR is breaking down the git representation into smaller JSON files that are aggregated by the running app. Fixes https://github.com/appsmithorg/appsmith/issues/17033 #### Type of change - New feature (non-breaking change which adds functionality) ## Testing #### How Has This Been Tested? - [ ] Manual - [ ] Jest - [ ] Cypress - [ ] JUnit #### Test Plan #### Issues raised during DP testing ## Checklist: #### Dev activity - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] PR is being merged under a feature flag #### QA activity: - [ ] [Speedbreak features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-) have been covered - [ ] Test plan covers all impacted features and [areas of interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-) - [ ] Test plan has been peer reviewed by project stakeholders and other QA members - [ ] Manually tested functionality on DP - [ ] We had an implementation alignment call with stakeholders post QA Round 2 - [ ] Cypress test cases have been added and approved by SDET/manual QA - [ ] Added `Test Plan Approved` label after Cypress tests were reviewed - [ ] Added `Test Plan Approved` label after JUnit tests were reviewed --------- Co-authored-by: brayn003 <rudra@appsmith.com> Co-authored-by: Parthvi <80334441+Parthvi12@users.noreply.github.com>
This commit is contained in:
parent
f442682834
commit
578b82080f
|
|
@ -105,8 +105,9 @@ describe("Git Bugs", function () {
|
|||
_.agHelper.GetNClick(_.locators._dialogCloseButton);
|
||||
});
|
||||
});
|
||||
|
||||
it("5. Bug 24206 : Open repository button is not functional in git sync modal", function () {
|
||||
// skipping this test for now, will update test logic and create new PR for it
|
||||
// TODO Parthvi
|
||||
it.skip("5. Bug 24206 : Open repository button is not functional in git sync modal", function () {
|
||||
_.gitSync.SwitchGitBranch("master");
|
||||
_.entityExplorer.DragDropWidgetNVerify("modalwidget", 50, 50);
|
||||
_.gitSync.CommitAndPush();
|
||||
|
|
|
|||
|
|
@ -139,9 +139,9 @@ describe("Git import flow ", function () {
|
|||
|
||||
it("3. Verfiy imported app should have all the data binding visible in view and edit mode", () => {
|
||||
// verify postgres data binded to table
|
||||
cy.get(".tbody").first().should("contain.text", "Test user 7");
|
||||
cy.get(".tbody").should("contain.text", "Test user 7");
|
||||
//verify MySQL data binded to table
|
||||
cy.get(".tbody").last().should("contain.text", "New Config");
|
||||
cy.get(".tbody").should("contain.text", "New Config");
|
||||
// verify api response binded to input widget
|
||||
cy.xpath("//input[@value='this is a test']").should("be.visible");
|
||||
// verify js object binded to input widget
|
||||
|
|
@ -155,9 +155,9 @@ describe("Git import flow ", function () {
|
|||
newBranch = branName;
|
||||
cy.log("newBranch is " + newBranch);
|
||||
});
|
||||
cy.get(".tbody").first().should("contain.text", "Test user 7");
|
||||
cy.get(".tbody").should("contain.text", "Test user 7");
|
||||
// verify MySQL data binded to table
|
||||
cy.get(".tbody").last().should("contain.text", "New Config");
|
||||
cy.get(".tbody").should("contain.text", "New Config");
|
||||
// verify api response binded to input widget
|
||||
cy.xpath("//input[@value='this is a test']");
|
||||
// verify js object binded to input widget
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ import DiscardFailedWarning from "../components/DiscardChangesError";
|
|||
|
||||
const Section = styled.div`
|
||||
margin-top: 0;
|
||||
margin-bottom: ${(props) => props.theme.spaces[11]}px;
|
||||
margin-bottom: ${(props) => props.theme.spaces[7]}px;
|
||||
`;
|
||||
|
||||
const Row = styled.div`
|
||||
|
|
@ -337,7 +337,7 @@ function Deploy() {
|
|||
{isFetchingGitStatus && (
|
||||
<StatusLoader loaderMsg={createMessage(FETCH_GIT_STATUS)} />
|
||||
)}
|
||||
<Space size={11} />
|
||||
{/* <Space size={11} /> */}
|
||||
{pullRequired && !isConflicting && (
|
||||
<>
|
||||
<Callout
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import {
|
|||
} from "@appsmith/constants/messages";
|
||||
import { getCurrentApplication } from "selectors/editorSelectors";
|
||||
import { changeInfoSinceLastCommit } from "../utils";
|
||||
import { Icon, Text } from "design-system";
|
||||
import { Callout, Icon, Text } from "design-system";
|
||||
|
||||
const DummyChange = styled.div`
|
||||
width: 50%;
|
||||
|
|
@ -36,9 +36,13 @@ const Wrapper = styled.div`
|
|||
gap: 6px;
|
||||
`;
|
||||
|
||||
const CalloutContainer = styled.div`
|
||||
margin-top: ${(props) => props.theme.spaces[7]}px;
|
||||
`;
|
||||
|
||||
const Changes = styled.div`
|
||||
margin-top: ${(props) => props.theme.spaces[7]}px;
|
||||
margin-bottom: ${(props) => props.theme.spaces[11]}px;
|
||||
margin-bottom: ${(props) => props.theme.spaces[7]}px;
|
||||
`;
|
||||
|
||||
export enum Kind {
|
||||
|
|
@ -216,6 +220,13 @@ export default function GitChangesList() {
|
|||
return loading ? (
|
||||
<DummyChange data-testid={"t--git-change-loading-dummy"} />
|
||||
) : changes.length ? (
|
||||
<Changes data-testid={"t--git-change-statuses"}>{changes}</Changes>
|
||||
<Changes data-testid={"t--git-change-statuses"}>
|
||||
{changes}
|
||||
{status?.migrationMessage ? (
|
||||
<CalloutContainer>
|
||||
<Callout kind="info">{status.migrationMessage}</Callout>
|
||||
</CalloutContainer>
|
||||
) : null}
|
||||
</Changes>
|
||||
) : null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -552,6 +552,7 @@ export type GitStatusData = {
|
|||
modifiedDatasources: number;
|
||||
modifiedJSLibs: number;
|
||||
discardDocUrl?: string;
|
||||
migrationMessage?: string;
|
||||
};
|
||||
|
||||
type GitErrorPayloadType = {
|
||||
|
|
|
|||
|
|
@ -2,13 +2,26 @@ package com.appsmith.git.constants;
|
|||
|
||||
public class CommonConstants {
|
||||
// This field will be useful when we migrate fields within JSON files (currently this will be useful for Git feature)
|
||||
public static Integer fileFormatVersion = 4;
|
||||
public static Integer fileFormatVersion = 5;
|
||||
public static String FILE_FORMAT_VERSION = "fileFormatVersion";
|
||||
|
||||
public static final String CANVAS = "canvas";
|
||||
|
||||
public static final String APPLICATION = "application";
|
||||
public static final String THEME = "theme";
|
||||
public static final String METADATA = "metadata";
|
||||
public static final String JSON_EXTENSION = ".json";
|
||||
public static final String JS_EXTENSION = ".js";
|
||||
public static final String TEXT_FILE_EXTENSION = ".txt";
|
||||
public static final String WIDGETS = "widgets";
|
||||
public static final String WIDGET_NAME = "widgetName";
|
||||
public static final String WIDGET_TYPE = "type";
|
||||
public static final String CHILDREN = "children";
|
||||
|
||||
public static final String CANVAS_WIDGET = "CANVAS_WIDGET";
|
||||
public static final String MAIN_CONTAINER = "MainContainer";
|
||||
public static final String DELIMITER_POINT = ".";
|
||||
public static final String DELIMITER_PATH = "/";
|
||||
public static final String EMPTY_STRING = "";
|
||||
public static final String FILE_MIGRATION_MESSAGE = "Some of the changes above are due to an improved file structure designed to reduce merge conflicts. You can safely commit them to your repository.";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,180 @@
|
|||
package com.appsmith.git.helpers;
|
||||
|
||||
import com.appsmith.git.constants.CommonConstants;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class DSLTransformerHelper {
|
||||
|
||||
public static Map<String, JSONObject> flatten(JSONObject jsonObject) {
|
||||
Map<String, JSONObject> flattenedMap = new HashMap<>();
|
||||
flattenObject(jsonObject, CommonConstants.EMPTY_STRING, flattenedMap);
|
||||
return new TreeMap<>(flattenedMap);
|
||||
}
|
||||
|
||||
private static void flattenObject(JSONObject jsonObject, String prefix, Map<String, JSONObject> flattenedMap) {
|
||||
String widgetName = jsonObject.optString(CommonConstants.WIDGET_NAME);
|
||||
if (widgetName.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
JSONArray children = jsonObject.optJSONArray(CommonConstants.CHILDREN);
|
||||
if (children != null) {
|
||||
// Check if the children object has type=CANVAS_WIDGET
|
||||
removeChildrenIfNotCanvasWidget(jsonObject);
|
||||
if (!isCanvasWidget(jsonObject)) {
|
||||
flattenedMap.put(prefix + widgetName, jsonObject);
|
||||
}
|
||||
|
||||
for (int i = 0; i < children.length(); i++) {
|
||||
JSONObject childObject = children.getJSONObject(i);
|
||||
String childPrefix = prefix + widgetName + CommonConstants.DELIMITER_POINT;
|
||||
flattenObject(childObject, childPrefix, flattenedMap);
|
||||
}
|
||||
} else {
|
||||
if (!isCanvasWidget(jsonObject)) {
|
||||
flattenedMap.put(prefix + widgetName, jsonObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static JSONObject removeChildrenIfNotCanvasWidget(JSONObject jsonObject) {
|
||||
JSONArray children = jsonObject.optJSONArray(CommonConstants.CHILDREN);
|
||||
if (children.length() == 1) {
|
||||
JSONObject child = children.getJSONObject(0);
|
||||
if (!CommonConstants.CANVAS_WIDGET.equals(child.optString(CommonConstants.WIDGET_TYPE))) {
|
||||
jsonObject.remove(CommonConstants.CHILDREN);
|
||||
} else {
|
||||
JSONObject childCopy = new JSONObject(child.toString());
|
||||
childCopy.remove(CommonConstants.CHILDREN);
|
||||
JSONArray jsonArray = new JSONArray();
|
||||
jsonArray.put(childCopy);
|
||||
jsonObject.put(CommonConstants.CHILDREN, jsonArray);
|
||||
}
|
||||
} else {
|
||||
jsonObject.remove(CommonConstants.CHILDREN);
|
||||
}
|
||||
return jsonObject;
|
||||
}
|
||||
|
||||
public static boolean hasChildren(JSONObject jsonObject) {
|
||||
JSONArray children = jsonObject.optJSONArray(CommonConstants.CHILDREN);
|
||||
return children != null && children.length() > 0;
|
||||
}
|
||||
|
||||
public static boolean isCanvasWidget(JSONObject jsonObject) {
|
||||
return jsonObject.optString(CommonConstants.WIDGET_TYPE).startsWith(CommonConstants.CANVAS_WIDGET);
|
||||
}
|
||||
|
||||
public static Map<String, List<String>> calculateParentDirectories(List<String> paths) {
|
||||
Map<String, List<String>> parentDirectories = new HashMap<>();
|
||||
|
||||
paths = paths.stream().map(currentPath -> currentPath.replace(CommonConstants.JSON_EXTENSION, CommonConstants.EMPTY_STRING)).collect(Collectors.toList());
|
||||
for (String path : paths) {
|
||||
String[] directories = path.split(CommonConstants.DELIMITER_PATH);
|
||||
int lastDirectoryIndex = directories.length - 1;
|
||||
|
||||
if (lastDirectoryIndex > 0 && directories[lastDirectoryIndex].equals(directories[lastDirectoryIndex - 1])) {
|
||||
if (lastDirectoryIndex - 2 >= 0) {
|
||||
String parentDirectory = directories[lastDirectoryIndex - 2];
|
||||
List<String> pathsList = parentDirectories.getOrDefault(parentDirectory, new ArrayList<>());
|
||||
pathsList.add(path);
|
||||
parentDirectories.put(parentDirectory, pathsList);
|
||||
}
|
||||
} else {
|
||||
String parentDirectory = directories[lastDirectoryIndex - 1];
|
||||
List<String> pathsList = parentDirectories.getOrDefault(parentDirectory, new ArrayList<>());
|
||||
pathsList.add(path);
|
||||
parentDirectories.put(parentDirectory, pathsList);
|
||||
}
|
||||
}
|
||||
|
||||
return parentDirectories;
|
||||
}
|
||||
|
||||
/*
|
||||
* /Form1/Button1.json,
|
||||
* /List1/List1.json,
|
||||
* /List1/Container1/Text2.json,
|
||||
* /List1/Container1/Image1.json,
|
||||
* /Form1/Button2.json,
|
||||
* /List1/Container1/Text1.json,
|
||||
* /Form1/Text3.json,
|
||||
* /Form1/Form1.json,
|
||||
* /List1/Container1/Container1.json,
|
||||
* /MainContainer.json
|
||||
*/
|
||||
public static JSONObject getNestedDSL(Map<String, JSONObject> jsonMap, Map<String, List<String>> pathMapping, JSONObject mainContainer) {
|
||||
// start from the root
|
||||
// Empty page with no widgets
|
||||
if (!pathMapping.containsKey(CommonConstants.MAIN_CONTAINER)) {
|
||||
return mainContainer;
|
||||
}
|
||||
for (String path : pathMapping.get(CommonConstants.MAIN_CONTAINER)) {
|
||||
JSONObject child = getChildren(path, jsonMap, pathMapping);
|
||||
JSONArray children = mainContainer.optJSONArray(CommonConstants.CHILDREN);
|
||||
if (children == null) {
|
||||
children = new JSONArray();
|
||||
children.put(child);
|
||||
mainContainer.put(CommonConstants.CHILDREN, children);
|
||||
} else {
|
||||
children.put(child);
|
||||
}
|
||||
}
|
||||
return mainContainer;
|
||||
}
|
||||
|
||||
public static JSONObject getChildren(String pathToWidget, Map<String, JSONObject> jsonMap, Map<String, List<String>> pathMapping) {
|
||||
// Recursively get the children
|
||||
List<String> children = pathMapping.get(getWidgetName(pathToWidget));
|
||||
JSONObject parentObject = jsonMap.get(pathToWidget + CommonConstants.JSON_EXTENSION);
|
||||
if (children != null) {
|
||||
JSONArray childArray = new JSONArray();
|
||||
for (String childWidget : children) {
|
||||
childArray.put(getChildren(childWidget, jsonMap, pathMapping));
|
||||
}
|
||||
// Check if the parent object has type=CANVAS_WIDGET as children
|
||||
// If yes, then add the children array to the CANVAS_WIDGET's children
|
||||
appendChildren(parentObject, childArray);
|
||||
}
|
||||
|
||||
return parentObject;
|
||||
}
|
||||
|
||||
public static String getWidgetName(String path) {
|
||||
String[] directories = path.split(CommonConstants.DELIMITER_PATH);
|
||||
return directories[directories.length - 1];
|
||||
}
|
||||
|
||||
public static JSONObject appendChildren(JSONObject parent, JSONArray childWidgets) {
|
||||
JSONArray children = parent.optJSONArray(CommonConstants.CHILDREN);
|
||||
if (children == null) {
|
||||
parent.put(CommonConstants.CHILDREN, childWidgets);
|
||||
} else {
|
||||
// Is the children CANVAS_WIDGET
|
||||
if (children.length() == 1) {
|
||||
JSONObject childObject = children.getJSONObject(0);
|
||||
if (CommonConstants.CANVAS_WIDGET.equals(childObject.optString(CommonConstants.WIDGET_TYPE))) {
|
||||
childObject.put(CommonConstants.CHILDREN, childWidgets);
|
||||
}
|
||||
} else {
|
||||
parent.put(CommonConstants.CHILDREN, childWidgets);
|
||||
}
|
||||
}
|
||||
return parent;
|
||||
}
|
||||
}
|
||||
|
|
@ -12,24 +12,34 @@ import com.appsmith.git.configurations.GitServiceConfig;
|
|||
import com.appsmith.git.constants.CommonConstants;
|
||||
import com.appsmith.git.converters.GsonDoubleToLongConverter;
|
||||
import com.appsmith.git.converters.GsonUnorderedToOrderedConverter;
|
||||
import com.appsmith.util.WebClientUtils;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.FileSystemUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Scheduler;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
import reactor.netty.resources.ConnectionProvider;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
|
|
@ -42,10 +52,13 @@ import java.nio.file.DirectoryNotEmptyException;
|
|||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
|
@ -83,6 +96,8 @@ public class FileUtilsImpl implements FileInterface {
|
|||
|
||||
private final Scheduler scheduler = Schedulers.boundedElastic();
|
||||
|
||||
private static final String CANVAS_WIDGET = "(Canvas)[0-9]*.";
|
||||
|
||||
/**
|
||||
Application will be stored in the following structure:
|
||||
|
||||
|
|
@ -227,11 +242,38 @@ public class FileUtilsImpl implements FileInterface {
|
|||
|
||||
Set<String> validPages = new HashSet<>();
|
||||
for (Map.Entry<String, Object> pageResource : pageEntries) {
|
||||
Map<String, String> validWidgetToParentMap = new HashMap<>();
|
||||
final String pageName = pageResource.getKey();
|
||||
Path pageSpecificDirectory = pageDirectory.resolve(pageName);
|
||||
Boolean isResourceUpdated = updatedResources.get(PAGE_LIST).contains(pageName);
|
||||
if(Boolean.TRUE.equals(isResourceUpdated)) {
|
||||
saveResource(pageResource.getValue(), pageSpecificDirectory.resolve(CommonConstants.CANVAS + CommonConstants.JSON_EXTENSION), gson);
|
||||
// Save page metadata
|
||||
saveResource(pageResource.getValue(), pageSpecificDirectory.resolve(pageName + CommonConstants.JSON_EXTENSION), gson);
|
||||
Map<String, JSONObject> result = DSLTransformerHelper.flatten(new JSONObject(applicationGitReference.getPageDsl().get(pageName)));
|
||||
result.forEach((key, jsonObject) -> {
|
||||
// get path with splitting the name via key
|
||||
String widgetName = key.substring(key.lastIndexOf(CommonConstants.DELIMITER_POINT ) + 1 );
|
||||
String childPath = key.replace(CommonConstants.MAIN_CONTAINER, CommonConstants.EMPTY_STRING)
|
||||
.replace(CommonConstants.DELIMITER_POINT, CommonConstants.DELIMITER_PATH);
|
||||
// Replace the canvas Widget as a child and add it to the same level as parent
|
||||
childPath = childPath.replaceAll(CANVAS_WIDGET, CommonConstants.EMPTY_STRING);
|
||||
if (!DSLTransformerHelper.hasChildren(jsonObject)) {
|
||||
// Save the widget as a directory or Save the widget as a file
|
||||
childPath = childPath.replace(widgetName, CommonConstants.EMPTY_STRING);
|
||||
}
|
||||
Path path = Paths.get(String.valueOf(pageSpecificDirectory.resolve(CommonConstants.WIDGETS)), childPath);
|
||||
validWidgetToParentMap.put(widgetName, path.toFile().toString());
|
||||
saveWidgets(
|
||||
jsonObject,
|
||||
widgetName,
|
||||
path
|
||||
);
|
||||
});
|
||||
// Remove deleted widgets from the file system
|
||||
deleteWidgets(pageSpecificDirectory.resolve(CommonConstants.WIDGETS).toFile(), validWidgetToParentMap);
|
||||
|
||||
// Remove the canvas.json from the file system since the value is stored in the page.json
|
||||
deleteFile(pageSpecificDirectory.resolve(CommonConstants.CANVAS + CommonConstants.JSON_EXTENSION));
|
||||
}
|
||||
validPages.add(pageName);
|
||||
}
|
||||
|
|
@ -371,6 +413,15 @@ public class FileUtilsImpl implements FileInterface {
|
|||
return false;
|
||||
}
|
||||
|
||||
private void saveWidgets(JSONObject sourceEntity, String resourceName, Path path) {
|
||||
try {
|
||||
Files.createDirectories(path);
|
||||
writeStringToFile(sourceEntity.toString(4), path.resolve(resourceName + CommonConstants.JSON_EXTENSION));
|
||||
} catch (IOException e) {
|
||||
log.debug("Error while writings widgets data to file, {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to write actionCollection specific resource to file system. We write the data in two steps
|
||||
* 1. Actual js code
|
||||
|
|
@ -453,8 +504,10 @@ public class FileUtilsImpl implements FileInterface {
|
|||
// unwanted file : corresponding resource from DB has been deleted
|
||||
if(resourceDirectory.toFile().exists()) {
|
||||
try (Stream<Path> paths = Files.walk(resourceDirectory)) {
|
||||
paths
|
||||
.filter(path -> Files.isRegularFile(path) && !validResources.contains(path.getFileName().toString()))
|
||||
paths.filter(pathLocal ->
|
||||
Files.isRegularFile(pathLocal) &&
|
||||
!validResources.contains(pathLocal.getFileName().toString())
|
||||
)
|
||||
.forEach(this::deleteFile);
|
||||
} catch (IOException e) {
|
||||
log.debug("Error while scanning directory: {}, with error {}", resourceDirectory, e);
|
||||
|
|
@ -491,7 +544,7 @@ public class FileUtilsImpl implements FileInterface {
|
|||
try {
|
||||
FileUtils.deleteDirectory(directory.toFile());
|
||||
} catch (IOException e){
|
||||
log.debug("Unable to delete directory for path {} with message {}", directory, e.getMessage());
|
||||
log.error("Unable to delete directory for path {} with message {}", directory, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -507,11 +560,11 @@ public class FileUtilsImpl implements FileInterface {
|
|||
}
|
||||
catch(DirectoryNotEmptyException e)
|
||||
{
|
||||
log.debug("Unable to delete non-empty directory at {}", filePath);
|
||||
log.error("Unable to delete non-empty directory at {}", filePath);
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
log.debug("Unable to delete file, {}", e.getMessage());
|
||||
log.error("Unable to delete file, {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -650,7 +703,7 @@ public class FileUtilsImpl implements FileInterface {
|
|||
* @return content of the file in the path
|
||||
*/
|
||||
private String readFileAsString(Path filePath) {
|
||||
String data = "";
|
||||
String data = CommonConstants.EMPTY_STRING;
|
||||
try {
|
||||
data = FileUtils.readFileToString(filePath.toFile(), "UTF-8");
|
||||
} catch (IOException e) {
|
||||
|
|
@ -672,7 +725,7 @@ public class FileUtilsImpl implements FileInterface {
|
|||
for (File dirFile : Objects.requireNonNull(directory.listFiles())) {
|
||||
String resourceName = dirFile.getName();
|
||||
Path resourcePath = directoryPath.resolve(resourceName).resolve( resourceName + CommonConstants.JS_EXTENSION);
|
||||
String body = "";
|
||||
String body = CommonConstants.EMPTY_STRING;
|
||||
if (resourcePath.toFile().exists()) {
|
||||
body = readFileAsString(resourcePath);
|
||||
}
|
||||
|
|
@ -697,7 +750,7 @@ public class FileUtilsImpl implements FileInterface {
|
|||
if (directory.isDirectory()) {
|
||||
for (File dirFile : Objects.requireNonNull(directory.listFiles())) {
|
||||
String resourceName = dirFile.getName();
|
||||
String body = "";
|
||||
String body = CommonConstants.EMPTY_STRING;
|
||||
Path queryPath = directoryPath.resolve(resourceName).resolve( resourceName + CommonConstants.TEXT_FILE_EXTENSION);
|
||||
if (queryPath.toFile().exists()) {
|
||||
body = readFileAsString(queryPath);
|
||||
|
|
@ -710,6 +763,10 @@ public class FileUtilsImpl implements FileInterface {
|
|||
return resource;
|
||||
}
|
||||
|
||||
private Object readPageMetadata(Path directoryPath, Gson gson) {
|
||||
return readFile(directoryPath.resolve(directoryPath.toFile().getName() + CommonConstants.JSON_EXTENSION), gson);
|
||||
}
|
||||
|
||||
private ApplicationGitReference fetchApplicationReference(Path baseRepoPath, Gson gson) {
|
||||
ApplicationGitReference applicationGitReference = new ApplicationGitReference();
|
||||
// Extract application metadata from the json
|
||||
|
|
@ -727,13 +784,13 @@ public class FileUtilsImpl implements FileInterface {
|
|||
switch (fileFormatVersion) {
|
||||
case 1 :
|
||||
// Extract actions
|
||||
applicationGitReference.setActions(readFiles(baseRepoPath.resolve(ACTION_DIRECTORY), gson, ""));
|
||||
applicationGitReference.setActions(readFiles(baseRepoPath.resolve(ACTION_DIRECTORY), gson, CommonConstants.EMPTY_STRING));
|
||||
// Extract actionCollections
|
||||
applicationGitReference.setActionCollections(readFiles(baseRepoPath.resolve(ACTION_COLLECTION_DIRECTORY), gson, ""));
|
||||
applicationGitReference.setActionCollections(readFiles(baseRepoPath.resolve(ACTION_COLLECTION_DIRECTORY), gson, CommonConstants.EMPTY_STRING));
|
||||
// Extract pages
|
||||
applicationGitReference.setPages(readFiles(pageDirectory, gson, ""));
|
||||
applicationGitReference.setPages(readFiles(pageDirectory, gson, CommonConstants.EMPTY_STRING));
|
||||
// Extract datasources
|
||||
applicationGitReference.setDatasources(readFiles(baseRepoPath.resolve(DATASOURCE_DIRECTORY), gson, ""));
|
||||
applicationGitReference.setDatasources(readFiles(baseRepoPath.resolve(DATASOURCE_DIRECTORY), gson, CommonConstants.EMPTY_STRING));
|
||||
break;
|
||||
|
||||
case 2:
|
||||
|
|
@ -742,18 +799,23 @@ public class FileUtilsImpl implements FileInterface {
|
|||
updateGitApplicationReference(baseRepoPath, gson, applicationGitReference, pageDirectory, fileFormatVersion);
|
||||
break;
|
||||
|
||||
case 5:
|
||||
updateGitApplicationReferenceV2(baseRepoPath, gson, applicationGitReference, pageDirectory, fileFormatVersion);
|
||||
break;
|
||||
|
||||
default:
|
||||
}
|
||||
applicationGitReference.setMetadata(metadata);
|
||||
|
||||
Path jsLibDirectory = baseRepoPath.resolve(JS_LIB_DIRECTORY);
|
||||
Map<String, Object> jsLibrariesMap = readFiles(jsLibDirectory, gson, "");
|
||||
Map<String, Object> jsLibrariesMap = readFiles(jsLibDirectory, gson, CommonConstants.EMPTY_STRING);
|
||||
applicationGitReference.setJsLibraries(jsLibrariesMap);
|
||||
|
||||
return applicationGitReference;
|
||||
}
|
||||
|
||||
private void updateGitApplicationReference(Path baseRepoPath, Gson gson, ApplicationGitReference applicationGitReference, Path pageDirectory, int fileFormatVersion) {
|
||||
@Deprecated
|
||||
private void updateGitApplicationReference(Path baseRepoPath, Gson gson, ApplicationGitReference applicationGitReference, Path pageDirectory, int fileFormatVersion) {
|
||||
// Extract pages and nested actions and actionCollections
|
||||
File directory = pageDirectory.toFile();
|
||||
Map<String, Object> pageMap = new HashMap<>();
|
||||
|
|
@ -761,7 +823,6 @@ public class FileUtilsImpl implements FileInterface {
|
|||
Map<String, String> actionBodyMap = new HashMap<>();
|
||||
Map<String, Object> actionCollectionMap = new HashMap<>();
|
||||
Map<String, String> actionCollectionBodyMap = new HashMap<>();
|
||||
// TODO same approach should be followed for modules(app level actions, actionCollections, widgets etc)
|
||||
if (directory.isDirectory()) {
|
||||
// Loop through all the directories and nested directories inside the pages directory to extract
|
||||
// pages, actions and actionCollections from the JSON files
|
||||
|
|
@ -787,7 +848,7 @@ public class FileUtilsImpl implements FileInterface {
|
|||
applicationGitReference.setActionCollectionBody(actionCollectionBodyMap);
|
||||
applicationGitReference.setPages(pageMap);
|
||||
// Extract datasources
|
||||
applicationGitReference.setDatasources(readFiles(baseRepoPath.resolve(DATASOURCE_DIRECTORY), gson, ""));
|
||||
applicationGitReference.setDatasources(readFiles(baseRepoPath.resolve(DATASOURCE_DIRECTORY), gson, CommonConstants.EMPTY_STRING));
|
||||
}
|
||||
|
||||
private Integer getFileFormatVersion(Object metadata) {
|
||||
|
|
@ -803,4 +864,121 @@ public class FileUtilsImpl implements FileInterface {
|
|||
private boolean isFileFormatCompatible(int savedFileFormat) {
|
||||
return savedFileFormat <= CommonConstants.fileFormatVersion;
|
||||
}
|
||||
|
||||
private void updateGitApplicationReferenceV2(Path baseRepoPath, Gson gson, ApplicationGitReference applicationGitReference, Path pageDirectory, int fileFormatVersion) {
|
||||
// Extract pages and nested actions and actionCollections
|
||||
File directory = pageDirectory.toFile();
|
||||
Map<String, Object> pageMap = new HashMap<>();
|
||||
Map<String, String> pageDsl = new HashMap<>();
|
||||
Map<String, Object> actionMap = new HashMap<>();
|
||||
Map<String, String> actionBodyMap = new HashMap<>();
|
||||
Map<String, Object> actionCollectionMap = new HashMap<>();
|
||||
Map<String, String> actionCollectionBodyMap = new HashMap<>();
|
||||
if (directory.isDirectory()) {
|
||||
// Loop through all the directories and nested directories inside the pages directory to extract
|
||||
// pages, actions and actionCollections from the JSON files
|
||||
for (File page : Objects.requireNonNull(directory.listFiles())) {
|
||||
pageMap.put(page.getName(), readPageMetadata(page.toPath(), gson));
|
||||
|
||||
JSONObject mainContainer = getMainContainer(pageMap.get(page.getName()), gson);
|
||||
|
||||
// Read widgets data recursively from the widgets directory
|
||||
Map<String, JSONObject> widgetsData = readWidgetsData(page.toPath().resolve(CommonConstants.WIDGETS).toString());
|
||||
// Construct the nested DSL from the widgets data
|
||||
Map<String, List<String>> parentDirectories = DSLTransformerHelper.calculateParentDirectories(widgetsData.keySet().stream().toList());
|
||||
JSONObject nestedDSL = DSLTransformerHelper.getNestedDSL(widgetsData, parentDirectories, mainContainer);
|
||||
pageDsl.put(page.getName(), nestedDSL.toString());
|
||||
actionMap.putAll(readAction(page.toPath().resolve(ACTION_DIRECTORY), gson, page.getName(), actionBodyMap));
|
||||
actionCollectionMap.putAll(readActionCollection(page.toPath().resolve(ACTION_COLLECTION_DIRECTORY), gson, page.getName(), actionCollectionBodyMap));
|
||||
}
|
||||
}
|
||||
applicationGitReference.setActions(actionMap);
|
||||
applicationGitReference.setActionBody(actionBodyMap);
|
||||
applicationGitReference.setActionCollections(actionCollectionMap);
|
||||
applicationGitReference.setActionCollectionBody(actionCollectionBodyMap);
|
||||
applicationGitReference.setPages(pageMap);
|
||||
applicationGitReference.setPageDsl(pageDsl);
|
||||
// Extract datasources
|
||||
applicationGitReference.setDatasources(readFiles(baseRepoPath.resolve(DATASOURCE_DIRECTORY), gson, CommonConstants.EMPTY_STRING));
|
||||
}
|
||||
|
||||
private Map<String, JSONObject> readWidgetsData(String directoryPath) {
|
||||
Map<String, JSONObject> jsonMap = new HashMap<>();
|
||||
File directory = new File(directoryPath);
|
||||
|
||||
if (!directory.isDirectory()) {
|
||||
log.error("Error reading directory: {}", directoryPath);
|
||||
return jsonMap;
|
||||
}
|
||||
|
||||
try {
|
||||
readFilesRecursively(directory, jsonMap, directoryPath);
|
||||
} catch (IOException exception) {
|
||||
log.error("Error reading directory: {}, error message {}", directoryPath, exception.getMessage());
|
||||
}
|
||||
|
||||
return jsonMap;
|
||||
}
|
||||
|
||||
private void readFilesRecursively(File directory, Map<String, JSONObject> jsonMap, String rootPath) throws IOException {
|
||||
File[] files = directory.listFiles();
|
||||
if (files == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (File file : files) {
|
||||
if (file.isFile()) {
|
||||
String filePath = file.getAbsolutePath();
|
||||
String relativePath = filePath.replace(rootPath, CommonConstants.EMPTY_STRING);
|
||||
relativePath = CommonConstants.DELIMITER_PATH + CommonConstants.MAIN_CONTAINER + relativePath.substring(relativePath.indexOf("//") + 1);
|
||||
try {
|
||||
String fileContent = new String(Files.readAllBytes(file.toPath()));
|
||||
JSONObject jsonObject = new JSONObject(fileContent);
|
||||
jsonMap.put(relativePath, jsonObject);
|
||||
} catch (IOException exception) {
|
||||
log.error("Error reading file: {}, error message {}", filePath, exception.getMessage());
|
||||
}
|
||||
} else if (file.isDirectory()) {
|
||||
readFilesRecursively(file, jsonMap, rootPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteWidgets(File directory, Map<String, String> validWidgetToParentMap) {
|
||||
File[] files = directory.listFiles();
|
||||
if (files == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
deleteWidgets(file, validWidgetToParentMap);
|
||||
}
|
||||
|
||||
String name = file.getName().replace(CommonConstants.JSON_EXTENSION, CommonConstants.EMPTY_STRING);
|
||||
// If input widget was inside a container before, but the user moved it out of the container
|
||||
// then we need to delete the widget from the container directory
|
||||
// The check here is to validate if the parent is correct or not
|
||||
if ( !validWidgetToParentMap.containsKey(name)) {
|
||||
if (file.isDirectory()) {
|
||||
deleteDirectory(file.toPath());
|
||||
} else {
|
||||
deleteFile(file.toPath());
|
||||
}
|
||||
} else if(!file.getParentFile().getPath().toString().equals(validWidgetToParentMap.get(name))
|
||||
&& !file.getPath().toString().equals(validWidgetToParentMap.get(name))) {
|
||||
if (file.isDirectory()) {
|
||||
deleteDirectory(file.toPath());
|
||||
} else {
|
||||
deleteFile(file.toPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private JSONObject getMainContainer(Object pageJson, Gson gson) {
|
||||
JSONObject pageJSON = new JSONObject(gson.toJson(pageJson));
|
||||
JSONArray layouts = pageJSON.getJSONObject("unpublishedPage").getJSONArray("layouts");
|
||||
return layouts.getJSONObject(0).getJSONObject("dsl");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.appsmith.git.service;
|
|||
|
||||
import com.appsmith.external.constants.AnalyticsEvents;
|
||||
import com.appsmith.external.constants.ErrorReferenceDocUrl;
|
||||
import com.appsmith.external.constants.GitConstants;
|
||||
import com.appsmith.external.dtos.GitBranchDTO;
|
||||
import com.appsmith.external.dtos.GitLogDTO;
|
||||
import com.appsmith.external.dtos.GitStatusDTO;
|
||||
|
|
@ -488,36 +489,51 @@ public class GitExecutorImpl implements GitExecutor {
|
|||
|
||||
Set<String> queriesModified = new HashSet<>();
|
||||
Set<String> jsObjectsModified = new HashSet<>();
|
||||
Set<String> pagesModified = new HashSet<>();
|
||||
int modifiedPages = 0;
|
||||
int modifiedQueries = 0;
|
||||
int modifiedJSObjects = 0;
|
||||
int modifiedDatasources = 0;
|
||||
int modifiedJSLibs = 0;
|
||||
for (String x : modifiedAssets) {
|
||||
if (x.contains(CommonConstants.CANVAS)) {
|
||||
modifiedPages++;
|
||||
} else if (x.contains(GitDirectories.ACTION_DIRECTORY + "/")) {
|
||||
String queryName = x.split(GitDirectories.ACTION_DIRECTORY + "/")[1];
|
||||
int position = queryName.indexOf("/");
|
||||
// begins with pages and filename and parent name should be same or contains widgets
|
||||
if (x.contains(CommonConstants.WIDGETS)) {
|
||||
if (!pagesModified.contains(getPageName(x))) {
|
||||
pagesModified.add(getPageName(x));
|
||||
modifiedPages++;
|
||||
}
|
||||
} else if (!x.contains(CommonConstants.WIDGETS)
|
||||
&& x.startsWith(GitDirectories.PAGE_DIRECTORY)
|
||||
&& !x.contains(GitDirectories.ACTION_DIRECTORY)
|
||||
&& !x.contains(GitDirectories.ACTION_COLLECTION_DIRECTORY)) {
|
||||
if (!pagesModified.contains(getPageName(x))) {
|
||||
pagesModified.add(getPageName(x));
|
||||
modifiedPages++;
|
||||
}
|
||||
} else if (x.contains(GitDirectories.ACTION_DIRECTORY + CommonConstants.DELIMITER_PATH)) {
|
||||
String queryName = x.split(GitDirectories.ACTION_DIRECTORY + CommonConstants.DELIMITER_PATH)[1];
|
||||
int position = queryName.indexOf(CommonConstants.DELIMITER_PATH);
|
||||
if(position != -1) {
|
||||
queryName = queryName.substring(0, position);
|
||||
String pageName = x.split("/")[1];
|
||||
String pageName = x.split(CommonConstants.DELIMITER_PATH)[1];
|
||||
if (!queriesModified.contains(pageName + queryName)) {
|
||||
queriesModified.add(pageName + queryName);
|
||||
modifiedQueries++;
|
||||
}
|
||||
}
|
||||
} else if (x.contains(GitDirectories.ACTION_COLLECTION_DIRECTORY + "/") && !x.endsWith(".json")) {
|
||||
String queryName = x.substring(x.lastIndexOf("/") + 1);
|
||||
String pageName = x.split("/")[1];
|
||||
} else if (x.contains(GitDirectories.ACTION_COLLECTION_DIRECTORY + CommonConstants.DELIMITER_PATH) && !x.endsWith(CommonConstants.JSON_EXTENSION)) {
|
||||
String queryName = x.substring(x.lastIndexOf(CommonConstants.DELIMITER_PATH) + 1);
|
||||
String pageName = x.split(CommonConstants.DELIMITER_PATH)[1];
|
||||
if (!jsObjectsModified.contains(pageName + queryName)) {
|
||||
jsObjectsModified.add(pageName + queryName);
|
||||
modifiedJSObjects++;
|
||||
}
|
||||
} else if (x.contains(GitDirectories.DATASOURCE_DIRECTORY + "/")) {
|
||||
} else if (x.contains(GitDirectories.DATASOURCE_DIRECTORY + CommonConstants.DELIMITER_PATH)) {
|
||||
modifiedDatasources++;
|
||||
} else if (x.contains(GitDirectories.JS_LIB_DIRECTORY + "/")) {
|
||||
} else if (x.contains(GitDirectories.JS_LIB_DIRECTORY + CommonConstants.DELIMITER_PATH)) {
|
||||
modifiedJSLibs++;
|
||||
} else if (x.equals(CommonConstants.METADATA + CommonConstants.JSON_EXTENSION)) {
|
||||
response.setMigrationMessage(CommonConstants.FILE_MIGRATION_MESSAGE);
|
||||
}
|
||||
}
|
||||
response.setModified(modifiedAssets);
|
||||
|
|
@ -558,6 +574,11 @@ public class GitExecutorImpl implements GitExecutor {
|
|||
.subscribeOn(scheduler);
|
||||
}
|
||||
|
||||
private String getPageName(String path) {
|
||||
String[] pathArray = path.split(CommonConstants.DELIMITER_PATH);
|
||||
return pathArray[1];
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> mergeBranch(Path repoSuffix, String sourceBranch, String destinationBranch) {
|
||||
return Mono.fromCallable(() -> {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,255 @@
|
|||
package com.appsmith.git.helpers;
|
||||
|
||||
import com.appsmith.git.constants.CommonConstants;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
public class DSLTransformHelperTest {
|
||||
|
||||
private Map<String, JSONObject> jsonMap;
|
||||
private Map<String, List<String>> pathMapping;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
// Initialize the JSON map and path mapping for each test
|
||||
jsonMap = new HashMap<>();
|
||||
pathMapping = new HashMap<>();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHasChildren_WithChildren() {
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
JSONArray children = new JSONArray();
|
||||
children.put(new JSONObject());
|
||||
jsonObject.put(CommonConstants.CHILDREN, children);
|
||||
|
||||
boolean result = DSLTransformerHelper.hasChildren(jsonObject);
|
||||
|
||||
Assertions.assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHasChildren_WithoutChildren() {
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
|
||||
boolean result = DSLTransformerHelper.hasChildren(jsonObject);
|
||||
|
||||
Assertions.assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsCanvasWidget_WithCanvasWidget() {
|
||||
JSONObject widgetObject = new JSONObject();
|
||||
widgetObject.put(CommonConstants.WIDGET_TYPE, "CANVAS_WIDGET_1");
|
||||
boolean result = DSLTransformerHelper.isCanvasWidget(widgetObject);
|
||||
Assertions.assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsCanvasWidget_WithNonCanvasWidget() {
|
||||
JSONObject widgetObject = new JSONObject();
|
||||
widgetObject.put("widgetType", "BUTTON_WIDGET");
|
||||
|
||||
boolean result = DSLTransformerHelper.isCanvasWidget(widgetObject);
|
||||
|
||||
Assertions.assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsCanvasWidget_WithMissingWidgetType() {
|
||||
JSONObject widgetObject = new JSONObject();
|
||||
|
||||
boolean result = DSLTransformerHelper.isCanvasWidget(widgetObject);
|
||||
|
||||
Assertions.assertFalse(result);
|
||||
}
|
||||
|
||||
/*@Test
|
||||
public void testCalculateParentDirectories() {
|
||||
// Test Case 1: Simple paths
|
||||
List<String> paths1 = Arrays.asList(
|
||||
"/root/dir1/file1",
|
||||
"/root/dir1/file2",
|
||||
"/root/dir2/file3",
|
||||
"/root/dir3/file4"
|
||||
);
|
||||
Map<String, List<String>> result1 = DSLTransformerHelper.calculateParentDirectories(paths1);
|
||||
Map<String, List<String>> expected1 = new HashMap<>();
|
||||
expected1.put("root", Arrays.asList("/root/dir1/file1", "/root/dir1/file2", "/root/dir2/file3", "/root/dir3/file4"));
|
||||
expected1.put("dir1", Arrays.asList("/root/dir1/file1", "/root/dir1/file2"));
|
||||
expected1.put("dir2", Arrays.asList("/root/dir2/file3"));
|
||||
expected1.put("dir3", Arrays.asList("/root/dir3/file4"));
|
||||
Assertions.assertEquals(expected1, result1);
|
||||
|
||||
// Test Case 2: Paths with duplicate directories
|
||||
List<String> paths2 = Arrays.asList(
|
||||
"/root/dir1/file1",
|
||||
"/root/dir1/file2",
|
||||
"/root/dir2/file3",
|
||||
"/root/dir1/file4"
|
||||
);
|
||||
Map<String, List<String>> result2 = DSLTransformerHelper.calculateParentDirectories(paths2);
|
||||
Map<String, List<String>> expected2 = new HashMap<>();
|
||||
expected2.put("root", Arrays.asList("/root/dir1/file1", "/root/dir1/file2", "/root/dir2/file3", "/root/dir1/file4"));
|
||||
expected2.put("dir1", Arrays.asList("/root/dir1/file1", "/root/dir1/file2", "/root/dir1/file4"));
|
||||
expected2.put("dir2", Arrays.asList("/root/dir2/file3"));
|
||||
Assertions.assertEquals(expected2, result2);
|
||||
|
||||
// Test Case 3: Paths with empty list
|
||||
List<String> paths3 = Collections.emptyList();
|
||||
Map<String, List<String>> result3 = DSLTransformerHelper.calculateParentDirectories(paths3);
|
||||
Map<String, List<String>> expected3 = Collections.emptyMap();
|
||||
Assertions.assertEquals(expected3, result3);
|
||||
|
||||
// Test Case 4: Paths with single-level directories
|
||||
List<String> paths4 = Arrays.asList(
|
||||
"/dir1/file1",
|
||||
"/dir2/file2",
|
||||
"/dir3/file3"
|
||||
);
|
||||
Map<String, List<String>> result4 = DSLTransformerHelper.calculateParentDirectories(paths4);
|
||||
Map<String, List<String>> expected4 = new HashMap<>();
|
||||
expected4.put("dir1", Arrays.asList("/dir1/file1"));
|
||||
expected4.put("dir2", Arrays.asList("/dir2/file2"));
|
||||
expected4.put("dir3", Arrays.asList("/dir3/file3"));
|
||||
Assertions.assertEquals(expected4, result4);
|
||||
}*/
|
||||
|
||||
// Test case for nested JSON object construction --------------------------------------------------------------------
|
||||
@Test
|
||||
public void testGetNestedDSL_EmptyPageWithNoWidgets() {
|
||||
JSONObject mainContainer = new JSONObject();
|
||||
|
||||
JSONObject result = DSLTransformerHelper.getNestedDSL(jsonMap, pathMapping, mainContainer);
|
||||
|
||||
Assertions.assertEquals(mainContainer, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetChildren_WithNoChildren() {
|
||||
JSONObject widgetObject = new JSONObject();
|
||||
widgetObject.put("type", "CANVAS_WIDGET");
|
||||
widgetObject.put("id", "widget1");
|
||||
jsonMap.put("widget1.json", widgetObject);
|
||||
|
||||
List<String> pathList = new ArrayList<>();
|
||||
pathMapping.put("widget1", pathList);
|
||||
|
||||
JSONObject result = DSLTransformerHelper.getChildren("widget1", jsonMap, pathMapping);
|
||||
|
||||
Assertions.assertEquals(widgetObject, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetChildren_WithNestedChildren() {
|
||||
JSONObject widgetObject = new JSONObject();
|
||||
widgetObject.put(CommonConstants.WIDGET_TYPE, "CANVAS_WIDGET");
|
||||
widgetObject.put("id", "widget1");
|
||||
jsonMap.put("widget1.json", widgetObject);
|
||||
|
||||
JSONObject childObject = new JSONObject();
|
||||
childObject.put(CommonConstants.WIDGET_TYPE, "BUTTON_WIDGET");
|
||||
childObject.put("id", "widget2");
|
||||
jsonMap.put("widget2.json", childObject);
|
||||
|
||||
List<String> pathList = new ArrayList<>();
|
||||
pathList.add("widget2");
|
||||
pathMapping.put("widget1", pathList);
|
||||
|
||||
JSONObject result = DSLTransformerHelper.getChildren("widget1", jsonMap, pathMapping);
|
||||
|
||||
Assertions.assertEquals(widgetObject, result);
|
||||
JSONArray children = result.optJSONArray(CommonConstants.CHILDREN);
|
||||
Assertions.assertNotNull(children);
|
||||
Assertions.assertEquals(1, children.length());
|
||||
JSONObject child = children.getJSONObject(0);
|
||||
Assertions.assertEquals(childObject, child);
|
||||
JSONArray childChildren = child.optJSONArray(CommonConstants.CHILDREN);
|
||||
Assertions.assertNull(childChildren);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetWidgetName_WithSingleDirectory() {
|
||||
String path = "widgets/parent/child";
|
||||
|
||||
String result = DSLTransformerHelper.getWidgetName(path);
|
||||
|
||||
Assertions.assertEquals("child", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetWidgetName_WithMultipleDirectories() {
|
||||
String path = "widgets/parent/child/grandchild";
|
||||
|
||||
String result = DSLTransformerHelper.getWidgetName(path);
|
||||
|
||||
Assertions.assertEquals("grandchild", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetWidgetName_WithEmptyPath() {
|
||||
String path = "";
|
||||
|
||||
String result = DSLTransformerHelper.getWidgetName(path);
|
||||
|
||||
Assertions.assertEquals("", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetWidgetName_WithRootDirectory() {
|
||||
String path = "widgets";
|
||||
|
||||
String result = DSLTransformerHelper.getWidgetName(path);
|
||||
|
||||
Assertions.assertEquals("widgets", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAppendChildren_WithNoExistingChildren() {
|
||||
JSONObject parent = new JSONObject();
|
||||
JSONArray childWidgets = new JSONArray()
|
||||
.put(new JSONObject().put(CommonConstants.WIDGET_NAME, "Child1"))
|
||||
.put(new JSONObject().put(CommonConstants.WIDGET_NAME, "Child2"));
|
||||
|
||||
JSONObject result = DSLTransformerHelper.appendChildren(parent, childWidgets);
|
||||
|
||||
JSONArray expectedChildren = new JSONArray()
|
||||
.put(new JSONObject().put(CommonConstants.WIDGET_NAME, "Child1"))
|
||||
.put(new JSONObject().put(CommonConstants.WIDGET_NAME, "Child2"));
|
||||
|
||||
Assertions.assertEquals(expectedChildren.toString(), result.optJSONArray(CommonConstants.CHILDREN).toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAppendChildren_WithExistingMultipleChildren() {
|
||||
JSONObject parent = new JSONObject();
|
||||
JSONArray existingChildren = new JSONArray()
|
||||
.put(new JSONObject().put(CommonConstants.WIDGET_NAME, "ExistingChild1"))
|
||||
.put(new JSONObject().put(CommonConstants.WIDGET_NAME, "ExistingChild2"));
|
||||
parent.put(CommonConstants.CHILDREN, existingChildren);
|
||||
JSONArray childWidgets = new JSONArray()
|
||||
.put(new JSONObject().put(CommonConstants.WIDGET_NAME, "Child1"))
|
||||
.put(new JSONObject().put(CommonConstants.WIDGET_NAME, "Child2"));
|
||||
|
||||
JSONObject result = DSLTransformerHelper.appendChildren(parent, childWidgets);
|
||||
|
||||
JSONArray expectedChildren = new JSONArray()
|
||||
.put(new JSONObject().put(CommonConstants.WIDGET_NAME, "Child1"))
|
||||
.put(new JSONObject().put(CommonConstants.WIDGET_NAME, "Child2"));
|
||||
|
||||
Assertions.assertEquals(expectedChildren.toString(), result.optJSONArray(CommonConstants.CHILDREN).toString());
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import org.junit.jupiter.api.BeforeEach;
|
|||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
|
|
|||
|
|
@ -51,4 +51,7 @@ public class GitStatusDTO {
|
|||
|
||||
// Documentation url for discard and pull functionality
|
||||
String discardDocUrl = Assets.GIT_DISCARD_DOC_URL;
|
||||
|
||||
// File Format migration
|
||||
String migrationMessage = "";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ public class ApplicationGitReference {
|
|||
Map<String, Object> actionCollections;
|
||||
Map<String, String> actionCollectionBody;
|
||||
Map<String, Object> pages;
|
||||
Map<String, String> pageDsl;
|
||||
Map<String, Object> datasources;
|
||||
Map<String, Object> jsLibraries;
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ import com.appsmith.server.services.SessionUserService;
|
|||
import com.google.gson.Gson;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.minidev.json.JSONObject;
|
||||
import net.minidev.json.parser.JSONParser;
|
||||
import net.minidev.json.parser.ParseException;
|
||||
import org.apache.commons.collections.PredicateUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
|
|
@ -156,6 +159,7 @@ public class GitFileUtils {
|
|||
// Insert only active pages which will then be committed to repo as individual file
|
||||
Map<String, Object> resourceMap = new HashMap<>();
|
||||
Map<String, String> resourceMapBody = new HashMap<>();
|
||||
Map<String, String> dslBody = new HashMap<>();
|
||||
applicationJson
|
||||
.getPageList()
|
||||
.stream()
|
||||
|
|
@ -168,12 +172,20 @@ public class GitFileUtils {
|
|||
? newPage.getUnpublishedPage().getName()
|
||||
: newPage.getPublishedPage().getName();
|
||||
removeUnwantedFieldsFromPage(newPage);
|
||||
JSONObject dsl = newPage.getUnpublishedPage().getLayouts().get(0).getDsl();
|
||||
// Get MainContainer widget data, remove the children and club with Canvas.json file
|
||||
JSONObject mainContainer = new JSONObject(dsl);
|
||||
mainContainer.remove("children");
|
||||
newPage.getUnpublishedPage().getLayouts().get(0).setDsl(mainContainer);
|
||||
// pageName will be used for naming the json file
|
||||
dslBody.put(pageName, dsl.toString());
|
||||
resourceMap.put(pageName, newPage);
|
||||
});
|
||||
|
||||
applicationReference.setPages(new HashMap<>(resourceMap));
|
||||
applicationReference.setPageDsl(new HashMap<>(dslBody));
|
||||
resourceMap.clear();
|
||||
resourceMapBody.clear();
|
||||
|
||||
// Insert active actions and also assign the keys which later will be used for saving the resource in actual filepath
|
||||
// For actions, we are referring to validNames to maintain unique file names as just name
|
||||
|
|
@ -403,6 +415,19 @@ public class GitFileUtils {
|
|||
List<NewPage> pages = getApplicationResource(applicationReference.getPages(), NewPage.class);
|
||||
// Remove null values
|
||||
org.apache.commons.collections.CollectionUtils.filter(pages, PredicateUtils.notNullPredicate());
|
||||
// Set the DSL to page object before saving
|
||||
Map<String, String> pageDsl = applicationReference.getPageDsl();
|
||||
pages.forEach(page -> {
|
||||
JSONParser jsonParser = new JSONParser();
|
||||
try {
|
||||
if (pageDsl != null && pageDsl.get(page.getUnpublishedPage().getName()) != null) {
|
||||
page.getUnpublishedPage().getLayouts().get(0).setDsl( (JSONObject) jsonParser.parse(pageDsl.get(page.getUnpublishedPage().getName())) );
|
||||
}
|
||||
} catch (ParseException e) {
|
||||
log.error("Error parsing the page dsl for page: {}", page.getUnpublishedPage().getName(), e);
|
||||
throw new AppsmithException(AppsmithError.JSON_PROCESSING_ERROR, page.getUnpublishedPage().getName());
|
||||
}
|
||||
});
|
||||
pages.forEach(newPage -> {
|
||||
// As we are publishing the app and then committing to git we expect the published and unpublished PageDTO
|
||||
// will be same, so we create a deep copy for the published version for page from the unpublishedPageDTO
|
||||
|
|
@ -426,7 +451,7 @@ public class GitFileUtils {
|
|||
// For REMOTE plugin like Twilio the user actions are stored in key value pairs and hence they need to be
|
||||
// deserialized separately unlike the body which is stored as string in the db.
|
||||
if (newAction.getPluginType().toString().equals("REMOTE")) {
|
||||
Map<String, Object> formData = new Gson().fromJson(actionBody.get(keyName), Map.class);
|
||||
Map<String, Object> formData = gson.fromJson(actionBody.get(keyName), Map.class);
|
||||
newAction.getUnpublishedAction().getActionConfiguration().setFormData(formData);
|
||||
} else {
|
||||
newAction.getUnpublishedAction().getActionConfiguration().setBody(actionBody.get(keyName));
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user