diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/ActionExecutionResult.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/ActionExecutionResult.java index a744609693..5bb255b6ac 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/ActionExecutionResult.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/ActionExecutionResult.java @@ -33,6 +33,8 @@ public class ActionExecutionResult { List dataTypes; + WidgetType suggestedWidget; + public void setErrorInfo(Throwable error) { this.body = error.getMessage(); diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/WidgetType.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/WidgetType.java new file mode 100644 index 0000000000..ff2bf4fe7f --- /dev/null +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/WidgetType.java @@ -0,0 +1,5 @@ +package com.appsmith.external.models; + +public enum WidgetType { + TEXT_WIDGET, LIST_WIDGET, DROP_DOWN_WIDGET, CHART_WIDGET, TABLE_WIDGET +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewActionServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewActionServiceImpl.java index ab574c8322..62c300bcea 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewActionServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewActionServiceImpl.java @@ -12,6 +12,7 @@ import com.appsmith.external.models.Param; import com.appsmith.external.models.Policy; import com.appsmith.external.models.Provider; import com.appsmith.external.models.RequestParamDTO; +import com.appsmith.external.models.WidgetType; import com.appsmith.external.plugins.PluginExecutor; import com.appsmith.server.acl.AclPermission; import com.appsmith.server.acl.PolicyGenerator; @@ -38,6 +39,8 @@ import com.appsmith.server.repositories.NewActionRepository; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang3.ObjectUtils; @@ -653,7 +656,7 @@ public class NewActionServiceImpl extends BaseService addDataTypes(result)); + .map(result -> addDataTypesAndSetSuggestedWidget(result)); } /* @@ -680,11 +683,13 @@ public class NewActionServiceImpl extends BaseService sendExecuteAnalyticsEvent( NewAction action, ActionDTO actionDTO, diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ActionServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ActionServiceTest.java index 5b51d262b4..2be4c087fd 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ActionServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ActionServiceTest.java @@ -15,6 +15,7 @@ import com.appsmith.external.models.PaginationType; import com.appsmith.external.models.ParsedDataType; import com.appsmith.external.models.Policy; import com.appsmith.external.models.Property; +import com.appsmith.external.models.WidgetType; import com.appsmith.external.plugins.PluginExecutor; import com.appsmith.server.acl.AclPermission; import com.appsmith.server.constants.FieldName; @@ -36,7 +37,10 @@ import com.appsmith.server.helpers.MockPluginExecutor; import com.appsmith.server.helpers.PluginExecutorHelper; import com.appsmith.server.repositories.OrganizationRepository; import com.appsmith.server.repositories.PluginRepository; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; import net.minidev.json.JSONArray; import net.minidev.json.JSONObject; @@ -439,6 +443,7 @@ public class ActionServiceTest { mockResult.setBody("response-body"); mockResult.setStatusCode("200"); mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); + mockResult.setSuggestedWidget(WidgetType.TEXT_WIDGET); ActionDTO action = new ActionDTO(); ActionConfiguration actionConfiguration = new ActionConfiguration(); @@ -468,6 +473,7 @@ public class ActionServiceTest { mockResult.setBody("response-body"); mockResult.setStatusCode("200"); mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); + mockResult.setSuggestedWidget(WidgetType.TEXT_WIDGET); ActionDTO action = new ActionDTO(); ActionConfiguration actionConfiguration = new ActionConfiguration(); @@ -494,6 +500,7 @@ public class ActionServiceTest { ActionExecutionResult mockResult = new ActionExecutionResult(); mockResult.setIsExecutionSuccess(true); mockResult.setBody("response-body"); + mockResult.setSuggestedWidget(WidgetType.TEXT_WIDGET); ActionDTO action = new ActionDTO(); ActionConfiguration actionConfiguration = new ActionConfiguration(); @@ -785,6 +792,7 @@ public class ActionServiceTest { private void executeAndAssertAction(ExecuteActionDTO executeActionDTO, ActionConfiguration actionConfiguration, ActionExecutionResult mockResult, List expectedReturnDataTypes) { + WidgetType expectedWidget = mockResult.getSuggestedWidget(); Mono actionExecutionResultMono = executeAction(executeActionDTO, actionConfiguration, mockResult); StepVerifier.create(actionExecutionResultMono) @@ -792,6 +800,7 @@ public class ActionServiceTest { assertThat(result).isNotNull(); assertThat(result.getBody()).isEqualTo(mockResult.getBody()); assertThat(result.getDataTypes().toString()).isEqualTo(expectedReturnDataTypes.toString()); + assertThat(result.getSuggestedWidget()).isEqualTo(expectedWidget); }) .verifyComplete(); } @@ -1212,6 +1221,7 @@ public class ActionServiceTest { "]"); mockResult.setStatusCode("200"); mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); + mockResult.setSuggestedWidget(WidgetType.TEXT_WIDGET); ActionDTO action = new ActionDTO(); ActionConfiguration actionConfiguration = new ActionConfiguration(); @@ -1251,6 +1261,7 @@ public class ActionServiceTest { " }"); mockResult.setStatusCode("200"); mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); + mockResult.setSuggestedWidget(WidgetType.TEXT_WIDGET); ActionDTO action = new ActionDTO(); ActionConfiguration actionConfiguration = new ActionConfiguration(); @@ -1290,6 +1301,7 @@ public class ActionServiceTest { mockResult.setStatusCode("200"); mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); + mockResult.setSuggestedWidget(WidgetType.TEXT_WIDGET); ActionDTO action = new ActionDTO(); ActionConfiguration actionConfiguration = new ActionConfiguration(); @@ -1320,6 +1332,7 @@ public class ActionServiceTest { mockResult.setBody(null); mockResult.setStatusCode("200"); mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); + mockResult.setSuggestedWidget(WidgetType.TEXT_WIDGET); ActionDTO action = new ActionDTO(); ActionConfiguration actionConfiguration = new ActionConfiguration(); @@ -1338,4 +1351,324 @@ public class ActionServiceTest { executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, new ArrayList<>()); } + + @Test + @WithUserDetails(value = "api_user") + public void testWidgetSuggestionAfterExecutionWithChartWidgetData() throws JsonProcessingException { + + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + ActionExecutionResult mockResult = new ActionExecutionResult(); + final String data = "{ \"data\": [\n" + + " {\n" + + " \"x\": \"Mon\",\n" + + " \"y\": 10000\n" + + " },\n" + + " {\n" + + " \"x\": \"Tue\",\n" + + " \"y\": 12000\n" + + " },\n" + + " {\n" + + " \"x\": \"Wed\",\n" + + " \"y\": 32000\n" + + " },\n" + + " {\n" + + " \"x\": \"Thu\",\n" + + " \"y\": 28000\n" + + " },\n" + + " {\n" + + " \"x\": \"Fri\",\n" + + " \"y\": 14000\n" + + " },\n" + + " {\n" + + " \"x\": \"Sat\",\n" + + " \"y\": 19000\n" + + " },\n" + + " {\n" + + " \"x\": \"Sun\",\n" + + " \"y\": 36000\n" + + " }\n" + + "]}"; + final JsonNode arrNode = new ObjectMapper().readTree(data).get("data");; + + mockResult.setIsExecutionSuccess(true); + mockResult.setBody(arrNode); + mockResult.setStatusCode("200"); + mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); + mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); + mockResult.setSuggestedWidget(WidgetType.CHART_WIDGET); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.POST); + actionConfiguration.setBody("random-request-body"); + actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecute"); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createAction(action).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, + List.of(new ParsedDataType(DisplayDataType.RAW))); + + } + + @Test + @WithUserDetails(value = "api_user") + public void testWidgetSuggestionAfterExecutionWithTableWidgetData() throws JsonProcessingException { + + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + ActionExecutionResult mockResult = new ActionExecutionResult(); + final String data = "{ \"data\": [\n" + + "\t{\n" + + "\t\t\"id\": \"0001\",\n" + + "\t\t\"type\": \"donut\",\n" + + "\t\t\"name\": \"Cake\",\n" + + "\t\t\"ppu\": 0.55,\n" + + "\t\t\"batters\":\n" + + "\t\t\t{\n" + + "\t\t\t\t\"batter\":\n" + + "\t\t\t\t\t[\n" + + "\t\t\t\t\t\t{ \"id\": \"1001\", \"type\": \"Regular\" },\n" + + "\t\t\t\t\t\t{ \"id\": \"1002\", \"type\": \"Chocolate\" },\n" + + "\t\t\t\t\t\t{ \"id\": \"1003\", \"type\": \"Blueberry\" },\n" + + "\t\t\t\t\t\t{ \"id\": \"1004\", \"type\": \"Devil's Food\" }\n" + + "\t\t\t\t\t]\n" + + "\t\t\t},\n" + + "\t\t\"topping\":\n" + + "\t\t\t[\n" + + "\t\t\t\t{ \"id\": \"5001\", \"type\": \"None\" },\n" + + "\t\t\t\t{ \"id\": \"5002\", \"type\": \"Glazed\" },\n" + + "\t\t\t\t{ \"id\": \"5005\", \"type\": \"Sugar\" },\n" + + "\t\t\t\t{ \"id\": \"5007\", \"type\": \"Powdered Sugar\" },\n" + + "\t\t\t\t{ \"id\": \"5006\", \"type\": \"Chocolate with Sprinkles\" },\n" + + "\t\t\t\t{ \"id\": \"5003\", \"type\": \"Chocolate\" },\n" + + "\t\t\t\t{ \"id\": \"5004\", \"type\": \"Maple\" }\n" + + "\t\t\t]\n" + + "\t},\n" + + "\t{\n" + + "\t\t\"id\": \"0002\",\n" + + "\t\t\"type\": \"donut\",\n" + + "\t\t\"name\": \"Raised\",\n" + + "\t\t\"ppu\": 0.55,\n" + + "\t\t\"batters\":\n" + + "\t\t\t{\n" + + "\t\t\t\t\"batter\":\n" + + "\t\t\t\t\t[\n" + + "\t\t\t\t\t\t{ \"id\": \"1001\", \"type\": \"Regular\" }\n" + + "\t\t\t\t\t]\n" + + "\t\t\t},\n" + + "\t\t\"topping\":\n" + + "\t\t\t[\n" + + "\t\t\t\t{ \"id\": \"5001\", \"type\": \"None\" },\n" + + "\t\t\t\t{ \"id\": \"5002\", \"type\": \"Glazed\" },\n" + + "\t\t\t\t{ \"id\": \"5005\", \"type\": \"Sugar\" },\n" + + "\t\t\t\t{ \"id\": \"5003\", \"type\": \"Chocolate\" },\n" + + "\t\t\t\t{ \"id\": \"5004\", \"type\": \"Maple\" }\n" + + "\t\t\t]\n" + + "\t},\n" + + "\t{\n" + + "\t\t\"id\": \"0003\",\n" + + "\t\t\"type\": \"donut\",\n" + + "\t\t\"name\": \"Old Fashioned\",\n" + + "\t\t\"ppu\": 0.55,\n" + + "\t\t\"batters\":\n" + + "\t\t\t{\n" + + "\t\t\t\t\"batter\":\n" + + "\t\t\t\t\t[\n" + + "\t\t\t\t\t\t{ \"id\": \"1001\", \"type\": \"Regular\" },\n" + + "\t\t\t\t\t\t{ \"id\": \"1002\", \"type\": \"Chocolate\" }\n" + + "\t\t\t\t\t]\n" + + "\t\t\t},\n" + + "\t\t\"topping\":\n" + + "\t\t\t[\n" + + "\t\t\t\t{ \"id\": \"5001\", \"type\": \"None\" },\n" + + "\t\t\t\t{ \"id\": \"5002\", \"type\": \"Glazed\" },\n" + + "\t\t\t\t{ \"id\": \"5003\", \"type\": \"Chocolate\" },\n" + + "\t\t\t\t{ \"id\": \"5004\", \"type\": \"Maple\" }\n" + + "\t\t\t]\n" + + "\t}\n" + + "]}"; + final JsonNode arrNode = new ObjectMapper().readTree(data).get("data");; + + mockResult.setIsExecutionSuccess(true); + mockResult.setBody(arrNode); + mockResult.setStatusCode("200"); + mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); + mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); + mockResult.setSuggestedWidget(WidgetType.TABLE_WIDGET); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.POST); + actionConfiguration.setBody("random-request-body"); + actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecute"); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createAction(action).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, + List.of(new ParsedDataType(DisplayDataType.RAW))); + + } + + @Test + @WithUserDetails(value = "api_user") + public void testWidgetSuggestionAfterExecutionWithListWidgetData() throws JsonProcessingException { + + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + ActionExecutionResult mockResult = new ActionExecutionResult(); + final String data = "{ \"data\": [\n" + + " {\n" + + " \"url\": \"images/thumbnails/0001.jpg\",\n" + + " \"width\": 32,\n" + + " \"height\": 32\n" + + " },\n" + + " {\n" + + " \"url\": \"images/0001.jpg\",\n" + + " \"width\": 200,\n" + + " \"height\": 200\n" + + " },\n" + + " {\n" + + " \"url\": \"images/0002.jpg\",\n" + + " \"width\": 200,\n" + + " \"height\": 200\n" + + " },\n" + + " {\n" + + " \"url\": \"images/0002.jpg\",\n" + + " \"width\": 200,\n" + + " \"height\": 200\n" + + " },\n" + + " {\n" + + " \"url\": \"images/0003.jpg\",\n" + + " \"width\": 200,\n" + + " \"height\": 200\n" + + " },\n" + + " {\n" + + " \"url\": \"images/0004.jpg\",\n" + + " \"width\": 200,\n" + + " \"height\": 200\n" + + " },\n" + + " {\n" + + " \"url\": \"images/0005.jpg\",\n" + + " \"width\": 200,\n" + + " \"height\": 200\n" + + " },\n" + + " {\n" + + " \"url\": \"images/0006.jpg\",\n" + + " \"width\": 200,\n" + + " \"height\": 200\n" + + " },\n" + + " {\n" + + " \"url\": \"images/0007.jpg\",\n" + + " \"width\": 200,\n" + + " \"height\": 200\n" + + " },\n" + + " {\n" + + " \"url\": \"images/0008.jpg\",\n" + + " \"width\": 200,\n" + + " \"height\": 200\n" + + " },\n" + + " {\n" + + " \"url\": \"images/0009.jpg\",\n" + + " \"width\": 200,\n" + + " \"height\": 200\n" + + " },\n" + + " {\n" + + " \"url\": \"images/0010.jpg\",\n" + + " \"width\": 200,\n" + + " \"height\": 200\n" + + " }\n" + + "]}"; + final JsonNode arrNode = new ObjectMapper().readTree(data).get("data");; + + mockResult.setIsExecutionSuccess(true); + mockResult.setBody(arrNode); + mockResult.setStatusCode("200"); + mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); + mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); + mockResult.setSuggestedWidget(WidgetType.LIST_WIDGET); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.POST); + actionConfiguration.setBody("random-request-body"); + actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecute"); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createAction(action).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, + List.of(new ParsedDataType(DisplayDataType.RAW))); + + } + + @Test + @WithUserDetails(value = "api_user") + public void testWidgetSuggestionAfterExecutionWithDropdownWidgetData() throws JsonProcessingException { + + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + ActionExecutionResult mockResult = new ActionExecutionResult(); + final String data = "{ \"data\": [\n" + + " {\n" + + " \"CarType\": \"BMW\",\n" + + " \"carID\": \"bmw123\"\n" + + " },\n" + + " {\n" + + " \"CarType\": \"mercedes\",\n" + + " \"carID\": \"merc123\"\n" + + " },\n" + + " {\n" + + " \"CarType\": \"volvo\",\n" + + " \"carID\": \"vol123r\"\n" + + " },\n" + + " {\n" + + " \"CarType\": \"ford\",\n" + + " \"carID\": \"ford123\"\n" + + " }\n" + + " ]}"; + final JsonNode arrNode = new ObjectMapper().readTree(data).get("data");; + + mockResult.setIsExecutionSuccess(true); + mockResult.setBody(arrNode); + mockResult.setStatusCode("200"); + mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); + mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); + mockResult.setSuggestedWidget(WidgetType.DROP_DOWN_WIDGET); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.POST); + actionConfiguration.setBody("random-request-body"); + actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecute"); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createAction(action).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, + List.of(new ParsedDataType(DisplayDataType.RAW))); + + } }