From 204a187bc28d2e6aaaefb4e531d21fc5c80cffe3 Mon Sep 17 00:00:00 2001 From: Nidhi Date: Wed, 26 Oct 2022 20:23:06 +0530 Subject: [PATCH] fix: Refactor entities with specific rules (#17523) * Refactor changes for DSL * Spaces * Action and collection refactor logic * Changes to some logic for DSL * Fixed tests, added dynamic trigger path list logic as well * Added test for dynamicTriggerList condition * added analytics data to response in ast * Fix for peer closed connection on AST * Added comments for clarity * Added logs for time taken by AST call * handle export default and update success param accordingly * updates for review comments Co-authored-by: ChandanBalajiBP --- app/rts/src/controllers/Ast/AstController.ts | 6 +- app/rts/src/controllers/BaseController.ts | 12 + app/rts/src/middlewares/rules/ast.ts | 15 + app/rts/src/routes/ast_routes.ts | 2 +- app/rts/src/services/AstService.ts | 2 + app/rts/src/test/server.test.ts | 52 +- .../com/appsmith/util/WebClientUtils.java | 16 + .../appsmith/server/constants/FieldName.java | 2 + .../com/appsmith/server/helpers/DslUtils.java | 107 +++++ .../server/services/ce/AstServiceCE.java | 3 + .../server/services/ce/AstServiceCEImpl.java | 91 +++- .../solutions/RefactoringSolutionImpl.java | 13 +- .../ce/RefactoringSolutionCEImpl.java | 450 ++++++++++++++---- .../services/ActionCollectionServiceTest.java | 10 +- .../ce/RefactoringSolutionCEImplTest.java | 135 ++++++ .../ce/RefactoringSolutionCETest.java | 37 +- .../ce/refactorDslWithOnlyWidgets.json | 374 +++++++++++++++ ...refactorDslWithOnlyWidgetsWithNewList.json | 374 +++++++++++++++ ...refactorDslWithOnlyWidgetsWithNewText.json | 374 +++++++++++++++ app/shared/ast/src/index.ts | 37 +- app/shared/ast/src/jsObject/index.ts | 19 +- 21 files changed, 2000 insertions(+), 131 deletions(-) create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/DslUtils.java create mode 100644 app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ce/RefactoringSolutionCEImplTest.java create mode 100644 app/server/appsmith-server/src/test/resources/com/appsmith/server/solutions/ce/refactorDslWithOnlyWidgets.json create mode 100644 app/server/appsmith-server/src/test/resources/com/appsmith/server/solutions/ce/refactorDslWithOnlyWidgetsWithNewList.json create mode 100644 app/server/appsmith-server/src/test/resources/com/appsmith/server/solutions/ce/refactorDslWithOnlyWidgetsWithNewText.json diff --git a/app/rts/src/controllers/Ast/AstController.ts b/app/rts/src/controllers/Ast/AstController.ts index 54505f401e..935892ffde 100644 --- a/app/rts/src/controllers/Ast/AstController.ts +++ b/app/rts/src/controllers/Ast/AstController.ts @@ -13,6 +13,7 @@ type entityRefactorType = { script: string; oldName: string; newName: string; + isJSObject: boolean; evalVersion?: number; }; @@ -74,15 +75,16 @@ export default class AstController extends BaseController { async entityRefactorController(req: Request, res: Response) { try { // By default the application eval version is set to be 2 - const { script, oldName, newName, evalVersion }: entityRefactorType = + const { script, oldName, newName, isJSObject, evalVersion }: entityRefactorType = req.body; const data = await AstService.entityRefactor( script, oldName, newName, + isJSObject, evalVersion ); - return super.sendResponse(res, data); + return super.sendEntityResponse(res, data.body, data.isSuccess); } catch (err) { return super.sendError( res, diff --git a/app/rts/src/controllers/BaseController.ts b/app/rts/src/controllers/BaseController.ts index f01904b0f8..ff8a153710 100644 --- a/app/rts/src/controllers/BaseController.ts +++ b/app/rts/src/controllers/BaseController.ts @@ -35,6 +35,18 @@ export default class BaseController { }); } + sendEntityResponse( + response: Response, + result?: unknown, + success?: boolean, + code: number = StatusCodes.OK + ): Response { + return response.status(code).json({ + success, + data: result, + }); + } + sendError( response: Response, error: string, diff --git a/app/rts/src/middlewares/rules/ast.ts b/app/rts/src/middlewares/rules/ast.ts index 8572727738..c557edff6d 100644 --- a/app/rts/src/middlewares/rules/ast.ts +++ b/app/rts/src/middlewares/rules/ast.ts @@ -12,4 +12,19 @@ export default class AstValidator { min: 1, }) .withMessage("Multiple scripts are required"); + + static getEntityRefactorValidator = () => [ + body("script") + .isString() + .withMessage("Script is required and can only be a string"), + body("oldName") + .isString() + .withMessage("OldName is required and can only be a string"), + body("newName") + .isString() + .withMessage("NewName is required and can only be a string"), + body("isJSObject") + .isBoolean() + .withMessage("isJSObject is required and can only be a boolean"), + ]; } diff --git a/app/rts/src/routes/ast_routes.ts b/app/rts/src/routes/ast_routes.ts index 26ebc6502c..046267b18c 100644 --- a/app/rts/src/routes/ast_routes.ts +++ b/app/rts/src/routes/ast_routes.ts @@ -22,7 +22,7 @@ router.post( ); router.post( "/entity-refactor", - AstRules.getScriptValidator(), + AstRules.getEntityRefactorValidator(), validator.validateRequest, astController.entityRefactorController ); diff --git a/app/rts/src/services/AstService.ts b/app/rts/src/services/AstService.ts index ef88076069..f145741c92 100644 --- a/app/rts/src/services/AstService.ts +++ b/app/rts/src/services/AstService.ts @@ -25,6 +25,7 @@ export default class AstService { script, oldName, newName, + isJSObject, evalVersion ): Promise { return new Promise((resolve, reject) => { @@ -33,6 +34,7 @@ export default class AstService { script, oldName, newName, + isJSObject, evalVersion ); diff --git a/app/rts/src/test/server.test.ts b/app/rts/src/test/server.test.ts index 0bd3b38f0c..473a06a567 100644 --- a/app/rts/src/test/server.test.ts +++ b/app/rts/src/test/server.test.ts @@ -18,23 +18,37 @@ const entityRefactor = [ script: "ApiNever", oldName: "ApiNever", newName: "ApiForever", + isJSObject: false, + evalVersion: 2, }, { script: "ApiNever.data", oldName: "ApiNever", newName: "ApiForever", + isJSObject: false, }, { script: "// ApiNever \n function ApiNever(abc) {let foo = \"I'm getting data from ApiNever but don't rename this string\" + ApiNever.data; \n if(true) { return ApiNever }}", oldName: "ApiNever", newName: "ApiForever", + isJSObject: false, + evalVersion: 2, }, { script: "//ApiNever \n function ApiNever(abc) {let ApiNever = \"I'm getting data from ApiNever but don't rename this string\" + ApiNever.data; \n if(true) { return ApiNever }}", oldName: "ApiNever", newName: "ApiForever", + isJSObject: false, + }, + { + script: + "export default {\n\tmyVar1: [],\n\tmyVar2: {},\n\t\tsearch: () => {\n\t\tif(Input1Copy.text.length==0){\n\t\t\treturn select_repair_db.data\n\t\t}\n\t\telse{\n\t\t\treturn(select_repair_db.data.filter(word => word.cust_name.toLowerCase().includes(Input1Copy.text.toLowerCase())))\n\t\t}\n\t},\n}", + oldName: "Input1Copy", + newName: "Input1", + isJSObject: true, + evalVersion: 2, }, ]; @@ -93,17 +107,22 @@ describe("AST tests", () => { entityRefactor.forEach(async (input, index) => { it(`Entity refactor test case ${index + 1}`, async () => { const expectedResponse = [ - { script: "ApiForever", count: 1 }, - { script: "ApiForever.data", count: 1 }, + { script: "ApiForever", refactorCount: 1 }, + { script: "ApiForever.data", refactorCount: 1 }, { script: "// ApiNever \n function ApiNever(abc) {let foo = \"I'm getting data from ApiNever but don't rename this string\" + ApiForever.data; \n if(true) { return ApiForever }}", - count: 2, + refactorCount: 2, }, { script: "//ApiNever \n function ApiNever(abc) {let ApiNever = \"I'm getting data from ApiNever but don't rename this string\" + ApiNever.data; \n if(true) { return ApiNever }}", - count: 0, + refactorCount: 0, + }, + { + script: + "export default {\n\tmyVar1: [],\n\tmyVar2: {},\n\t\tsearch: () => {\n\t\tif(Input1.text.length==0){\n\t\t\treturn select_repair_db.data\n\t\t}\n\t\telse{\n\t\t\treturn(select_repair_db.data.filter(word => word.cust_name.toLowerCase().includes(Input1.text.toLowerCase())))\n\t\t}\n\t},\n}", + refactorCount: 2, }, ]; @@ -118,10 +137,31 @@ describe("AST tests", () => { expect(response.body.data.script).toEqual( expectedResponse[index].script ); - expect(response.body.data.count).toEqual( - expectedResponse[index].count + expect(response.body.data.refactorCount).toEqual( + expectedResponse[index].refactorCount ); }); }); }); + + it("Entity refactor syntax error", async () => { + let request = { + script: "ApiNever++++", + oldName: "ApiNever", + newName: "ApiForever", + isJSObject: true, + evalVersion: 2, + }; + + await supertest(app) + .post(`${RTS_BASE_API_PATH}/ast/entity-refactor`, { + JSON: true, + }) + .send(request) + .expect(200) + .then((response) => { + expect(response.body.success).toEqual(false); + expect(response.body.data.error).toEqual("Syntax Error"); + }); + }); }); diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/util/WebClientUtils.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/util/WebClientUtils.java index ae0acfd498..9046d4ee6c 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/util/WebClientUtils.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/util/WebClientUtils.java @@ -11,6 +11,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.web.reactive.function.client.WebClient; import reactor.netty.http.client.HttpClient; +import reactor.netty.resources.ConnectionProvider; import java.net.InetAddress; import java.net.InetSocketAddress; @@ -35,12 +36,23 @@ public class WebClientUtils { .build(); } + public static WebClient create(ConnectionProvider provider) { + return builder(provider) + .build(); + } + public static WebClient create(String baseUrl) { return builder() .baseUrl(baseUrl) .build(); } + public static WebClient create(String baseUrl, ConnectionProvider provider) { + return builder(provider) + .baseUrl(baseUrl) + .build(); + } + private static boolean shouldUseSystemProxy() { return "true".equals(System.getProperty("java.net.useSystemProxies")) && (!System.getProperty("http.proxyHost", "").isEmpty() || !System.getProperty("https.proxyHost", "").isEmpty()); @@ -50,6 +62,10 @@ public class WebClientUtils { return builder(HttpClient.create()); } + public static WebClient.Builder builder(ConnectionProvider provider) { + return builder(HttpClient.create(provider)); + } + public static WebClient.Builder builder(HttpClient httpClient) { return WebClient.builder() .clientConnector(new ReactorClientHttpConnector(makeSafeHttpClient(httpClient))); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/FieldName.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/FieldName.java index 88df1229b8..9334322171 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/FieldName.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/FieldName.java @@ -78,6 +78,8 @@ public class FieldName { public static String PUBLISHED_APPLICATION = "deployed application"; public static final String TOKEN = "token"; public static String WIDGET_TYPE = "type"; + public static String LIST_WIDGET_TEMPLATE = "template"; + public static String LIST_WIDGET = "LIST_WIDGET"; public static String TABLE_WIDGET = "TABLE_WIDGET"; public static String CONTAINER_WIDGET = "CONTAINER_WIDGET"; public static String CANVAS_WIDGET = "CANVAS_WIDGET"; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/DslUtils.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/DslUtils.java new file mode 100644 index 0000000000..f9ff98f59e --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/DslUtils.java @@ -0,0 +1,107 @@ +package com.appsmith.server.helpers; + +import com.appsmith.external.helpers.MustacheHelper; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import lombok.AllArgsConstructor; +import org.apache.commons.lang3.StringUtils; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +public class DslUtils { + + public static Set getMustacheValueSetFromSpecificDynamicBindingPath(JsonNode dsl, String fieldPath) { + + DslNodeWalkResponse dslWalkResponse = getDslWalkResponse(dsl, fieldPath); + + + // Only extract mustache keys from leaf nodes + if (dslWalkResponse != null && dslWalkResponse.isLeafNode) { + + // We found the path. But if the path does not have any mustache bindings, return with empty set + if (!MustacheHelper.laxIsBindingPresentInString(((TextNode) dslWalkResponse.currentNode).asText())) { + return new HashSet<>(); + } + + // Stricter extraction of dynamic bindings + Set mustacheKeysFromFields = MustacheHelper.extractMustacheKeysFromFields(((TextNode) dslWalkResponse.currentNode).asText()); + return mustacheKeysFromFields; + } + + // This was not a text node, we do not know how to handle this + return new HashSet<>(); + } + + public static JsonNode replaceValuesInSpecificDynamicBindingPath(JsonNode dsl, String fieldPath, Map replacementMap) { + DslNodeWalkResponse dslWalkResponse = getDslWalkResponse(dsl, fieldPath); + + if (dslWalkResponse != null && dslWalkResponse.isLeafNode) { + final String oldValue = ((TextNode) dslWalkResponse.currentNode).asText(); + + final String newValue = StringUtils.replaceEach( + oldValue, + replacementMap.keySet().toArray(new String[0]), + replacementMap.values().toArray(new String[0])); + + ((ObjectNode) dslWalkResponse.parentNode).set(dslWalkResponse.currentKey, new TextNode(newValue)); + } + return dsl; + } + + private static DslNodeWalkResponse getDslWalkResponse(JsonNode dsl, String fieldPath) { + String[] fields = fieldPath.split("[].\\[]"); + // For nested fields, the parent dsl to search in would shift by one level every iteration + Object currentNode = dsl; + Object parent = null; + Iterator fieldsIterator = Arrays.stream(fields).filter(fieldToken -> !fieldToken.isBlank()).iterator(); + boolean isLeafNode = false; + String nextKey = null; + // This loop will end at either a leaf node, or the last identified JSON field (by throwing an exception) + // Valid forms of the fieldPath for this search could be: + // root.field.list[index].childField.anotherList.indexWithDotOperator.multidimensionalList[index1][index2] + while (fieldsIterator.hasNext()) { + nextKey = fieldsIterator.next(); + parent = currentNode; + if (currentNode instanceof ArrayNode) { + if (Pattern.matches(Pattern.compile("[0-9]+").toString(), nextKey)) { + try { + currentNode = ((ArrayNode) currentNode).get(Integer.parseInt(nextKey)); + } catch (IndexOutOfBoundsException e) { + // The index being referred does not exist, hence the path would not exist. + return null; + } + } else { + // This is an array but the fieldPath does not have an index to refer to + return null; + } + } else { + currentNode = ((JsonNode) currentNode).get(nextKey); + } + // After updating the currentNode, check for the types + if (currentNode == null) { + return null; + } else if (currentNode instanceof TextNode) { + // If we get String value, then this is a leaf node + isLeafNode = true; + break; + } + } + + return new DslNodeWalkResponse(currentNode, parent, nextKey, isLeafNode); + } + + @AllArgsConstructor + private static class DslNodeWalkResponse { + Object currentNode; + Object parentNode; + String currentKey; + Boolean isLeafNode; + } +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AstServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AstServiceCE.java index 87e8155d98..1480d531bd 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AstServiceCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AstServiceCE.java @@ -2,6 +2,7 @@ package com.appsmith.server.services.ce; import reactor.core.publisher.Mono; +import java.util.Map; import java.util.Set; public interface AstServiceCE { @@ -18,4 +19,6 @@ public interface AstServiceCE { * @return A mono of list of strings that represent all valid global references in the binding string */ Mono> getPossibleReferencesFromDynamicBinding(String bindingValue, int evalVersion); + + Mono> refactorNameInDynamicBindings(Set bindingValues, String oldName, String newName, int evalVersion); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AstServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AstServiceCEImpl.java index ac87bd8fd4..a5cfd72d56 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AstServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AstServiceCEImpl.java @@ -13,10 +13,17 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; import org.springframework.util.StringUtils; import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.netty.resources.ConnectionProvider; +import reactor.util.function.Tuple2; +import java.time.Duration; import java.util.HashSet; +import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; @Slf4j @RequiredArgsConstructor @@ -26,6 +33,15 @@ public class AstServiceCEImpl implements AstServiceCE { private final InstanceConfig instanceConfig; + private final WebClient webClient = WebClientUtils.create(ConnectionProvider.builder("rts-provider") + .maxConnections(500) + .maxIdleTime(Duration.ofSeconds(5)) + .maxLifeTime(Duration.ofSeconds(10)) + .pendingAcquireTimeout(Duration.ofSeconds(10)) + .evictInBackground(Duration.ofSeconds(20)).build()); + + private final static long MAX_API_RESPONSE_TIME = 50; + @Override public Mono> getPossibleReferencesFromDynamicBinding(String bindingValue, int evalVersion) { if (!StringUtils.hasLength(bindingValue)) { @@ -38,16 +54,61 @@ public class AstServiceCEImpl implements AstServiceCE { return Mono.just(new HashSet<>(MustacheHelper.getPossibleParentsOld(bindingValue))); } - return WebClientUtils.create(commonConfig.getRtsBaseDomain() + "/rts-api/v1/ast/single-script-data") + return webClient .post() + .uri(commonConfig.getRtsBaseDomain() + "/rts-api/v1/ast/single-script-data") .contentType(MediaType.APPLICATION_JSON) .body(BodyInserters.fromValue(new GetIdentifiersRequest(bindingValue, evalVersion))) .retrieve() .bodyToMono(GetIdentifiersResponse.class) + .elapsed() + .map(tuple -> { + log.debug("Time elapsed since AST get identifiers call: {} ms", tuple.getT1()); + if (tuple.getT1() > MAX_API_RESPONSE_TIME) { + log.debug("This call took longer than expected. The binding was: {}", bindingValue); + } + return tuple.getT2(); + }) .map(response -> response.data.references); // TODO: add error handling scenario for when RTS is not accessible in fat container } + @Override + public Mono> refactorNameInDynamicBindings(Set bindingValues, String oldName, String newName, int evalVersion) { + if (bindingValues == null || bindingValues.isEmpty()) { + return Mono.empty(); + } + + return Flux.fromIterable(bindingValues) + .flatMap(bindingValue -> { + return webClient + .post() + .uri(commonConfig.getRtsBaseDomain() + "/rts-api/v1/ast/entity-refactor") + .contentType(MediaType.APPLICATION_JSON) + .body(BodyInserters.fromValue(new EntityRefactorRequest(bindingValue, oldName, newName, evalVersion))) + .retrieve() + .bodyToMono(EntityRefactorResponse.class) + .elapsed() + .map(tuple -> { + log.debug("Time elapsed since AST refactor call: {} ms", tuple.getT1()); + if (tuple.getT1() > MAX_API_RESPONSE_TIME) { + log.debug("This call took longer than expected. The binding was: {}", bindingValue); + } + return tuple.getT2(); + }) + .map(EntityRefactorResponse::getData) + .filter(details -> details.refactorCount > 0) + .flatMap(response -> Mono.just(bindingValue).zipWith(Mono.just(response.script))) + .onErrorResume(error -> { + var temp = bindingValue; + // If there is a problem with parsing and refactoring this binding, we just ignore it and move ahead + // The expectation is that this binding would error out during eval anyway + return Mono.empty(); + }); + }) + .collect(Collectors.toMap(Tuple2::getT1, Tuple2::getT2)); + } + @NoArgsConstructor @AllArgsConstructor @Getter @@ -90,4 +151,32 @@ public class AstServiceCEImpl implements AstServiceCE { Set functionalParams; Set variables; } + + @NoArgsConstructor + @AllArgsConstructor + @Getter + static class EntityRefactorRequest { + String script; + String oldName; + String newName; + int evalVersion; + } + + @NoArgsConstructor + @AllArgsConstructor + @Getter + @Setter + static class EntityRefactorResponse { + EntityRefactorResponseDetails data; + } + + @NoArgsConstructor + @AllArgsConstructor + @Getter + @Setter + static class EntityRefactorResponseDetails { + String script; + int referenceCount; + int refactorCount; + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/RefactoringSolutionImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/RefactoringSolutionImpl.java index 169cb9c23a..a73942050b 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/RefactoringSolutionImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/RefactoringSolutionImpl.java @@ -1,7 +1,10 @@ package com.appsmith.server.solutions; +import com.appsmith.server.configurations.InstanceConfig; import com.appsmith.server.helpers.ResponseUtils; import com.appsmith.server.services.ActionCollectionService; +import com.appsmith.server.services.ApplicationService; +import com.appsmith.server.services.AstService; import com.appsmith.server.services.LayoutActionService; import com.appsmith.server.services.NewActionService; import com.appsmith.server.services.NewPageService; @@ -19,12 +22,18 @@ public class RefactoringSolutionImpl extends RefactoringSolutionCEImpl implement NewActionService newActionService, ActionCollectionService actionCollectionService, ResponseUtils responseUtils, - LayoutActionService layoutActionService) { + LayoutActionService layoutActionService, + ApplicationService applicationService, + AstService astService, + InstanceConfig instanceConfig) { super(objectMapper, newPageService, newActionService, actionCollectionService, responseUtils, - layoutActionService); + layoutActionService, + applicationService, + astService, + instanceConfig); } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/RefactoringSolutionCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/RefactoringSolutionCEImpl.java index 5fd8bb8c19..71b0476ab0 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/RefactoringSolutionCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/RefactoringSolutionCEImpl.java @@ -2,6 +2,9 @@ package com.appsmith.server.solutions.ce; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionDTO; +import com.appsmith.external.models.PluginType; +import com.appsmith.server.configurations.InstanceConfig; +import com.appsmith.server.constants.FieldName; import com.appsmith.server.domains.Layout; import com.appsmith.server.domains.NewAction; import com.appsmith.server.dtos.ActionCollectionDTO; @@ -11,8 +14,11 @@ import com.appsmith.server.dtos.RefactorActionNameDTO; import com.appsmith.server.dtos.RefactorNameDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; +import com.appsmith.server.helpers.DslUtils; import com.appsmith.server.helpers.ResponseUtils; import com.appsmith.server.services.ActionCollectionService; +import com.appsmith.server.services.ApplicationService; +import com.appsmith.server.services.AstService; import com.appsmith.server.services.LayoutActionService; import com.appsmith.server.services.NewActionService; import com.appsmith.server.services.NewPageService; @@ -21,44 +27,78 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import net.minidev.json.JSONObject; +import org.jetbrains.annotations.NotNull; import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; +import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import static com.appsmith.server.acl.AclPermission.MANAGE_ACTIONS; import static com.appsmith.server.acl.AclPermission.MANAGE_PAGES; +import static com.appsmith.server.services.ce.ApplicationPageServiceCEImpl.EVALUATION_VERSION; import static java.util.stream.Collectors.toSet; @Slf4j -@RequiredArgsConstructor public class RefactoringSolutionCEImpl implements RefactoringSolutionCE { - private final ObjectMapper objectMapper; private final NewPageService newPageService; private final NewActionService newActionService; private final ActionCollectionService actionCollectionService; private final ResponseUtils responseUtils; private final LayoutActionService layoutActionService; + private final ApplicationService applicationService; + private final AstService astService; + private final InstanceConfig instanceConfig; + private final Boolean isRtsAccessible; + + private static final Pattern actionCollectionBodyPattern = Pattern.compile("export default(.*)", Pattern.DOTALL); + private static final String EXPORT_DEFAULT_STRING = "export default"; /* * To replace fetchUsers in `{{JSON.stringify(fetchUsers)}}` with getUsers, the following regex is required : * `\\b(fetchUsers)\\b`. To achieve this the following strings preWord and postWord are declared here to be used * at run time to create the regex pattern. */ - private final String preWord = "\\b("; - private final String postWord = ")\\b"; + private static final String preWord = "\\b("; + private static final String postWord = ")\\b"; + public RefactoringSolutionCEImpl(ObjectMapper objectMapper, + NewPageService newPageService, + NewActionService newActionService, + ActionCollectionService actionCollectionService, + ResponseUtils responseUtils, + LayoutActionService layoutActionService, + ApplicationService applicationService, + AstService astService, + InstanceConfig instanceConfig) { + this.objectMapper = objectMapper; + this.newPageService = newPageService; + this.newActionService = newActionService; + this.actionCollectionService = actionCollectionService; + this.responseUtils = responseUtils; + this.layoutActionService = layoutActionService; + this.applicationService = applicationService; + this.astService = astService; + this.instanceConfig = instanceConfig; + + // TODO Remove this variable and access the field directly when RTS API is ready + this.isRtsAccessible = false; + + } @Override public Mono refactorWidgetName(RefactorNameDTO refactorNameDTO) { @@ -77,7 +117,7 @@ public class RefactoringSolutionCEImpl implements RefactoringSolutionCE { @Override public Mono refactorWidgetName(RefactorNameDTO refactorNameDTO, String branchName) { - if (StringUtils.isEmpty(branchName)) { + if (!StringUtils.hasLength(branchName)) { return refactorWidgetName(refactorNameDTO); } @@ -94,13 +134,13 @@ public class RefactoringSolutionCEImpl implements RefactoringSolutionCE { String pageId = refactorActionNameDTO.getPageId(); String layoutId = refactorActionNameDTO.getLayoutId(); String oldName = refactorActionNameDTO.getOldName(); - final String oldFullyQualifiedName = StringUtils.isEmpty(refactorActionNameDTO.getCollectionName()) ? - oldName : - refactorActionNameDTO.getCollectionName() + "." + oldName; + final String oldFullyQualifiedName = StringUtils.hasLength(refactorActionNameDTO.getCollectionName()) ? + refactorActionNameDTO.getCollectionName() + "." + oldName : + oldName; String newName = refactorActionNameDTO.getNewName(); - final String newFullyQualifiedName = StringUtils.isEmpty(refactorActionNameDTO.getCollectionName()) ? - newName : - refactorActionNameDTO.getCollectionName() + "." + newName; + final String newFullyQualifiedName = StringUtils.hasLength(refactorActionNameDTO.getCollectionName()) ? + refactorActionNameDTO.getCollectionName() + "." + newName : + newName; String actionId = refactorActionNameDTO.getActionId(); return Mono.just(newActionService.validateActionName(newName)) .flatMap(isValidName -> { @@ -118,7 +158,7 @@ public class RefactoringSolutionCEImpl implements RefactoringSolutionCE { }) .flatMap(action -> { action.setName(newName); - if (!StringUtils.isEmpty(refactorActionNameDTO.getCollectionName())) { + if (StringUtils.hasLength(refactorActionNameDTO.getCollectionName())) { action.setFullyQualifiedName(newFullyQualifiedName); } return newActionService.updateUnpublishedAction(actionId, action); @@ -139,34 +179,48 @@ public class RefactoringSolutionCEImpl implements RefactoringSolutionCE { .map(responseUtils::updateLayoutDTOWithDefaultResources); } - /** * Assumption here is that the refactoring name provided is indeed unique and is fit to be replaced everywhere. *

* At this point, the user must have MANAGE_PAGES and MANAGE_ACTIONS permissions for page and action respectively * - * @param pageId - * @param layoutId - * @param oldName - * @param newName - * @return + * @param pageId : The page that this entity belongs to + * @param layoutId : The layout to parse through for replacement + * @param oldName : The original name to look for + * @param newName : The new name to refactor all references to + * @return : The DSL after refactor updates */ @Override public Mono refactorName(String pageId, String layoutId, String oldName, String newName) { String regexPattern = preWord + oldName + postWord; Pattern oldNamePattern = Pattern.compile(regexPattern); - Mono updatePageMono = newPageService + Mono pageMono = newPageService // fetch the unpublished page .findPageById(pageId, MANAGE_PAGES, false) + .cache(); + + Mono evalVersionMono = pageMono .flatMap(page -> { + return applicationService.findById(page.getApplicationId()) + .map(application -> { + Integer evaluationVersion = application.getEvaluationVersion(); + if (evaluationVersion == null) { + evaluationVersion = EVALUATION_VERSION; + } + return evaluationVersion; + }); + }) + .cache(); + + Mono updatePageMono = Mono.zip(pageMono, evalVersionMono) + .flatMap(tuple -> { + PageDTO page = tuple.getT1(); + int evalVersion = tuple.getT2(); + List layouts = page.getLayouts(); for (Layout layout : layouts) { if (layoutId.equals(layout.getId()) && layout.getDsl() != null) { - final JsonNode dslNode = objectMapper.convertValue(layout.getDsl(), JsonNode.class); - final JsonNode dslNodeAfterReplacement = this.replaceStringInJsonNode(dslNode, oldNamePattern, newName); - layout.setDsl(objectMapper.convertValue(dslNodeAfterReplacement, JSONObject.class)); - // DSL has removed all the old names and replaced it with new name. If the change of name // was one of the mongoEscaped widgets, then update the names in the set as well Set mongoEscapedWidgetNames = layout.getMongoEscapedWidgetNames(); @@ -174,9 +228,18 @@ public class RefactoringSolutionCEImpl implements RefactoringSolutionCE { mongoEscapedWidgetNames.remove(oldName); mongoEscapedWidgetNames.add(newName); } - page.setLayouts(layouts); + + final JsonNode dslNode = objectMapper.convertValue(layout.getDsl(), JsonNode.class); + Mono refactorNameInDslMono = this.refactorNameInDsl(dslNode, oldName, newName, evalVersion, oldNamePattern) + .then(Mono.fromCallable(() -> { + layout.setDsl(objectMapper.convertValue(dslNode, JSONObject.class)); + page.setLayouts(layouts); + return page; + })); + // Since the page has most probably changed, save the page and return. - return newPageService.saveUnpublishedPage(page); + return refactorNameInDslMono + .flatMap(newPageService::saveUnpublishedPage); } } // If we have reached here, the layout was not found and the page should be returned as is. @@ -187,62 +250,76 @@ public class RefactoringSolutionCEImpl implements RefactoringSolutionCE { Mono> updateActionsMono = newActionService .findByPageIdAndViewMode(pageId, false, MANAGE_ACTIONS) + .flatMap(newAction -> Mono.just(newAction).zipWith(evalVersionMono)) /* * Assuming that the datasource should not be dependent on the widget and hence not going through the same * to look for replacement pattern. */ - .flatMap(newAction1 -> { - final NewAction newAction = newAction1; + .flatMap(tuple -> { + final NewAction newAction = tuple.getT1(); + final Integer evalVersion = tuple.getT2(); // We need actionDTO to be populated with pluginType from NewAction // so that we can check for the JS path Mono actionMono = newActionService.generateActionByViewMode(newAction, false); return actionMono.flatMap(action -> { - newAction.setUnpublishedAction(action); - boolean actionUpdateRequired = false; - ActionConfiguration actionConfiguration = action.getActionConfiguration(); - Set jsonPathKeys = action.getJsonPathKeys(); - - if (jsonPathKeys != null && !jsonPathKeys.isEmpty()) { - // Since json path keys actually contain the entire inline js function instead of just the widget/action - // name, we can not simply use the set.contains(obj) function. We need to iterate over all the keys - // in the set and see if the old name is a substring of the json path key. - for (String key : jsonPathKeys) { - if (oldNamePattern.matcher(key).find()) { - actionUpdateRequired = true; - break; - } - } - } - - if (!actionUpdateRequired || actionConfiguration == null) { + if (action.getActionConfiguration() == null) { return Mono.just(newAction); } - // if actionUpdateRequired is true AND actionConfiguration is not null - if (action.getCollectionId() != null) { + // If this is a JS function rename, add this collection for rename + // because the action configuration won't tell us this + if (StringUtils.hasLength(action.getCollectionId()) && newName.equals(action.getValidName())) { updatableCollectionIds.add(action.getCollectionId()); } - final JsonNode actionConfigurationNode = objectMapper.convertValue(actionConfiguration, JsonNode.class); - final JsonNode actionConfigurationNodeAfterReplacement = replaceStringInJsonNode(actionConfigurationNode, oldNamePattern, newName); - - ActionConfiguration newActionConfiguration = objectMapper.convertValue(actionConfigurationNodeAfterReplacement, ActionConfiguration.class); - action.setActionConfiguration(newActionConfiguration); - NewAction newAction2 = newActionService.extractAndSetJsonPathKeys(newAction); - return newActionService.save(newAction2); + newAction.setUnpublishedAction(action); + return this.refactorNameInAction(action, oldName, newName, evalVersion, oldNamePattern) + .flatMap(updates -> { + if (updates.isEmpty()) { + return Mono.just(newAction); + } + if (StringUtils.hasLength(action.getCollectionId())) { + updatableCollectionIds.add(action.getCollectionId()); + } + newActionService.extractAndSetJsonPathKeys(newAction); + return newActionService.save(newAction); + }); }); }) .map(savedAction -> savedAction.getUnpublishedAction().getName()) .collect(toSet()) - .flatMap(updatedActions -> { + .zipWith(evalVersionMono) + .flatMap(tuple -> { + Set updatedActions = tuple.getT1(); + Integer evalVersion = tuple.getT2(); // If these actions belonged to collections, update the collection body return Flux.fromIterable(updatableCollectionIds) .flatMap(collectionId -> actionCollectionService.findById(collectionId, MANAGE_ACTIONS)) .flatMap(actionCollection -> { final ActionCollectionDTO unpublishedCollection = actionCollection.getUnpublishedCollection(); - Matcher matcher = oldNamePattern.matcher(unpublishedCollection.getBody()); - String newBodyAsString = matcher.replaceAll(newName); - unpublishedCollection.setBody(newBodyAsString); - return actionCollectionService.save(actionCollection); + + Matcher matcher = actionCollectionBodyPattern.matcher(unpublishedCollection.getBody()); + if (matcher.find()) { + String parsableBody = matcher.group(1); + return this.replaceValueInMustacheKeys( + new HashSet<>(Collections.singletonList(parsableBody)), + oldName, + newName, + evalVersion, + oldNamePattern) + .flatMap(replacedMap -> { + Optional replacedValue = replacedMap.values().stream().findFirst(); + // This value should always be there + if (replacedValue.isPresent()) { + final String replacedBody = EXPORT_DEFAULT_STRING + replacedValue.get(); + unpublishedCollection.setBody(replacedBody); + return actionCollectionService.save(actionCollection); + } + return Mono.just(actionCollection); + }); + } else { + // TODO make this error more informative, users should never edit JS objects to this state + return Mono.error(new AppsmithException(AppsmithError.INTERNAL_SERVER_ERROR)); + } }) .collectList() .thenReturn(updatedActions); @@ -264,59 +341,248 @@ public class RefactoringSolutionCEImpl implements RefactoringSolutionCE { }); } + Mono> refactorNameInDsl(JsonNode dsl, String oldName, String newName, int evalVersion, Pattern oldNamePattern) { - private JsonNode replaceStringInJsonNode(JsonNode jsonNode, Pattern oldNamePattern, String newName) { - // If this is a text node, perform replacement directly - if (jsonNode.isTextual()) { - Matcher matcher = oldNamePattern.matcher(jsonNode.asText()); - String valueAfterReplacement = matcher.replaceAll(newName); - return new TextNode(valueAfterReplacement); + Mono> refactorNameInWidgetMono = Mono.just(new HashSet<>()); + Mono> recursiveRefactorNameInDslMono = Mono.just(new HashSet<>()); + + // if current object is widget, + if (dsl.has(FieldName.WIDGET_ID)) { + // enter parse widget method + refactorNameInWidgetMono = refactorNameInWidget(dsl, oldName, newName, evalVersion, oldNamePattern); + } + // if current object has children, + if (dsl.has("children")) { + ArrayNode dslChildren = (ArrayNode) dsl.get("children"); + // recurse over each child + recursiveRefactorNameInDslMono = Flux.fromStream(StreamSupport.stream(dslChildren.spliterator(), true)) + .flatMap(child -> refactorNameInDsl(child, oldName, newName, evalVersion, oldNamePattern)) + .reduce(new HashSet<>(), (x, y) -> { + // for each child, aggregate the refactored paths + y.addAll(x); + return y; + }); } - // TODO This is special handling for the list widget that has been added to allow refactoring of - // just the default widgets inside the list. This is required because for the list, the widget names - // exist as keys at the location List1.template(.Text1) [Ref #9281] - // Ideally, we should avoid any non-structural elements as keys. This will be improved in list widget v2 - if (jsonNode.has("type") && "LIST_WIDGET".equals(jsonNode.get("type").asText())) { - final JsonNode template = jsonNode.get("template"); + return refactorNameInWidgetMono + .zipWith(recursiveRefactorNameInDslMono) + .map(tuple -> { + tuple.getT1().addAll(tuple.getT2()); + return tuple.getT1(); + }); + } + + Mono> refactorNameInWidget(JsonNode widgetDsl, String oldName, String newName, int evalVersion, Pattern oldNamePattern) { + boolean isRefactoredWidget = false; + boolean isRefactoredTemplate = false; + String widgetName = ""; + // If the name of this widget matches the old name, replace the name + if (widgetDsl.has(FieldName.WIDGET_NAME)) { + widgetName = widgetDsl.get(FieldName.WIDGET_NAME).asText(); + if (oldName.equals(widgetName)) { + ((ObjectNode) widgetDsl).set(FieldName.WIDGET_NAME, new TextNode(newName)); + // We mark this widget name as being a path that was refactored using this boolean value + isRefactoredWidget = true; + } + } + + // This is special handling for the list widget that has been added to allow refactoring of + // just the default widgets inside the list. This is required because for the list, the widget names + // exist as keys at the location List1.template(.Text1) [Ref #9281] + // Ideally, we should avoid any non-structural elements as keys. This will be improved in list widget v2 + if (widgetDsl.has(FieldName.WIDGET_TYPE) && FieldName.LIST_WIDGET.equals(widgetDsl.get(FieldName.WIDGET_TYPE).asText())) { + final JsonNode template = widgetDsl.get(FieldName.LIST_WIDGET_TEMPLATE); JsonNode newJsonNode = null; String fieldName = null; final Iterator templateIterator = template.fieldNames(); while (templateIterator.hasNext()) { fieldName = templateIterator.next(); - // For each element within template, check whether it would match the replacement pattern - final Matcher listWidgetTemplateKeyMatcher = oldNamePattern.matcher(fieldName); - if (listWidgetTemplateKeyMatcher.find()) { + if (oldName.equals(fieldName)) { newJsonNode = template.get(fieldName); break; } } if (newJsonNode != null) { + // If we are here, it means that the widget being refactored was from a list widget template + // Go ahead and refactor this template as well + ((ObjectNode) newJsonNode).set(FieldName.WIDGET_NAME, new TextNode(newName)); // If such a pattern is found, remove that element and attach it back with the new name ((ObjectNode) template).remove(fieldName); ((ObjectNode) template).set(newName, newJsonNode); + // We mark this template path as being a path that was refactored using this boolean value + isRefactoredTemplate = true; } } - final Iterator> iterator = jsonNode.fields(); - // Go through each field to recursively operate on it - while (iterator.hasNext()) { - final Map.Entry next = iterator.next(); - final JsonNode value = next.getValue(); - if (value.isArray()) { - // If this field is an array type, iterate through each element and perform replacement - final ArrayNode arrayNode = (ArrayNode) value; - final ArrayNode newArrayNode = objectMapper.createArrayNode(); - arrayNode.forEach(x -> newArrayNode.add(replaceStringInJsonNode(x, oldNamePattern, newName))); - // Make this array node created from replaced values the new value - next.setValue(newArrayNode); - } else { - // This is either directly a text node or another json node - // In either case, recurse over the entire value to get the replaced value - next.setValue(replaceStringInJsonNode(value, oldNamePattern, newName)); + Mono> refactorDynamicBindingsMono = Mono.just(new HashSet<>()); + Mono> refactorTriggerBindingsMono = Mono.just(new HashSet<>()); + + // If there are dynamic bindings in this action configuration, inspect them + if (widgetDsl.has(FieldName.DYNAMIC_BINDING_PATH_LIST) && !widgetDsl.get(FieldName.DYNAMIC_BINDING_PATH_LIST).isEmpty()) { + ArrayNode dslDynamicBindingPathList = (ArrayNode) widgetDsl.get(FieldName.DYNAMIC_BINDING_PATH_LIST); + // recurse over each child + refactorDynamicBindingsMono = refactorBindingsUsingBindingPaths( + widgetDsl, + oldName, + newName, + evalVersion, + oldNamePattern, + dslDynamicBindingPathList, + widgetName); + } + + // If there are dynamic triggers in this action configuration, inspect them + if (widgetDsl.has(FieldName.DYNAMIC_TRIGGER_PATH_LIST) && !widgetDsl.get(FieldName.DYNAMIC_TRIGGER_PATH_LIST).isEmpty()) { + ArrayNode dslDynamicTriggerPathList = (ArrayNode) widgetDsl.get(FieldName.DYNAMIC_TRIGGER_PATH_LIST); + // recurse over each child + refactorTriggerBindingsMono = refactorBindingsUsingBindingPaths( + widgetDsl, + oldName, + newName, + evalVersion, + oldNamePattern, + dslDynamicTriggerPathList, + widgetName); + } + + final String finalWidgetNamePath = widgetName + ".widgetName"; + final boolean finalIsRefactoredWidget = isRefactoredWidget; + final boolean finalIsRefactoredTemplate = isRefactoredTemplate; + final String finalWidgetTemplatePath = widgetName + ".template"; + return refactorDynamicBindingsMono.zipWith(refactorTriggerBindingsMono) + .map(tuple -> { + tuple.getT1().addAll(tuple.getT2()); + return tuple.getT1(); + }) + .map(refactoredBindings -> { + if (Boolean.TRUE.equals(finalIsRefactoredWidget)) { + refactoredBindings.add(finalWidgetNamePath); + } + if (Boolean.TRUE.equals(finalIsRefactoredTemplate)) { + refactoredBindings.add(finalWidgetTemplatePath); + } + return refactoredBindings; + }); + } + + @NotNull + private Mono> refactorBindingsUsingBindingPaths(JsonNode widgetDsl, String oldName, String newName, int evalVersion, Pattern oldNamePattern, ArrayNode bindingPathList, String widgetName) { + Mono> refactorBindingsMono; + refactorBindingsMono = Flux.fromStream(StreamSupport.stream(bindingPathList.spliterator(), true)) + .flatMap(bindingPath -> { + String key = bindingPath.get(FieldName.KEY).asText(); + // This is inside a list widget, and the path starts with template., + // We need to update the binding path list entry itself as well + if (widgetDsl.has(FieldName.WIDGET_TYPE) && + FieldName.LIST_WIDGET.equals(widgetDsl.get(FieldName.WIDGET_TYPE).asText()) && + key.startsWith("template." + oldName)) { + key = key.replace(oldName, newName); + ((ObjectNode) bindingPath).set(FieldName.KEY, new TextNode(key)); + } + // Find values inside mustache bindings in this path + Set mustacheValues = DslUtils.getMustacheValueSetFromSpecificDynamicBindingPath(widgetDsl, key); + final String finalKey = key; + // Perform refactor for each mustache value + return this.replaceValueInMustacheKeys(mustacheValues, oldName, newName, evalVersion, oldNamePattern) + .flatMap(replacementMap -> { + if (replacementMap.isEmpty()) { + // If the map is empty, it means that this path did not have anything that had to be refactored + return Mono.empty(); + } + // Replace the binding path value with the new mustache values + DslUtils.replaceValuesInSpecificDynamicBindingPath(widgetDsl, finalKey, replacementMap); + // Mark this path as refactored + String entityPath = StringUtils.hasLength(widgetName) ? widgetName + "." : ""; + return Mono.just(entityPath + finalKey); + }); + }) + .collect(Collectors.toSet()); + return refactorBindingsMono; + } + + Mono> refactorNameInAction(ActionDTO actionDTO, String oldName, String newName, + int evalVersion, Pattern oldNamePattern) { + // If we're going the fallback route (without AST), we can first filter actions to be refactored + // By performing a check on whether json path keys had a reference + // This is not needed in the AST way since it would be costlier to make double the number of API calls + if (Boolean.FALSE.equals(this.isRtsAccessible)) { + Set jsonPathKeys = actionDTO.getJsonPathKeys(); + + boolean isReferenceFound = false; + if (jsonPathKeys != null && !jsonPathKeys.isEmpty()) { + // Since json path keys actually contain the entire inline js function instead of just the widget/action + // name, we can not simply use the set.contains(obj) function. We need to iterate over all the keys + // in the set and see if the old name is a substring of the json path key. + for (String key : jsonPathKeys) { + if (oldNamePattern.matcher(key).find()) { + isReferenceFound = true; + break; + } + } + } + // If no reference was found, return with an empty set + if (Boolean.FALSE.equals(isReferenceFound)) { + return Mono.just(new HashSet<>()); } } - return jsonNode; + + ActionConfiguration actionConfiguration = actionDTO.getActionConfiguration(); + final JsonNode actionConfigurationNode = objectMapper.convertValue(actionConfiguration, JsonNode.class); + + Mono> refactorDynamicBindingsMono = Mono.just(new HashSet<>()); + + // If there are dynamic bindings in this action configuration, inspect them + if (actionDTO.getDynamicBindingPathList() != null && !actionDTO.getDynamicBindingPathList().isEmpty()) { + // recurse over each child + refactorDynamicBindingsMono = Flux.fromIterable(actionDTO.getDynamicBindingPathList()) + .flatMap(dynamicBindingPath -> { + String key = dynamicBindingPath.getKey(); + Set mustacheValues = new HashSet<>(); + if (PluginType.JS.equals(actionDTO.getPluginType()) && "body".equals(key)) { + mustacheValues.add(actionConfiguration.getBody()); + + } else { + mustacheValues = DslUtils.getMustacheValueSetFromSpecificDynamicBindingPath(actionConfigurationNode, key); + } + return this.replaceValueInMustacheKeys(mustacheValues, oldName, newName, evalVersion, oldNamePattern) + .flatMap(replacementMap -> { + if (replacementMap.isEmpty()) { + return Mono.empty(); + } + DslUtils.replaceValuesInSpecificDynamicBindingPath(actionConfigurationNode, key, replacementMap); + String entityPath = StringUtils.hasLength(actionDTO.getValidName()) ? actionDTO.getValidName() + "." : ""; + return Mono.just(entityPath + key); + }); + }) + .collect(Collectors.toSet()) + .map(entityPaths -> { + actionDTO.setActionConfiguration(objectMapper.convertValue(actionConfigurationNode, ActionConfiguration.class)); + return entityPaths; + }); + } + + return refactorDynamicBindingsMono; + } + + Mono> replaceValueInMustacheKeys(Set mustacheKeySet, String oldName, String + newName, int evalVersion, Pattern oldNamePattern) { + if (Boolean.TRUE.equals(this.isRtsAccessible)) { + return astService.refactorNameInDynamicBindings(mustacheKeySet, oldName, newName, evalVersion); + } + return this.replaceValueInMustacheKeys(mustacheKeySet, oldNamePattern, newName); + } + + Mono> replaceValueInMustacheKeys(Set mustacheKeySet, Pattern + oldNamePattern, String newName) { + return Flux.fromIterable(mustacheKeySet) + .flatMap(mustacheKey -> { + Matcher matcher = oldNamePattern.matcher(mustacheKey); + if (matcher.find()) { + return Mono.zip(Mono.just(mustacheKey), Mono.just(matcher.replaceAll(newName))); + } + return Mono.empty(); + }) + .collectMap(Tuple2::getT1, Tuple2::getT2); } } diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ActionCollectionServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ActionCollectionServiceTest.java index 15892804e4..818d1c80d4 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ActionCollectionServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ActionCollectionServiceTest.java @@ -301,6 +301,7 @@ public class ActionCollectionServiceTest { action1.getActionConfiguration().setBody("mockBody"); actionCollectionDTO1.setActions(List.of(action1)); actionCollectionDTO1.setPluginType(PluginType.JS); + actionCollectionDTO1.setBody("export default { x: 1 }"); final ActionCollectionDTO createdActionCollectionDTO1 = layoutCollectionService.createCollection(actionCollectionDTO1).block(); @@ -316,7 +317,7 @@ public class ActionCollectionServiceTest { action2.getActionConfiguration().setBody("testCollection1.testAction1()"); actionCollectionDTO2.setActions(List.of(action2)); actionCollectionDTO2.setPluginType(PluginType.JS); - actionCollectionDTO2.setBody("testCollection1.testAction1()"); + actionCollectionDTO2.setBody("export default { x: testCollection1.testAction1() }"); final ActionCollectionDTO createdActionCollectionDTO2 = layoutCollectionService.createCollection(actionCollectionDTO2).block(); @@ -337,7 +338,7 @@ public class ActionCollectionServiceTest { StepVerifier.create(actionCollectionMono) .assertNext(actionCollection -> { assertEquals( - "testCollection1.newTestAction1()", + "export default { x: testCollection1.newTestAction1() }", actionCollection.getUnpublishedCollection().getBody() ); }) @@ -379,6 +380,7 @@ public class ActionCollectionServiceTest { action1.getActionConfiguration().setBody("mockBody"); actionCollectionDTO1.setActions(List.of(action1)); actionCollectionDTO1.setPluginType(PluginType.JS); + actionCollectionDTO1.setBody("export default { x: 1 }"); final ActionCollectionDTO createdActionCollectionDTO1 = layoutCollectionService.createCollection(actionCollectionDTO1).block(); @@ -394,7 +396,7 @@ public class ActionCollectionServiceTest { action2.getActionConfiguration().setBody("Api1.run()"); actionCollectionDTO2.setActions(List.of(action2)); actionCollectionDTO2.setPluginType(PluginType.JS); - actionCollectionDTO2.setBody("Api1.run()"); + actionCollectionDTO2.setBody("export default { x: Api1.run() }"); final ActionCollectionDTO createdActionCollectionDTO2 = layoutCollectionService.createCollection(actionCollectionDTO2).block(); @@ -415,7 +417,7 @@ public class ActionCollectionServiceTest { StepVerifier.create(actionCollectionMono) .assertNext(actionCollection -> { assertEquals( - "Api1.run()", + "export default { x: Api1.run() }", actionCollection.getUnpublishedCollection().getBody() ); }) diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ce/RefactoringSolutionCEImplTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ce/RefactoringSolutionCEImplTest.java new file mode 100644 index 0000000000..56b1361580 --- /dev/null +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ce/RefactoringSolutionCEImplTest.java @@ -0,0 +1,135 @@ +package com.appsmith.server.solutions.ce; + +import com.appsmith.server.configurations.InstanceConfig; +import com.appsmith.server.helpers.ResponseUtils; +import com.appsmith.server.services.ActionCollectionService; +import com.appsmith.server.services.ApplicationService; +import com.appsmith.server.services.AstService; +import com.appsmith.server.services.LayoutActionService; +import com.appsmith.server.services.NewActionService; +import com.appsmith.server.services.NewPageService; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Set; +import java.util.regex.Pattern; + +@ExtendWith(SpringExtension.class) +@Slf4j +class RefactoringSolutionCEImplTest { + + RefactoringSolutionCEImpl refactoringSolutionCE; + @MockBean + private ObjectMapper objectMapper; + @MockBean + private NewPageService newPageService; + @MockBean + private NewActionService newActionService; + @MockBean + private ActionCollectionService actionCollectionService; + @MockBean + private ResponseUtils responseUtils; + @MockBean + private LayoutActionService layoutActionService; + @MockBean + private ApplicationService applicationService; + @MockBean + private AstService astService; + @MockBean + private InstanceConfig instanceConfig; + + ObjectMapper mapper = new ObjectMapper(); + + private final String preWord = "\\b("; + private final String postWord = ")\\b"; + + @BeforeEach + public void setUp() { + refactoringSolutionCE = new RefactoringSolutionCEImpl(objectMapper, + newPageService, + newActionService, + actionCollectionService, + responseUtils, + layoutActionService, + applicationService, + astService, + instanceConfig); + } + + @Test + void testRefactorNameInDsl_whenRenamingTextWidget_replacesAllReferences() { + try (InputStream initialStream = this.getClass().getResourceAsStream("refactorDslWithOnlyWidgets.json"); + InputStream finalStream = this.getClass().getResourceAsStream("refactorDslWithOnlyWidgetsWithNewText.json")) { + assert initialStream != null; + JsonNode dslAsJsonNode = mapper.readTree(initialStream); + final String oldName = "Text3"; + Mono> updatesMono = refactoringSolutionCE.refactorNameInDsl( + dslAsJsonNode, + oldName, + "newText", + 2, + Pattern.compile(preWord + oldName + postWord)); + + StepVerifier.create(updatesMono) + .assertNext(updatedPaths -> { + Assertions.assertThat(updatedPaths).hasSize(3); + Assertions.assertThat(updatedPaths).containsExactlyInAnyOrder( + "Text3.widgetName", + "List1.template", + "List1.onListItemClick"); + }) + .verifyComplete(); + + JsonNode finalDslAsJsonNode = mapper.readTree(finalStream); + Assertions.assertThat(dslAsJsonNode).isEqualTo(finalDslAsJsonNode); + + } catch (IOException e) { + Assertions.fail("Unexpected IOException", e); + } + } + + @Test + void testRefactorNameInDsl_whenRenamingListWidget_replacesTemplateReferences() { + try (InputStream initialStream = this.getClass().getResourceAsStream("refactorDslWithOnlyWidgets.json"); + InputStream finalStream = this.getClass().getResourceAsStream("refactorDslWithOnlyWidgetsWithNewList.json")) { + assert initialStream != null; + JsonNode dslAsJsonNode = mapper.readTree(initialStream); + final String oldName = "List1"; + Mono> updatesMono = refactoringSolutionCE.refactorNameInDsl( + dslAsJsonNode, + oldName, + "newList", + 2, + Pattern.compile(preWord + oldName + postWord)); + + StepVerifier.create(updatesMono) + .assertNext(updatedPaths -> { + Assertions.assertThat(updatedPaths).hasSize(4); + Assertions.assertThat(updatedPaths).containsExactlyInAnyOrder( + "List1.widgetName", + "List1.template.Text4.text", + "List1.template.Image1.image", + "List1.template.Text3.text"); + }) + .verifyComplete(); + + JsonNode finalDslAsJsonNode = mapper.readTree(finalStream); + Assertions.assertThat(dslAsJsonNode).isEqualTo(finalDslAsJsonNode); + + } catch (IOException e) { + Assertions.fail("Unexpected IOException", e); + } + } + +} \ No newline at end of file diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ce/RefactoringSolutionCETest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ce/RefactoringSolutionCETest.java index 9a41d17f5f..db67bab42d 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ce/RefactoringSolutionCETest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ce/RefactoringSolutionCETest.java @@ -4,6 +4,7 @@ import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionDTO; import com.appsmith.external.models.Datasource; import com.appsmith.external.models.PluginType; +import com.appsmith.external.models.Property; import com.appsmith.server.constants.FieldName; import com.appsmith.server.domains.ActionCollection; import com.appsmith.server.domains.Application; @@ -247,9 +248,12 @@ class RefactoringSolutionCETest { action.setDatasource(datasource); JSONObject dsl = new JSONObject(); + dsl.put("widgetId", "firstWidgetId"); dsl.put("widgetName", "firstWidget"); JSONArray temp = new JSONArray(); - temp.addAll(List.of(new JSONObject(Map.of("key", "testField")))); + temp.addAll(List.of(new JSONObject(Map.of("key", "innerArrayReference[0].innerK")), + new JSONObject(Map.of("key", "innerObjectReference.k")), + new JSONObject(Map.of("key", "testField")))); dsl.put("dynamicBindingPathList", temp); dsl.put("testField", "{{ \tbeforeNameChange.data }}"); final JSONObject innerObjectReference = new JSONObject(); @@ -311,9 +315,12 @@ class RefactoringSolutionCETest { action.setDatasource(datasource); JSONObject dsl = new JSONObject(); + dsl.put("widgetId", "firstWidgetId"); dsl.put("widgetName", "firstWidget"); JSONArray temp = new JSONArray(); - temp.addAll(List.of(new JSONObject(Map.of("key", "testField")))); + temp.addAll(List.of(new JSONObject(Map.of("key", "innerArrayReference[0].innerK")), + new JSONObject(Map.of("key", "innerObjectReference.k")), + new JSONObject(Map.of("key", "testField")))); dsl.put("dynamicBindingPathList", temp); dsl.put("testField", "{{ \tbeforeNameChange.data }}"); final JSONObject innerObjectReference = new JSONObject(); @@ -477,6 +484,7 @@ class RefactoringSolutionCETest { action.setDatasource(datasource); JSONObject dsl = new JSONObject(); + dsl.put("widgetId", "firstWidgetId"); dsl.put("widgetName", "firstWidget"); JSONArray temp = new JSONArray(); temp.addAll(List.of(new JSONObject(Map.of("key", "testField")))); @@ -545,6 +553,7 @@ class RefactoringSolutionCETest { Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor())); JSONObject dsl = new JSONObject(); + dsl.put("widgetId", "testId"); dsl.put("widgetName", "Table1"); dsl.put("type", "TABLE_WIDGET"); Map primaryColumns = new HashMap(); @@ -591,6 +600,7 @@ class RefactoringSolutionCETest { Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor())); JSONObject dsl = new JSONObject(); + dsl.put("widgetId", "simpleRefactorId"); dsl.put("widgetName", "Table1"); dsl.put("type", "TABLE_WIDGET"); Layout layout = testPage.getLayouts().get(0); @@ -626,13 +636,17 @@ class RefactoringSolutionCETest { Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor())); JSONObject dsl = new JSONObject(); + dsl.put("widgetId", "testId"); dsl.put("widgetName", "List1"); dsl.put("type", "LIST_WIDGET"); JSONObject template = new JSONObject(); - template.put("oldWidgetName", "irrelevantContent"); + JSONObject oldWidgetTemplate = new JSONObject(); + oldWidgetTemplate.put("widgetName", "oldWidgetName"); + template.put("oldWidgetName", oldWidgetTemplate); dsl.put("template", template); final JSONArray children = new JSONArray(); final JSONObject defaultWidget = new JSONObject(); + defaultWidget.put("widgetId", "testId2"); defaultWidget.put("widgetName", "oldWidgetName"); defaultWidget.put("type", "TEXT_WIDGET"); children.add(defaultWidget); @@ -648,7 +662,7 @@ class RefactoringSolutionCETest { refactorNameDTO.setOldName("oldWidgetName"); refactorNameDTO.setNewName("newWidgetName"); - Mono widgetRenameMono = refactoringSolution.refactorWidgetName(refactorNameDTO).cache(); + Mono widgetRenameMono = refactoringSolution.refactorWidgetName(refactorNameDTO); StepVerifier .create(widgetRenameMono) @@ -667,6 +681,7 @@ class RefactoringSolutionCETest { // Set up table widget in DSL JSONObject dsl = new JSONObject(); + dsl.put("widgetId", "testId"); dsl.put("widgetName", "Table1"); dsl.put("type", "TABLE_WIDGET"); Layout layout = testPage.getLayouts().get(0); @@ -681,11 +696,16 @@ class RefactoringSolutionCETest { actionCollectionDTO1.setApplicationId(testApp.getId()); actionCollectionDTO1.setWorkspaceId(testApp.getWorkspaceId()); actionCollectionDTO1.setPluginId(jsDatasource.getPluginId()); + ActionDTO action1 = new ActionDTO(); action1.setName("testAction1"); action1.setActionConfiguration(new ActionConfiguration()); action1.getActionConfiguration().setBody("\tTable1"); - actionCollectionDTO1.setBody("\tTable1"); + action1.setDynamicBindingPathList(List.of(new Property("body", null))); + action1.setPluginType(PluginType.JS); + + actionCollectionDTO1.setActions(List.of(action1)); + actionCollectionDTO1.setBody("export default { x : \tTable1 }"); actionCollectionDTO1.setActions(List.of(action1)); actionCollectionDTO1.setPluginType(PluginType.JS); @@ -710,7 +730,7 @@ class RefactoringSolutionCETest { .assertNext(tuple -> { final ActionCollection actionCollection = tuple.getT1(); final NewAction action = tuple.getT2(); - assertThat(actionCollection.getUnpublishedCollection().getBody()).isEqualTo("\tNewNameTable1"); + assertThat(actionCollection.getUnpublishedCollection().getBody()).isEqualTo("export default { x : \tNewNameTable1 }"); final ActionDTO unpublishedAction = action.getUnpublishedAction(); assertThat(unpublishedAction.getJsonPathKeys().size()).isEqualTo(1); final Optional first = unpublishedAction.getJsonPathKeys().stream().findFirst(); @@ -734,6 +754,7 @@ class RefactoringSolutionCETest { originalActionCollectionDTO.setPageId(testPage.getId()); originalActionCollectionDTO.setPluginId(jsDatasource.getPluginId()); originalActionCollectionDTO.setPluginType(PluginType.JS); + originalActionCollectionDTO.setBody("export default { x: 1 }"); ActionDTO action1 = new ActionDTO(); action1.setName("testAction1"); @@ -747,7 +768,7 @@ class RefactoringSolutionCETest { ActionCollectionDTO actionCollectionDTO = new ActionCollectionDTO(); assert dto != null; actionCollectionDTO.setId(dto.getId()); - actionCollectionDTO.setBody("body"); + actionCollectionDTO.setBody("export default { x: Table1 }"); actionCollectionDTO.setName("newName"); RefactorActionNameInCollectionDTO refactorActionNameInCollectionDTO = new RefactorActionNameInCollectionDTO(); @@ -772,7 +793,7 @@ class RefactoringSolutionCETest { final ActionCollectionDTO actionCollectionDTOResult = tuple.getT1().getUnpublishedCollection(); final NewAction newAction = tuple.getT2(); assertEquals("originalName", actionCollectionDTOResult.getName()); - assertEquals("body", actionCollectionDTOResult.getBody()); + assertEquals("export default { x: Table1 }", actionCollectionDTOResult.getBody()); assertEquals("newTestAction", newAction.getUnpublishedAction().getName()); assertEquals("originalName.newTestAction", newAction.getUnpublishedAction().getFullyQualifiedName()); }) diff --git a/app/server/appsmith-server/src/test/resources/com/appsmith/server/solutions/ce/refactorDslWithOnlyWidgets.json b/app/server/appsmith-server/src/test/resources/com/appsmith/server/solutions/ce/refactorDslWithOnlyWidgets.json new file mode 100644 index 0000000000..8723af564c --- /dev/null +++ b/app/server/appsmith-server/src/test/resources/com/appsmith/server/solutions/ce/refactorDslWithOnlyWidgets.json @@ -0,0 +1,374 @@ +{ + "widgetName": "MainContainer", + "widgetId": "0", + "type": "CANVAS_WIDGET", + "dynamicBindingPathList": [], + "children": [ + { + "widgetName": "Text1", + "type": "TEXT_WIDGET", + "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "dynamicBindingPathList": [ + { + "key": "fontFamily" + }, + { + "key": "borderRadius" + } + ], + "text": "Label", + "key": "3pqpn28ba4", + "widgetId": "wemfst2t7m", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + }, + { + "widgetName": "Text2", + "type": "TEXT_WIDGET", + "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "dynamicBindingPathList": [ + { + "key": "fontFamily" + }, + { + "key": "borderRadius" + }, + { + "key": "text" + } + ], + "text": "{{Text1.text}}", + "key": "3pqpn28ba4", + "widgetId": "2bensj901c", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + }, + { + "template": { + "Image1": { + "image": "{{List1.listData.map((currentItem) => currentItem.img)}}", + "widgetName": "Image1", + "type": "IMAGE_WIDGET", + "key": "e0c7wcn17q", + "dynamicBindingPathList": [ + { + "key": "image" + }, + { + "key": "borderRadius" + } + ], + "widgetId": "bvixbymoxr", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + }, + "Text3": { + "text": "{{List1.listData.map((currentItem) => currentItem.name)}}", + "widgetName": "Text3", + "type": "TEXT_WIDGET", + "key": "3pqpn28ba4", + "dynamicBindingPathList": [ + { + "key": "text" + }, + { + "key": "fontFamily" + }, + { + "key": "borderRadius" + } + ], + "widgetId": "6ox4ujv63y", + "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + }, + "Text4": { + "text": "{{List1.listData.map((currentItem, currentIndex) => {\n return (function(){\n return currentItem.id + Text1.text;\n })();\n })}}", + "widgetName": "Text4", + "type": "TEXT_WIDGET", + "key": "3pqpn28ba4", + "dynamicBindingPathList": [ + { + "key": "text" + }, + { + "key": "fontFamily" + }, + { + "key": "borderRadius" + } + ], + "widgetId": "rtlyvpkvhc", + "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + } + }, + "boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}", + "widgetName": "List1", + "type": "LIST_WIDGET", + "dynamicBindingPathList": [ + { + "key": "accentColor" + }, + { + "key": "borderRadius" + }, + { + "key": "boxShadow" + }, + { + "key": "template.Image1.image" + }, + { + "key": "template.Text3.text" + }, + { + "key": "template.Text4.text" + } + ], + "dynamicTriggerPathList": [ + { + "key": "onListItemClick" + } + ], + "onListItemClick": "{{Text3.text}}", + "children": [ + { + "widgetName": "Canvas1", + "type": "CANVAS_WIDGET", + "dynamicBindingPathList": [ + { + "key": "borderRadius" + }, + { + "key": "accentColor" + } + ], + "children": [ + { + "boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}", + "widgetName": "Container1", + "dynamicBindingPathList": [ + { + "key": "borderRadius" + }, + { + "key": "boxShadow" + } + ], + "children": [ + { + "widgetName": "Canvas2", + "type": "CANVAS_WIDGET", + "dynamicBindingPathList": [ + { + "key": "borderRadius" + }, + { + "key": "accentColor" + } + ], + "children": [ + { + "widgetName": "Image1", + "type": "IMAGE_WIDGET", + "dynamicBindingPathList": [ + { + "key": "image" + }, + { + "key": "borderRadius" + } + ], + "key": "e0c7wcn17q", + "image": "{{currentItem.img}}", + "widgetId": "bvixbymoxr", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + }, + { + "widgetName": "Text3", + "type": "TEXT_WIDGET", + "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "dynamicBindingPathList": [ + { + "key": "text" + }, + { + "key": "fontFamily" + }, + { + "key": "borderRadius" + } + ], + "text": "{{currentItem.name}}", + "key": "3pqpn28ba4", + "widgetId": "6ox4ujv63y", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + }, + { + "widgetName": "Text4", + "type": "TEXT_WIDGET", + "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "dynamicBindingPathList": [ + { + "key": "text" + }, + { + "key": "fontFamily" + }, + { + "key": "borderRadius" + } + ], + "text": "{{currentItem.id + Text1.text}}", + "key": "3pqpn28ba4", + "widgetId": "rtlyvpkvhc", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + } + ], + "key": "3m0y9rrh1o", + "widgetId": "zdz4f503fm", + "accentColor": "{{appsmith.theme.colors.primaryColor}}", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + } + ], + "key": "sca9shlkpb", + "widgetId": "vt8i2g9u5r", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + } + ], + "key": "3m0y9rrh1o", + "widgetId": "ki75z4pfxm", + "accentColor": "{{appsmith.theme.colors.primaryColor}}", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + } + ], + "key": "t35n4gddpu", + "widgetId": "bunz1f076j", + "accentColor": "{{appsmith.theme.colors.primaryColor}}", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + }, + { + "boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}", + "type": "TABLE_WIDGET_V2", + "dynamicBindingPathList": [ + { + "key": "primaryColumns.step.computedValue" + }, + { + "key": "primaryColumns.task.computedValue" + }, + { + "key": "primaryColumns.status.computedValue" + }, + { + "key": "primaryColumns.action.computedValue" + }, + { + "key": "primaryColumns.action.buttonColor" + }, + { + "key": "primaryColumns.action.borderRadius" + }, + { + "key": "primaryColumns.action.boxShadow" + }, + { + "key": "accentColor" + }, + { + "key": "borderRadius" + }, + { + "key": "boxShadow" + }, + { + "key": "childStylesheet.button.buttonColor" + }, + { + "key": "childStylesheet.button.borderRadius" + }, + { + "key": "childStylesheet.menuButton.menuColor" + }, + { + "key": "childStylesheet.menuButton.borderRadius" + }, + { + "key": "childStylesheet.iconButton.buttonColor" + }, + { + "key": "childStylesheet.iconButton.borderRadius" + }, + { + "key": "childStylesheet.editActions.saveButtonColor" + }, + { + "key": "childStylesheet.editActions.saveBorderRadius" + }, + { + "key": "childStylesheet.editActions.discardButtonColor" + }, + { + "key": "childStylesheet.editActions.discardBorderRadius" + } + ], + "accentColor": "{{appsmith.theme.colors.primaryColor}}", + "childStylesheet": { + "button": { + "buttonColor": "{{appsmith.theme.colors.primaryColor}}", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + }, + "menuButton": { + "menuColor": "{{appsmith.theme.colors.primaryColor}}", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + }, + "iconButton": { + "buttonColor": "{{appsmith.theme.colors.primaryColor}}", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + }, + "editActions": { + "saveButtonColor": "{{appsmith.theme.colors.primaryColor}}", + "saveBorderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", + "discardButtonColor": "{{appsmith.theme.colors.primaryColor}}", + "discardBorderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + } + }, + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", + "widgetName": "Table1", + "primaryColumns": { + "step": { + "id": "step", + "originalId": "step", + "alias": "step", + "label": "step", + "computedValue": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( currentRow[\"step\"]))}}" + }, + "task": { + "id": "task", + "originalId": "task", + "alias": "task", + "label": "task", + "computedValue": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( Text1.text + \" \" + currentRow[\"task\"]))}}" + }, + "status": { + "id": "status", + "originalId": "status", + "alias": "status", + "label": "status", + "computedValue": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( currentRow[\"status\"]))}}" + }, + "action": { + "id": "action", + "originalId": "action", + "alias": "action", + "label": "action", + "onClick": "{{currentRow.step === '#1' ? showAlert('Done', 'success') : currentRow.step === '#2' ? navigateTo('https://docs.appsmith.com/core-concepts/connecting-to-data-sources/querying-a-database',undefined,'NEW_WINDOW') : navigateTo('https://docs.appsmith.com/core-concepts/displaying-data-read/display-data-tables',undefined,'NEW_WINDOW')}}", + "computedValue": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( currentRow[\"action\"]))}}", + "buttonColor": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( appsmith.theme.colors.primaryColor))}}", + "borderRadius": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( appsmith.theme.borderRadius.appBorderRadius))}}", + "boxShadow": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( 'none'))}}" + } + }, + "key": "ouqfcjyuwa", + "widgetId": "vrcp6kbiz8" + } + ] +} \ No newline at end of file diff --git a/app/server/appsmith-server/src/test/resources/com/appsmith/server/solutions/ce/refactorDslWithOnlyWidgetsWithNewList.json b/app/server/appsmith-server/src/test/resources/com/appsmith/server/solutions/ce/refactorDslWithOnlyWidgetsWithNewList.json new file mode 100644 index 0000000000..b1515daaf7 --- /dev/null +++ b/app/server/appsmith-server/src/test/resources/com/appsmith/server/solutions/ce/refactorDslWithOnlyWidgetsWithNewList.json @@ -0,0 +1,374 @@ +{ + "widgetName": "MainContainer", + "widgetId": "0", + "type": "CANVAS_WIDGET", + "dynamicBindingPathList": [], + "children": [ + { + "widgetName": "Text1", + "type": "TEXT_WIDGET", + "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "dynamicBindingPathList": [ + { + "key": "fontFamily" + }, + { + "key": "borderRadius" + } + ], + "text": "Label", + "key": "3pqpn28ba4", + "widgetId": "wemfst2t7m", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + }, + { + "widgetName": "Text2", + "type": "TEXT_WIDGET", + "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "dynamicBindingPathList": [ + { + "key": "fontFamily" + }, + { + "key": "borderRadius" + }, + { + "key": "text" + } + ], + "text": "{{Text1.text}}", + "key": "3pqpn28ba4", + "widgetId": "2bensj901c", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + }, + { + "template": { + "Image1": { + "image": "{{newList.listData.map((currentItem) => currentItem.img)}}", + "widgetName": "Image1", + "type": "IMAGE_WIDGET", + "key": "e0c7wcn17q", + "dynamicBindingPathList": [ + { + "key": "image" + }, + { + "key": "borderRadius" + } + ], + "widgetId": "bvixbymoxr", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + }, + "Text3": { + "text": "{{newList.listData.map((currentItem) => currentItem.name)}}", + "widgetName": "Text3", + "type": "TEXT_WIDGET", + "key": "3pqpn28ba4", + "dynamicBindingPathList": [ + { + "key": "text" + }, + { + "key": "fontFamily" + }, + { + "key": "borderRadius" + } + ], + "widgetId": "6ox4ujv63y", + "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + }, + "Text4": { + "text": "{{newList.listData.map((currentItem, currentIndex) => {\n return (function(){\n return currentItem.id + Text1.text;\n })();\n })}}", + "widgetName": "Text4", + "type": "TEXT_WIDGET", + "key": "3pqpn28ba4", + "dynamicBindingPathList": [ + { + "key": "text" + }, + { + "key": "fontFamily" + }, + { + "key": "borderRadius" + } + ], + "widgetId": "rtlyvpkvhc", + "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + } + }, + "boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}", + "widgetName": "newList", + "type": "LIST_WIDGET", + "dynamicBindingPathList": [ + { + "key": "accentColor" + }, + { + "key": "borderRadius" + }, + { + "key": "boxShadow" + }, + { + "key": "template.Image1.image" + }, + { + "key": "template.Text3.text" + }, + { + "key": "template.Text4.text" + } + ], + "dynamicTriggerPathList": [ + { + "key": "onListItemClick" + } + ], + "onListItemClick": "{{Text3.text}}", + "children": [ + { + "widgetName": "Canvas1", + "type": "CANVAS_WIDGET", + "dynamicBindingPathList": [ + { + "key": "borderRadius" + }, + { + "key": "accentColor" + } + ], + "children": [ + { + "boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}", + "widgetName": "Container1", + "dynamicBindingPathList": [ + { + "key": "borderRadius" + }, + { + "key": "boxShadow" + } + ], + "children": [ + { + "widgetName": "Canvas2", + "type": "CANVAS_WIDGET", + "dynamicBindingPathList": [ + { + "key": "borderRadius" + }, + { + "key": "accentColor" + } + ], + "children": [ + { + "widgetName": "Image1", + "type": "IMAGE_WIDGET", + "dynamicBindingPathList": [ + { + "key": "image" + }, + { + "key": "borderRadius" + } + ], + "key": "e0c7wcn17q", + "image": "{{currentItem.img}}", + "widgetId": "bvixbymoxr", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + }, + { + "widgetName": "Text3", + "type": "TEXT_WIDGET", + "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "dynamicBindingPathList": [ + { + "key": "text" + }, + { + "key": "fontFamily" + }, + { + "key": "borderRadius" + } + ], + "text": "{{currentItem.name}}", + "key": "3pqpn28ba4", + "widgetId": "6ox4ujv63y", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + }, + { + "widgetName": "Text4", + "type": "TEXT_WIDGET", + "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "dynamicBindingPathList": [ + { + "key": "text" + }, + { + "key": "fontFamily" + }, + { + "key": "borderRadius" + } + ], + "text": "{{currentItem.id + Text1.text}}", + "key": "3pqpn28ba4", + "widgetId": "rtlyvpkvhc", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + } + ], + "key": "3m0y9rrh1o", + "widgetId": "zdz4f503fm", + "accentColor": "{{appsmith.theme.colors.primaryColor}}", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + } + ], + "key": "sca9shlkpb", + "widgetId": "vt8i2g9u5r", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + } + ], + "key": "3m0y9rrh1o", + "widgetId": "ki75z4pfxm", + "accentColor": "{{appsmith.theme.colors.primaryColor}}", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + } + ], + "key": "t35n4gddpu", + "widgetId": "bunz1f076j", + "accentColor": "{{appsmith.theme.colors.primaryColor}}", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + }, + { + "boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}", + "type": "TABLE_WIDGET_V2", + "dynamicBindingPathList": [ + { + "key": "primaryColumns.step.computedValue" + }, + { + "key": "primaryColumns.task.computedValue" + }, + { + "key": "primaryColumns.status.computedValue" + }, + { + "key": "primaryColumns.action.computedValue" + }, + { + "key": "primaryColumns.action.buttonColor" + }, + { + "key": "primaryColumns.action.borderRadius" + }, + { + "key": "primaryColumns.action.boxShadow" + }, + { + "key": "accentColor" + }, + { + "key": "borderRadius" + }, + { + "key": "boxShadow" + }, + { + "key": "childStylesheet.button.buttonColor" + }, + { + "key": "childStylesheet.button.borderRadius" + }, + { + "key": "childStylesheet.menuButton.menuColor" + }, + { + "key": "childStylesheet.menuButton.borderRadius" + }, + { + "key": "childStylesheet.iconButton.buttonColor" + }, + { + "key": "childStylesheet.iconButton.borderRadius" + }, + { + "key": "childStylesheet.editActions.saveButtonColor" + }, + { + "key": "childStylesheet.editActions.saveBorderRadius" + }, + { + "key": "childStylesheet.editActions.discardButtonColor" + }, + { + "key": "childStylesheet.editActions.discardBorderRadius" + } + ], + "accentColor": "{{appsmith.theme.colors.primaryColor}}", + "childStylesheet": { + "button": { + "buttonColor": "{{appsmith.theme.colors.primaryColor}}", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + }, + "menuButton": { + "menuColor": "{{appsmith.theme.colors.primaryColor}}", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + }, + "iconButton": { + "buttonColor": "{{appsmith.theme.colors.primaryColor}}", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + }, + "editActions": { + "saveButtonColor": "{{appsmith.theme.colors.primaryColor}}", + "saveBorderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", + "discardButtonColor": "{{appsmith.theme.colors.primaryColor}}", + "discardBorderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + } + }, + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", + "widgetName": "Table1", + "primaryColumns": { + "step": { + "id": "step", + "originalId": "step", + "alias": "step", + "label": "step", + "computedValue": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( currentRow[\"step\"]))}}" + }, + "task": { + "id": "task", + "originalId": "task", + "alias": "task", + "label": "task", + "computedValue": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( Text1.text + \" \" + currentRow[\"task\"]))}}" + }, + "status": { + "id": "status", + "originalId": "status", + "alias": "status", + "label": "status", + "computedValue": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( currentRow[\"status\"]))}}" + }, + "action": { + "id": "action", + "originalId": "action", + "alias": "action", + "label": "action", + "onClick": "{{currentRow.step === '#1' ? showAlert('Done', 'success') : currentRow.step === '#2' ? navigateTo('https://docs.appsmith.com/core-concepts/connecting-to-data-sources/querying-a-database',undefined,'NEW_WINDOW') : navigateTo('https://docs.appsmith.com/core-concepts/displaying-data-read/display-data-tables',undefined,'NEW_WINDOW')}}", + "computedValue": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( currentRow[\"action\"]))}}", + "buttonColor": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( appsmith.theme.colors.primaryColor))}}", + "borderRadius": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( appsmith.theme.borderRadius.appBorderRadius))}}", + "boxShadow": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( 'none'))}}" + } + }, + "key": "ouqfcjyuwa", + "widgetId": "vrcp6kbiz8" + } + ] +} \ No newline at end of file diff --git a/app/server/appsmith-server/src/test/resources/com/appsmith/server/solutions/ce/refactorDslWithOnlyWidgetsWithNewText.json b/app/server/appsmith-server/src/test/resources/com/appsmith/server/solutions/ce/refactorDslWithOnlyWidgetsWithNewText.json new file mode 100644 index 0000000000..27f05d9c77 --- /dev/null +++ b/app/server/appsmith-server/src/test/resources/com/appsmith/server/solutions/ce/refactorDslWithOnlyWidgetsWithNewText.json @@ -0,0 +1,374 @@ +{ + "widgetName": "MainContainer", + "widgetId": "0", + "type": "CANVAS_WIDGET", + "dynamicBindingPathList": [], + "children": [ + { + "widgetName": "Text1", + "type": "TEXT_WIDGET", + "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "dynamicBindingPathList": [ + { + "key": "fontFamily" + }, + { + "key": "borderRadius" + } + ], + "text": "Label", + "key": "3pqpn28ba4", + "widgetId": "wemfst2t7m", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + }, + { + "widgetName": "Text2", + "type": "TEXT_WIDGET", + "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "dynamicBindingPathList": [ + { + "key": "fontFamily" + }, + { + "key": "borderRadius" + }, + { + "key": "text" + } + ], + "text": "{{Text1.text}}", + "key": "3pqpn28ba4", + "widgetId": "2bensj901c", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + }, + { + "template": { + "Image1": { + "image": "{{List1.listData.map((currentItem) => currentItem.img)}}", + "widgetName": "Image1", + "type": "IMAGE_WIDGET", + "key": "e0c7wcn17q", + "dynamicBindingPathList": [ + { + "key": "image" + }, + { + "key": "borderRadius" + } + ], + "widgetId": "bvixbymoxr", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + }, + "newText": { + "text": "{{List1.listData.map((currentItem) => currentItem.name)}}", + "widgetName": "newText", + "type": "TEXT_WIDGET", + "key": "3pqpn28ba4", + "dynamicBindingPathList": [ + { + "key": "text" + }, + { + "key": "fontFamily" + }, + { + "key": "borderRadius" + } + ], + "widgetId": "6ox4ujv63y", + "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + }, + "Text4": { + "text": "{{List1.listData.map((currentItem, currentIndex) => {\n return (function(){\n return currentItem.id + Text1.text;\n })();\n })}}", + "widgetName": "Text4", + "type": "TEXT_WIDGET", + "key": "3pqpn28ba4", + "dynamicBindingPathList": [ + { + "key": "text" + }, + { + "key": "fontFamily" + }, + { + "key": "borderRadius" + } + ], + "widgetId": "rtlyvpkvhc", + "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + } + }, + "boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}", + "widgetName": "List1", + "type": "LIST_WIDGET", + "dynamicBindingPathList": [ + { + "key": "accentColor" + }, + { + "key": "borderRadius" + }, + { + "key": "boxShadow" + }, + { + "key": "template.Image1.image" + }, + { + "key": "template.newText.text" + }, + { + "key": "template.Text4.text" + } + ], + "dynamicTriggerPathList": [ + { + "key": "onListItemClick" + } + ], + "onListItemClick": "{{newText.text}}", + "children": [ + { + "widgetName": "Canvas1", + "type": "CANVAS_WIDGET", + "dynamicBindingPathList": [ + { + "key": "borderRadius" + }, + { + "key": "accentColor" + } + ], + "children": [ + { + "boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}", + "widgetName": "Container1", + "dynamicBindingPathList": [ + { + "key": "borderRadius" + }, + { + "key": "boxShadow" + } + ], + "children": [ + { + "widgetName": "Canvas2", + "type": "CANVAS_WIDGET", + "dynamicBindingPathList": [ + { + "key": "borderRadius" + }, + { + "key": "accentColor" + } + ], + "children": [ + { + "widgetName": "Image1", + "type": "IMAGE_WIDGET", + "dynamicBindingPathList": [ + { + "key": "image" + }, + { + "key": "borderRadius" + } + ], + "key": "e0c7wcn17q", + "image": "{{currentItem.img}}", + "widgetId": "bvixbymoxr", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + }, + { + "widgetName": "newText", + "type": "TEXT_WIDGET", + "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "dynamicBindingPathList": [ + { + "key": "text" + }, + { + "key": "fontFamily" + }, + { + "key": "borderRadius" + } + ], + "text": "{{currentItem.name}}", + "key": "3pqpn28ba4", + "widgetId": "6ox4ujv63y", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + }, + { + "widgetName": "Text4", + "type": "TEXT_WIDGET", + "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "dynamicBindingPathList": [ + { + "key": "text" + }, + { + "key": "fontFamily" + }, + { + "key": "borderRadius" + } + ], + "text": "{{currentItem.id + Text1.text}}", + "key": "3pqpn28ba4", + "widgetId": "rtlyvpkvhc", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + } + ], + "key": "3m0y9rrh1o", + "widgetId": "zdz4f503fm", + "accentColor": "{{appsmith.theme.colors.primaryColor}}", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + } + ], + "key": "sca9shlkpb", + "widgetId": "vt8i2g9u5r", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + } + ], + "key": "3m0y9rrh1o", + "widgetId": "ki75z4pfxm", + "accentColor": "{{appsmith.theme.colors.primaryColor}}", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + } + ], + "key": "t35n4gddpu", + "widgetId": "bunz1f076j", + "accentColor": "{{appsmith.theme.colors.primaryColor}}", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + }, + { + "boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}", + "type": "TABLE_WIDGET_V2", + "dynamicBindingPathList": [ + { + "key": "primaryColumns.step.computedValue" + }, + { + "key": "primaryColumns.task.computedValue" + }, + { + "key": "primaryColumns.status.computedValue" + }, + { + "key": "primaryColumns.action.computedValue" + }, + { + "key": "primaryColumns.action.buttonColor" + }, + { + "key": "primaryColumns.action.borderRadius" + }, + { + "key": "primaryColumns.action.boxShadow" + }, + { + "key": "accentColor" + }, + { + "key": "borderRadius" + }, + { + "key": "boxShadow" + }, + { + "key": "childStylesheet.button.buttonColor" + }, + { + "key": "childStylesheet.button.borderRadius" + }, + { + "key": "childStylesheet.menuButton.menuColor" + }, + { + "key": "childStylesheet.menuButton.borderRadius" + }, + { + "key": "childStylesheet.iconButton.buttonColor" + }, + { + "key": "childStylesheet.iconButton.borderRadius" + }, + { + "key": "childStylesheet.editActions.saveButtonColor" + }, + { + "key": "childStylesheet.editActions.saveBorderRadius" + }, + { + "key": "childStylesheet.editActions.discardButtonColor" + }, + { + "key": "childStylesheet.editActions.discardBorderRadius" + } + ], + "accentColor": "{{appsmith.theme.colors.primaryColor}}", + "childStylesheet": { + "button": { + "buttonColor": "{{appsmith.theme.colors.primaryColor}}", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + }, + "menuButton": { + "menuColor": "{{appsmith.theme.colors.primaryColor}}", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + }, + "iconButton": { + "buttonColor": "{{appsmith.theme.colors.primaryColor}}", + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + }, + "editActions": { + "saveButtonColor": "{{appsmith.theme.colors.primaryColor}}", + "saveBorderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", + "discardButtonColor": "{{appsmith.theme.colors.primaryColor}}", + "discardBorderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + } + }, + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", + "widgetName": "Table1", + "primaryColumns": { + "step": { + "id": "step", + "originalId": "step", + "alias": "step", + "label": "step", + "computedValue": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( currentRow[\"step\"]))}}" + }, + "task": { + "id": "task", + "originalId": "task", + "alias": "task", + "label": "task", + "computedValue": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( Text1.text + \" \" + currentRow[\"task\"]))}}" + }, + "status": { + "id": "status", + "originalId": "status", + "alias": "status", + "label": "status", + "computedValue": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( currentRow[\"status\"]))}}" + }, + "action": { + "id": "action", + "originalId": "action", + "alias": "action", + "label": "action", + "onClick": "{{currentRow.step === '#1' ? showAlert('Done', 'success') : currentRow.step === '#2' ? navigateTo('https://docs.appsmith.com/core-concepts/connecting-to-data-sources/querying-a-database',undefined,'NEW_WINDOW') : navigateTo('https://docs.appsmith.com/core-concepts/displaying-data-read/display-data-tables',undefined,'NEW_WINDOW')}}", + "computedValue": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( currentRow[\"action\"]))}}", + "buttonColor": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( appsmith.theme.colors.primaryColor))}}", + "borderRadius": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( appsmith.theme.borderRadius.appBorderRadius))}}", + "boxShadow": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( 'none'))}}" + } + }, + "key": "ouqfcjyuwa", + "widgetId": "vrcp6kbiz8" + } + ] +} \ No newline at end of file diff --git a/app/shared/ast/src/index.ts b/app/shared/ast/src/index.ts index a24ffd485d..a144366904 100644 --- a/app/shared/ast/src/index.ts +++ b/app/shared/ast/src/index.ts @@ -3,7 +3,7 @@ import { ancestor, simple } from "acorn-walk"; import { ECMA_VERSION, NodeTypes } from "./constants/ast"; import { has, isFinite, isString, memoize, toPath } from "lodash"; import { isTrueObject, sanitizeScript } from "./utils"; - +import { jsObjectDeclaration } from "./jsObject/index"; /* * Valuable links: * @@ -107,6 +107,11 @@ type NodeWithLocation = NodeType & { type AstOptions = Omit; +type EntityRefactorResponse = { + isSuccess: boolean; + body: { script: string; refactorCount: number } | { error: string }; +}; + /* We need these functions to typescript casts the nodes with the correct types */ export const isIdentifierNode = (node: Node): node is IdentifierNode => { return node.type === NodeTypes.Identifier; @@ -213,7 +218,6 @@ export const extractIdentifierInfoFromCode = ( evaluationVersion: number, invalidIdentifiers?: Record ): IdentifierInfo => { - let ast: Node = { end: 0, start: 0, type: "" }; try { const sanitizedScript = sanitizeScript(code, evaluationVersion); @@ -262,9 +266,12 @@ export const entityRefactorFromCode = ( script: string, oldName: string, newName: string, + isJSObject: boolean, evaluationVersion: number, invalidIdentifiers?: Record -): Record | string => { +): EntityRefactorResponse => { + //If script is a JSObject then replace export default to decalartion. + if (isJSObject) script = jsObjectToCode(script); let ast: Node = { end: 0, start: 0, type: "" }; //Copy of script to refactor let refactorScript = script; @@ -284,7 +291,7 @@ export const entityRefactorFromCode = ( identifierList, }: NodeList = ancestorWalk(ast); let identifierArray = Array.from(identifierList) as Array; - const referencesArr = Array.from(references).filter((reference, index) => { + Array.from(references).forEach((reference, index) => { const topLevelIdentifier = toPath(reference)[0]; let shouldUpdateNode = !( functionalParams.has(topLevelIdentifier) || @@ -308,13 +315,17 @@ export const entityRefactorFromCode = ( refactorOffset += nameLengthDiff; ++refactorCount; } - return shouldUpdateNode; }); - return { script: refactorScript, count: refactorCount }; + //If script is a JSObject then revert decalartion to export default. + if (isJSObject) refactorScript = jsCodeToObject(refactorScript); + return { + isSuccess: true, + body: { script: refactorScript, refactorCount }, + }; } catch (e) { if (e instanceof SyntaxError) { // Syntax error. Ignore and return empty list - return "Syntax Error"; + return { isSuccess: false, body: { error: "Syntax Error" } }; } throw e; } @@ -598,3 +609,15 @@ const ancestorWalk = (ast: Node): NodeList => { identifierList, }; }; + +//Replace export default by a variable declaration. +//This is required for acorn to parse code into AST. +const jsObjectToCode = (script: string) => { + return script.replace(/export default/g, jsObjectDeclaration); +}; + +//Revert the string replacement from 'jsObjectToCode'. +//variable declaration is replaced back by export default. +const jsCodeToObject = (script: string) => { + return script.replace(jsObjectDeclaration, "export default"); +}; diff --git a/app/shared/ast/src/jsObject/index.ts b/app/shared/ast/src/jsObject/index.ts index a9655e24ac..b3a4feff74 100644 --- a/app/shared/ast/src/jsObject/index.ts +++ b/app/shared/ast/src/jsObject/index.ts @@ -1,7 +1,7 @@ -import { Node } from 'acorn'; -import { getAST } from '../index'; -import { generate } from 'astring'; -import { simple } from 'acorn-walk'; +import { Node } from "acorn"; +import { getAST } from "../index"; +import { generate } from "astring"; +import { simple } from "acorn-walk"; import { getFunctionalParamsFromNode, isPropertyAFunctionNode, @@ -9,7 +9,7 @@ import { isObjectExpression, PropertyNode, functionParam, -} from '../index'; +} from "../index"; type JsObjectProperty = { key: string; @@ -18,6 +18,11 @@ type JsObjectProperty = { arguments?: Array; }; +const jsObjectVariableName = + "____INTERNAL_JS_OBJECT_NAME_USED_FOR_PARSING_____"; + +export const jsObjectDeclaration = `var ${jsObjectVariableName} =`; + export const parseJSObjectWithAST = ( jsObjectBody: string ): Array => { @@ -26,9 +31,7 @@ export const parseJSObjectWithAST = ( if the variable name will be same then also we won't have problem here as jsObjectVariableName will be last node in VariableDeclarator hence overriding the previous JSObjectProperties. Keeping this just for sanity check if any caveat was missed. */ - const jsObjectVariableName = - '____INTERNAL_JS_OBJECT_NAME_USED_FOR_PARSING_____'; - const jsCode = `var ${jsObjectVariableName} = ${jsObjectBody}`; + const jsCode = `${jsObjectDeclaration} ${jsObjectBody}`; const ast = getAST(jsCode);