From 86ce80abc32d8f7472a06037b2ade6fb2f68104f Mon Sep 17 00:00:00 2001 From: Tolulope Adetula <31691737+Tooluloope@users.noreply.github.com> Date: Wed, 6 Apr 2022 12:48:55 +0100 Subject: [PATCH 01/34] feat: add total records and pageCount to table header --- .../Binding/Bind_TableTextPagination_spec.js | 5 +++++ .../TableWidget/component/TableHeader.tsx | 22 ++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_TableTextPagination_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_TableTextPagination_spec.js index 0ad1e94dfc..e270e40b29 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_TableTextPagination_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_TableTextPagination_spec.js @@ -73,6 +73,11 @@ describe("Test Create Api and Bind to Table widget", function() { cy.wait(500); cy.wait("@postExecute"); cy.wait(500); + cy.get(".show-page-items").should("contain", "20 Records"); + cy.get(".page-item") + .next() + .should("contain", "of 2"); + cy.get(".t--table-widget-next-page").should("not.have.attr", "disabled"); cy.ValidateTableData("1"); diff --git a/app/client/src/widgets/TableWidget/component/TableHeader.tsx b/app/client/src/widgets/TableWidget/component/TableHeader.tsx index dbdb5c3f5a..3da5102722 100644 --- a/app/client/src/widgets/TableWidget/component/TableHeader.tsx +++ b/app/client/src/widgets/TableWidget/component/TableHeader.tsx @@ -165,6 +165,11 @@ function TableHeader(props: TableHeaderProps) { {props.isVisiblePagination && props.serverSidePaginationEnabled && ( + {props.totalRecordsCount ? ( + + {props.totalRecordsCount} Records + + ) : null} - - {props.pageNo + 1} - + {props.totalRecordsCount ? ( + + Page{" "} + + {props.pageNo + 1} + {" "} + {`of ${props.pageCount}`} + + ) : ( + + {props.pageNo + 1} + + )} + Date: Thu, 7 Apr 2022 05:23:08 +0100 Subject: [PATCH 02/34] fix: add HTML default for empty space --- .../src/widgets/TableWidget/component/TableHeader.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/client/src/widgets/TableWidget/component/TableHeader.tsx b/app/client/src/widgets/TableWidget/component/TableHeader.tsx index 3da5102722..c643dc5a49 100644 --- a/app/client/src/widgets/TableWidget/component/TableHeader.tsx +++ b/app/client/src/widgets/TableWidget/component/TableHeader.tsx @@ -181,10 +181,11 @@ function TableHeader(props: TableHeaderProps) { {props.totalRecordsCount ? ( - Page{" "} + Page  {props.pageNo + 1} - {" "} + +   {`of ${props.pageCount}`} ) : ( @@ -223,14 +224,14 @@ function TableHeader(props: TableHeaderProps) { - Page{" "} + Page  {" "} - of {props.pageCount} + /> +   of {props.pageCount} Date: Tue, 19 Apr 2022 09:40:42 +0100 Subject: [PATCH 03/34] fix: Table Multiselect checkbox alignment --- .../widgets/TableWidget/component/TableStyledWrappers.tsx | 8 ++++++-- .../src/widgets/TableWidget/component/TableUtilities.tsx | 1 - 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/client/src/widgets/TableWidget/component/TableStyledWrappers.tsx b/app/client/src/widgets/TableWidget/component/TableStyledWrappers.tsx index 0bb1a555fa..fb88c1eb60 100644 --- a/app/client/src/widgets/TableWidget/component/TableStyledWrappers.tsx +++ b/app/client/src/widgets/TableWidget/component/TableStyledWrappers.tsx @@ -435,8 +435,12 @@ export const CellWrapper = styled.div<{ `; export const CellCheckboxWrapper = styled(CellWrapper)<{ isChecked?: boolean }>` - justify-content: center; - width: 40px; + &&& { + justify-content: center; + width: 40px; + padding: 0px; + align-items: center; + } & > div { ${(props) => props.isChecked diff --git a/app/client/src/widgets/TableWidget/component/TableUtilities.tsx b/app/client/src/widgets/TableWidget/component/TableUtilities.tsx index c5f78db54f..df7490c6ef 100644 --- a/app/client/src/widgets/TableWidget/component/TableUtilities.tsx +++ b/app/client/src/widgets/TableWidget/component/TableUtilities.tsx @@ -484,7 +484,6 @@ export const renderCheckBoxHeaderCell = ( isChecked={!!checkState} onClick={onClick} role="columnheader" - style={{ padding: "0px", justifyContent: "center" }} > {checkState === 1 && } From 04ac78ba1513277c61067b4a79057de7a74bf514 Mon Sep 17 00:00:00 2001 From: Tolulope Adetula <31691737+Tooluloope@users.noreply.github.com> Date: Wed, 20 Apr 2022 14:56:01 +0100 Subject: [PATCH 04/34] fix: Primary column control visibility icon out of sync --- app/client/src/components/ads/DraggableListCard.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/client/src/components/ads/DraggableListCard.tsx b/app/client/src/components/ads/DraggableListCard.tsx index 0645c8c95d..01fc9fe4da 100644 --- a/app/client/src/components/ads/DraggableListCard.tsx +++ b/app/client/src/components/ads/DraggableListCard.tsx @@ -61,6 +61,10 @@ export function DraggableListCard(props: RenderComponentProps) { const ref = useRef(null); const debouncedUpdate = _.debounce(updateOption, 1000); + useEffect(() => { + setVisibility(item.isVisible); + }, [item.isVisible]); + useEffect(() => { if (!isEditing && item && item.label) setValue(item.label); }, [item?.label, isEditing]); From b88cdacc7d072bbb5724742dcc7b04f28434019f Mon Sep 17 00:00:00 2001 From: Abhijeet <41686026+abhvsn@users.noreply.github.com> Date: Tue, 26 Apr 2022 13:13:58 +0530 Subject: [PATCH 05/34] Fix npe and duplication for JSObject during clone application (#13297) --- .../ce/ApplicationPageServiceCEImpl.java | 28 +- .../ce/ExamplesOrganizationClonerCEImpl.java | 10 +- .../services/ApplicationServiceTest.java | 258 +++++++++++++++++- 3 files changed, 281 insertions(+), 15 deletions(-) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ApplicationPageServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ApplicationPageServiceCEImpl.java index 15fabe82fb..25b871247a 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ApplicationPageServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ApplicationPageServiceCEImpl.java @@ -62,6 +62,7 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import static com.appsmith.external.helpers.AppsmithBeanUtils.copyNestedNonNullProperties; import static com.appsmith.server.acl.AclPermission.MANAGE_ACTIONS; import static com.appsmith.server.acl.AclPermission.MANAGE_APPLICATIONS; import static com.appsmith.server.acl.AclPermission.MANAGE_PAGES; @@ -557,7 +558,9 @@ public class ApplicationPageServiceCEImpl implements ApplicationPageServiceCE { unpublishedCollection.setPageId(newPageId); actionCollection.setApplicationId(clonedPage.getApplicationId()); - actionCollection.setDefaultResources(clonedPageDefaultResources); + DefaultResources defaultResources = new DefaultResources(); + copyNestedNonNullProperties(clonedPageDefaultResources, defaultResources); + actionCollection.setDefaultResources(defaultResources); DefaultResources defaultResourcesForDTO = new DefaultResources(); defaultResourcesForDTO.setPageId(clonedPageDefaultResources.getPageId()); @@ -576,14 +579,27 @@ public class ApplicationPageServiceCEImpl implements ApplicationPageServiceCE { if (StringUtils.isEmpty(clonedPageDefaultResources.getBranchName())) { unpublishedCollection .getDefaultToBranchedActionIdsMap() - .forEach((defaultId, oldActionId) -> - updatedDefaultToBranchedActionId.put(actionIdsMap.get(oldActionId), actionIdsMap.get(oldActionId))); - + .forEach((defaultId, oldActionId) -> { + // Filter out the actionIds for which the reference is not + // present in cloned actions, this happens when we have + // deleted action in unpublished mode + if (StringUtils.hasLength(oldActionId) && StringUtils.hasLength(actionIdsMap.get(oldActionId))) { + updatedDefaultToBranchedActionId + .put(actionIdsMap.get(oldActionId), actionIdsMap.get(oldActionId)); + } + }); } else { unpublishedCollection .getDefaultToBranchedActionIdsMap() - .forEach((defaultId, oldActionId) -> - updatedDefaultToBranchedActionId.put(defaultId, actionIdsMap.get(oldActionId))); + .forEach((defaultId, oldActionId) -> { + // Filter out the actionIds for which the reference is not + // present in cloned actions, this happens when we have + // deleted action in unpublished mode + if (StringUtils.hasLength(defaultId) && StringUtils.hasLength(actionIdsMap.get(oldActionId))) { + updatedDefaultToBranchedActionId + .put(defaultId, actionIdsMap.get(oldActionId)); + } + }); } unpublishedCollection.setDefaultToBranchedActionIdsMap(updatedDefaultToBranchedActionId); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ExamplesOrganizationClonerCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ExamplesOrganizationClonerCEImpl.java index 4aecbd9e27..bad41ed491 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ExamplesOrganizationClonerCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ExamplesOrganizationClonerCEImpl.java @@ -314,11 +314,13 @@ public class ExamplesOrganizationClonerCEImpl implements ExamplesOrganizationClo unpublishedCollection .getDefaultToBranchedActionIdsMap() .forEach((defaultActionId, oldActionId) -> { - if (!StringUtils.isEmpty(actionIdsMap.get(oldActionId))) { + if (StringUtils.hasLength(oldActionId) + && StringUtils.hasLength(actionIdsMap.get(oldActionId))) { + + // As this is a new application and not connected + // through git branch, the default and newly + // created actionId will be same newActionIds - // As this is a new application and not connected - // through git branch, the default and newly - // created actionId will be same .put(actionIdsMap.get(oldActionId), actionIdsMap.get(oldActionId)); } else { log.debug("Unable to find action {} while forking inside ID map: {}", oldActionId, actionIdsMap); diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ApplicationServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ApplicationServiceTest.java index 7f85bc2285..1da2175e58 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ApplicationServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ApplicationServiceTest.java @@ -1276,7 +1276,7 @@ public class ApplicationServiceTest { temp = new JSONArray(); temp.addAll(List.of(new JSONObject(Map.of("key", "testField1")))); secondWidget.put("dynamicBindingPathList", temp); - secondWidget.put("testField1", "{{ testCollection1.cloneActionCollection1.data }}"); + secondWidget.put("testField1", "{{ testCollection1.getData.data }}"); children.add(secondWidget); Layout layout = testPage.getLayouts().get(0); @@ -1295,16 +1295,29 @@ public class ApplicationServiceTest { "\t\tconst data = await cloneActionTest.run();\n" + "\t\treturn data;\n" + "\t}\n" + + "\tanotherMethod: async () => {\n" + + "\t\tconst data = await cloneActionTest.run();\n" + + "\t\treturn data;\n" + + "\t}\n" + "}"); ActionDTO action1 = new ActionDTO(); - action1.setName("cloneActionCollection1"); + action1.setName("getData"); action1.setActionConfiguration(new ActionConfiguration()); action1.getActionConfiguration().setBody( "async () => {\n" + "\t\tconst data = await cloneActionTest.run();\n" + "\t\treturn data;\n" + "\t}"); - actionCollectionDTO.setActions(List.of(action1)); + + ActionDTO action2 = new ActionDTO(); + action2.setName("anotherMethod"); + action2.setActionConfiguration(new ActionConfiguration()); + action2.getActionConfiguration().setBody( + "async () => {\n" + + "\t\tconst data = await cloneActionTest.run();\n" + + "\t\treturn data;\n" + + "\t}"); + actionCollectionDTO.setActions(List.of(action1, action2)); actionCollectionDTO.setPluginType(PluginType.JS); return Mono.zip( @@ -1399,7 +1412,7 @@ public class ApplicationServiceTest { }); }); - assertThat(actionList).hasSize(2); + assertThat(actionList).hasSize(3); actionList.forEach(newAction -> { assertThat(newAction.getDefaultResources()).isNotNull(); assertThat(newAction.getDefaultResources().getActionId()).isEqualTo(newAction.getId()); @@ -1422,7 +1435,7 @@ public class ApplicationServiceTest { ActionCollectionDTO unpublishedCollection = actionCollection.getUnpublishedCollection(); assertThat(unpublishedCollection.getDefaultToBranchedActionIdsMap()) - .hasSize(1); + .hasSize(2); unpublishedCollection.getDefaultToBranchedActionIdsMap().keySet() .forEach(key -> assertThat(key).isEqualTo(unpublishedCollection.getDefaultToBranchedActionIdsMap().get(key)) @@ -1512,6 +1525,241 @@ public class ApplicationServiceTest { .verifyComplete(); } + @Test + @WithUserDetails(value = "api_user") + public void cloneApplication_withDeletedActionInActionCollection_deletedActionIsNotCloned() { + Application testApplication = new Application(); + testApplication.setName("ApplicationServiceTest-clone-application-deleted-action-within-collection"); + + Mono originalApplicationMono = applicationPageService.createApplication(testApplication, orgId) + .cache(); + + Map> originalResourceIds = new HashMap<>(); + Mono resultMono = originalApplicationMono + .zipWhen(application -> newPageService.findPageById(application.getPages().get(0).getId(), READ_PAGES, false)) + .flatMap(tuple -> { + Application application = tuple.getT1(); + PageDTO testPage = tuple.getT2(); + + ActionDTO action = new ActionDTO(); + action.setName("cloneActionTest"); + action.setPageId(application.getPages().get(0).getId()); + action.setExecuteOnLoad(true); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.GET); + action.setActionConfiguration(actionConfiguration); + action.setDatasource(testDatasource); + + + // Save actionCollection + ActionCollectionDTO actionCollectionDTO = new ActionCollectionDTO(); + actionCollectionDTO.setName("testCollection1"); + actionCollectionDTO.setPageId(application.getPages().get(0).getId()); + actionCollectionDTO.setApplicationId(application.getId()); + actionCollectionDTO.setOrganizationId(application.getOrganizationId()); + actionCollectionDTO.setPluginId(testPlugin.getId()); + actionCollectionDTO.setVariables(List.of(new JSValue("test", "String", "test", true))); + actionCollectionDTO.setBody("export default {\n" + + "\tgetData: async () => {\n" + + "\t\tconst data = await cloneActionTest.run();\n" + + "\t\treturn data;\n" + + "\t},\n" + + "\tanotherMethod: async () => {\n" + + "\t\tconst data = await cloneActionTest.run();\n" + + "\t\treturn data;\n" + + "\t}\n" + + "}"); + ActionDTO action1 = new ActionDTO(); + action1.setName("getData"); + action1.setActionConfiguration(new ActionConfiguration()); + action1.getActionConfiguration().setBody( + "async () => {\n" + + "\t\tconst data = await cloneActionTest.run();\n" + + "\t\treturn data;\n" + + "\t}"); + ActionDTO action2 = new ActionDTO(); + action2.setName("anotherMethod"); + action2.setActionConfiguration(new ActionConfiguration()); + action2.getActionConfiguration().setBody( + "async () => {\n" + + "\t\tconst data = await cloneActionTest.run();\n" + + "\t\treturn data;\n" + + "\t}"); + actionCollectionDTO.setActions(List.of(action1, action2)); + actionCollectionDTO.setPluginType(PluginType.JS); + + ObjectMapper objectMapper = new ObjectMapper(); + JSONObject parentDsl = null; + try { + parentDsl = new JSONObject(objectMapper.readValue(DEFAULT_PAGE_LAYOUT, new TypeReference>() { + })); + } catch (JsonProcessingException e) { + log.debug("Error while creating JSONObj from defaultPageLayout: ", e); + } + + ArrayList children = (ArrayList) parentDsl.get("children"); + JSONObject firstWidget = new JSONObject(); + firstWidget.put("widgetName", "firstWidget"); + JSONArray temp = new JSONArray(); + temp.addAll(List.of(new JSONObject(Map.of("key", "testField")))); + firstWidget.put("dynamicBindingPathList", temp); + firstWidget.put("testField", "{{ cloneActionTest.data }}"); + children.add(firstWidget); + + JSONObject secondWidget = new JSONObject(); + secondWidget.put("widgetName", "secondWidget"); + temp = new JSONArray(); + temp.addAll(List.of(new JSONObject(Map.of("key", "testField1")))); + secondWidget.put("dynamicBindingPathList", temp); + secondWidget.put("testField1", "{{ testCollection1.getData.data }}"); + children.add(secondWidget); + + JSONObject thirdWidget = new JSONObject(); + thirdWidget.put("widgetName", "thirdWidget"); + temp = new JSONArray(); + temp.addAll(List.of(new JSONObject(Map.of("key", "testField1")))); + thirdWidget.put("dynamicBindingPathList", temp); + thirdWidget.put("testField1", "{{ testCollection1.anotherMethod.data }}"); + children.add(thirdWidget); + + Layout layout = testPage.getLayouts().get(0); + layout.setDsl(parentDsl); + + return Mono.zip( + layoutCollectionService.createCollection(actionCollectionDTO), + layoutActionService.createSingleAction(action), + layoutActionService.updateLayout(testPage.getId(), layout.getId(), layout), + Mono.just(application) + ); + }) + .flatMap(tuple -> { + List pageIds = new ArrayList<>(), collectionIds = new ArrayList<>(); + ActionCollectionDTO collectionDTO = tuple.getT1(); + collectionIds.add(collectionDTO.getId()); + tuple.getT4().getPages().forEach(page -> pageIds.add(page.getId())); + + originalResourceIds.put("pageIds", pageIds); + originalResourceIds.put("collectionIds", collectionIds); + + String deletedActionIdWithinActionCollection = String + .valueOf(collectionDTO.getDefaultToBranchedActionIdsMap().values().stream().findAny().orElse(null)); + + return newActionService.deleteUnpublishedAction(deletedActionIdWithinActionCollection) + .thenMany(newActionService.findAllByApplicationIdAndViewMode(tuple.getT4().getId(), false, READ_ACTIONS, null)) + .collectList() + .flatMap(actionList -> { + List actionIds = actionList.stream().map(BaseDomain::getId).collect(Collectors.toList()); + originalResourceIds.put("actionIds", actionIds); + return applicationPageService.cloneApplication(tuple.getT4().getId(), null); + }); + }) + .cache(); + + Policy manageAppPolicy = Policy.builder().permission(MANAGE_APPLICATIONS.getValue()) + .users(Set.of("api_user")) + .build(); + Policy readAppPolicy = Policy.builder().permission(READ_APPLICATIONS.getValue()) + .users(Set.of("api_user")) + .build(); + + Policy managePagePolicy = Policy.builder().permission(MANAGE_PAGES.getValue()) + .users(Set.of("api_user")) + .build(); + Policy readPagePolicy = Policy.builder().permission(READ_PAGES.getValue()) + .users(Set.of("api_user")) + .build(); + + StepVerifier.create(resultMono + .zipWhen(application -> Mono.zip( + newActionService.findAllByApplicationIdAndViewMode(application.getId(), false, READ_ACTIONS, null).collectList(), + actionCollectionService.findAllByApplicationIdAndViewMode(application.getId(), false, READ_ACTIONS, null).collectList(), + newPageService.findNewPagesByApplicationId(application.getId(), READ_PAGES).collectList() + ))) + .assertNext(tuple -> { + Application application = tuple.getT1(); // cloned application + List actionList = tuple.getT2().getT1(); + List actionCollectionList = tuple.getT2().getT2(); + List pageList = tuple.getT2().getT3(); + + assertThat(application).isNotNull(); + assertThat(application.isAppIsExample()).isFalse(); + assertThat(application.getId()).isNotNull(); + assertThat(application.getName().equals("ApplicationServiceTest Clone Source TestApp Copy")); + assertThat(application.getPolicies()).containsAll(Set.of(manageAppPolicy, readAppPolicy)); + assertThat(application.getOrganizationId().equals(orgId)); + assertThat(application.getModifiedBy()).isEqualTo("api_user"); + assertThat(application.getUpdatedAt()).isNotNull(); + List pages = application.getPages(); + Set pageIdsFromApplication = pages.stream().map(page -> page.getId()).collect(Collectors.toSet()); + Set pageIdsFromDb = pageList.stream().map(page -> page.getId()).collect(Collectors.toSet()); + + assertThat(pageIdsFromApplication.containsAll(pageIdsFromDb)); + + assertThat(pageList).isNotEmpty(); + for (NewPage page : pageList) { + assertThat(page.getPolicies()).containsAll(Set.of(managePagePolicy, readPagePolicy)); + assertThat(page.getApplicationId()).isEqualTo(application.getId()); + } + + assertThat(pageList).isNotEmpty(); + pageList.forEach(newPage -> { + assertThat(newPage.getDefaultResources()).isNotNull(); + assertThat(newPage.getDefaultResources().getPageId()).isEqualTo(newPage.getId()); + assertThat(newPage.getDefaultResources().getApplicationId()).isEqualTo(application.getId()); + + newPage.getUnpublishedPage() + .getLayouts() + .forEach(layout -> { + assertThat(layout.getLayoutOnLoadActions()).hasSize(1); + layout.getLayoutOnLoadActions().forEach(dslActionDTOS -> { + assertThat(dslActionDTOS).hasSize(2); + dslActionDTOS.forEach(actionDTO -> { + assertThat(actionDTO.getId()).isEqualTo(actionDTO.getDefaultActionId()); + if (StringUtils.hasLength(actionDTO.getCollectionId())) { + assertThat(actionDTO.getDefaultCollectionId()).isEqualTo(actionDTO.getCollectionId()); + } + }); + }); + }); + }); + + assertThat(actionList).hasSize(2); + actionList.forEach(newAction -> { + assertThat(newAction.getDefaultResources()).isNotNull(); + assertThat(newAction.getDefaultResources().getActionId()).isEqualTo(newAction.getId()); + assertThat(newAction.getDefaultResources().getApplicationId()).isEqualTo(application.getId()); + + ActionDTO action = newAction.getUnpublishedAction(); + assertThat(action.getDefaultResources()).isNotNull(); + assertThat(action.getDefaultResources().getPageId()).isEqualTo(application.getPages().get(0).getId()); + if (!StringUtils.isEmpty(action.getDefaultResources().getCollectionId())) { + assertThat(action.getDefaultResources().getCollectionId()).isEqualTo(action.getCollectionId()); + } + }); + + assertThat(actionCollectionList).hasSize(1); + actionCollectionList.forEach(actionCollection -> { + assertThat(actionCollection.getDefaultResources()).isNotNull(); + assertThat(actionCollection.getDefaultResources().getCollectionId()).isEqualTo(actionCollection.getId()); + assertThat(actionCollection.getDefaultResources().getApplicationId()).isEqualTo(application.getId()); + + ActionCollectionDTO unpublishedCollection = actionCollection.getUnpublishedCollection(); + + // We should have single entry as other action is deleted from the parent application + assertThat(unpublishedCollection.getDefaultToBranchedActionIdsMap()).hasSize(1); + unpublishedCollection.getDefaultToBranchedActionIdsMap().keySet() + .forEach(key -> + assertThat(key).isEqualTo(unpublishedCollection.getDefaultToBranchedActionIdsMap().get(key)) + ); + + assertThat(unpublishedCollection.getDefaultResources()).isNotNull(); + assertThat(unpublishedCollection.getDefaultResources().getPageId()) + .isEqualTo(application.getPages().get(0).getId()); + }); + }) + .verifyComplete(); + } + @Test @WithUserDetails(value = "api_user") public void basicPublishApplicationTest() { From 7e9eb58f03d2c88e4e8c328fd50862cf859230ec Mon Sep 17 00:00:00 2001 From: Sumit Kumar Date: Tue, 26 Apr 2022 16:51:30 +0530 Subject: [PATCH 06/34] fix: fix action objects missing plugin id and plugin type info in database (#13263) * add plugin id and type info to action object if found missing * this fix is currently added to the read and update action flows --- .../services/ce/NewActionServiceCE.java | 1 + .../services/ce/NewActionServiceCEImpl.java | 116 +++++++++++++++--- .../ce/NewActionServiceCEImplTest.java | 65 ++++++++++ 3 files changed, 168 insertions(+), 14 deletions(-) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/NewActionServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/NewActionServiceCE.java index 13599dbe02..4e1c531ed6 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/NewActionServiceCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/NewActionServiceCE.java @@ -99,4 +99,5 @@ public interface NewActionServiceCE extends CrudService { Mono findBranchedIdByBranchNameAndDefaultActionId(String branchName, String defaultActionId, AclPermission permission); + public Mono sanitizeAction(NewAction action); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/NewActionServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/NewActionServiceCEImpl.java index 836b49e2c5..853e690700 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/NewActionServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/NewActionServiceCEImpl.java @@ -120,6 +120,8 @@ public class NewActionServiceCEImpl extends BaseService { // In case of external datasource (not embedded) instead of storing the entire datasource // again inside the action, instead replace it with just the datasource ID. This is so that @@ -1100,7 +1102,8 @@ public class NewActionServiceCEImpl extends BaseService findUnpublishedOnLoadActionsExplicitSetByUserInPage(String pageId) { return repository - .findUnpublishedActionsByPageIdAndExecuteOnLoadSetByUserTrue(pageId, MANAGE_ACTIONS); + .findUnpublishedActionsByPageIdAndExecuteOnLoadSetByUserTrue(pageId, MANAGE_ACTIONS) + .flatMap(this::sanitizeAction); } /** @@ -1113,27 +1116,32 @@ public class NewActionServiceCEImpl extends BaseService findUnpublishedActionsInPageByNames(Set names, String pageId) { return repository - .findUnpublishedActionsByNameInAndPageId(names, pageId, MANAGE_ACTIONS); + .findUnpublishedActionsByNameInAndPageId(names, pageId, MANAGE_ACTIONS) + .flatMap(this::sanitizeAction); } @Override public Mono findById(String id) { - return repository.findById(id); + return repository.findById(id) + .flatMap(this::sanitizeAction); } @Override public Mono findById(String id, AclPermission aclPermission) { - return repository.findById(id, aclPermission); + return repository.findById(id, aclPermission) + .flatMap(this::sanitizeAction); } @Override public Flux findByPageId(String pageId, AclPermission permission) { - return repository.findByPageId(pageId, permission); + return repository.findByPageId(pageId, permission) + .flatMap(this::sanitizeAction); } @Override public Flux findByPageIdAndViewMode(String pageId, Boolean viewMode, AclPermission permission) { - return repository.findByPageIdAndViewMode(pageId, viewMode, permission); + return repository.findByPageIdAndViewMode(pageId, viewMode, permission) + .flatMap(this::sanitizeAction); } @Override @@ -1151,7 +1159,8 @@ public class NewActionServiceCEImpl extends BaseService repository.findByApplicationIdAndViewMode(application.getId(), false, READ_ACTIONS)) + .flatMap(this::sanitizeAction) .flatMap(this::setTransientFieldsInUnpublishedAction); } return repository.findAllActionsByNameAndPageIdsAndViewMode(name, pageIds, false, READ_ACTIONS, sort) + .flatMap(this::sanitizeAction) .flatMap(this::setTransientFieldsInUnpublishedAction); } @@ -1365,6 +1376,75 @@ public class NewActionServiceCEImpl extends BaseService !PluginType.JS.equals(actionDTO.getPluginType())); } + /** + * This method is meant to be used to check for any missing or bad values in NewAction object and attempt to fix it. + * + * This method is added in response to certain cases where it was found that pluginId and pluginType keys + * were missing from the NewAction object in the database.Since it is currently not know what exactly causes + * these values to go missing, this check will serve as a workaround by fetching and setting pluginId and + * pluginType using the datasource object contained in the ActionDTO object. + * Ref: https://github.com/appsmithorg/appsmith/issues/11927 + * + */ + public Mono sanitizeAction(NewAction action) { + Mono actionMono = Mono.just(action); + if (isPluginTypeOrPluginIdMissing(action)) { + actionMono = providePluginTypeAndIdToNewActionObjectUsingJSTypeOrDatasource(action); + } + + return actionMono; + } + + private boolean isPluginTypeOrPluginIdMissing(NewAction action) { + return action.getPluginId() == null || action.getPluginType() == null; + } + + private Mono providePluginTypeAndIdToNewActionObjectUsingJSTypeOrDatasource(NewAction action) { + ActionDTO actionDTO = action.getUnpublishedAction(); + if (actionDTO == null) { + return Mono.just(action); + } + + /** + * if path: + * In case an action object is related to a JS Object then it must have a non-null collectionId. + * + * else path: + * Otherwise, check if the datasource object has the pluginId. If so, use this pluginId to fetch the correct + * pluginType. + */ + Datasource datasource = actionDTO.getDatasource(); + if (actionDTO.getCollectionId() != null) { + return setPluginIdAndTypeForJSAction(action); + } + else if (datasource != null && datasource.getPluginId() != null) { + String pluginId = datasource.getPluginId(); + action.setPluginId(pluginId); + + return setPluginTypeFromId(action, pluginId); + } + + return Mono.just(action); + } + + private Mono setPluginTypeFromId(NewAction action, String pluginId) { + return pluginService.findById(pluginId) + .flatMap(plugin -> { + action.setPluginType(plugin.getType()); + return Mono.just(action); + }); + } + + private Mono setPluginIdAndTypeForJSAction(NewAction action) { + action.setPluginType(JS_PLUGIN_TYPE); + + return pluginService.findByPackageName(JS_PLUGIN_PACKAGE_NAME) + .flatMap(plugin -> { + action.setPluginId(plugin.getId()); + return Mono.just(action); + }); + } + // We can afford to make this call all the time since we already have all the info we need in context private Mono getRemoteDatasourceContext(Plugin plugin, Datasource datasource) { final DatasourceContext datasourceContext = new DatasourceContext(); @@ -1388,7 +1468,9 @@ public class NewActionServiceCEImpl extends BaseService repository.save(sanitizedAction)); } @Override @@ -1396,12 +1478,17 @@ public class NewActionServiceCEImpl extends BaseService action.getGitSyncId() == null) .forEach(action -> action.setGitSyncId(action.getApplicationId() + "_" + Instant.now().toString())); - return repository.saveAll(actions); + + return Flux.fromIterable(actions) + .flatMap(this::sanitizeAction) + .collectList() + .flatMapMany(actionList -> repository.saveAll(actionList)); } @Override public Flux findByPageId(String pageId) { - return repository.findByPageId(pageId); + return repository.findByPageId(pageId) + .flatMap(this::sanitizeAction); } /** @@ -1642,7 +1729,8 @@ public class NewActionServiceCEImpl extends BaseService findBranchedIdByBranchNameAndDefaultActionId(String branchName, String defaultActionId, AclPermission permission) { diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/NewActionServiceCEImplTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/NewActionServiceCEImplTest.java index 87c141b66a..f1f809d037 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/NewActionServiceCEImplTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/NewActionServiceCEImplTest.java @@ -1,8 +1,13 @@ package com.appsmith.server.services.ce; import com.appsmith.external.models.ActionExecutionResult; +import com.appsmith.external.models.Datasource; import com.appsmith.server.acl.PolicyGenerator; import com.appsmith.server.constants.FieldName; +import com.appsmith.server.domains.NewAction; +import com.appsmith.server.domains.Plugin; +import com.appsmith.server.domains.PluginType; +import com.appsmith.server.dtos.ActionDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; import com.appsmith.server.helpers.PluginExecutorHelper; @@ -23,6 +28,7 @@ import lombok.extern.slf4j.Slf4j; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.core.codec.ByteBufferDecoder; import org.springframework.core.codec.StringDecoder; @@ -56,6 +62,12 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.spy; + @RunWith(SpringRunner.class) @Slf4j @@ -215,4 +227,57 @@ public class NewActionServiceCEImplTest { .verify(); } + @Test + public void testMissingPluginIdAndTypeFixForNonJSPluginType() { + /* Mock `findById` method of pluginService to return `testPlugin` */ + Plugin testPlugin = new Plugin(); + testPlugin.setId("testId"); + testPlugin.setType(PluginType.DB); + Mockito.when(pluginService.findById(anyString())) + .thenReturn(Mono.just(testPlugin)); + + NewAction action = new NewAction(); + action.setPluginId(null); + action.setPluginType(null); + ActionDTO actionDTO = new ActionDTO(); + Datasource datasource = new Datasource(); + /* Datasource has correct plugin id */ + datasource.setPluginId(testPlugin.getId()); + actionDTO.setDatasource(datasource); + action.setUnpublishedAction(actionDTO); + + Mono updatedActionFlux = newActionService.sanitizeAction(action); + StepVerifier.create(updatedActionFlux) + .assertNext(updatedAction -> { + assertEquals("testId", updatedAction.getPluginId()); + assertEquals(PluginType.DB, updatedAction.getPluginType()); + }) + .verifyComplete(); + } + + @Test + public void testMissingPluginIdAndTypeFixForJSPluginType() { + /* Mock `findByPackageName` method of pluginService to return `testPlugin` */ + Plugin testPlugin = new Plugin(); + testPlugin.setId("testId"); + testPlugin.setType(PluginType.JS); + Mockito.when(pluginService.findByPackageName(anyString())) + .thenReturn(Mono.just(testPlugin)); + + NewAction action = new NewAction(); + action.setPluginId(null); + action.setPluginType(null); + ActionDTO actionDTO = new ActionDTO(); + /* Non-null collection id to indicate a JS action */ + actionDTO.setCollectionId("testId"); + action.setUnpublishedAction(actionDTO); + + Mono updatedActionFlux = newActionService.sanitizeAction(action); + StepVerifier.create(updatedActionFlux) + .assertNext(updatedAction -> { + assertEquals("testId", updatedAction.getPluginId()); + assertEquals(PluginType.JS, updatedAction.getPluginType()); + }) + .verifyComplete(); + } } \ No newline at end of file From 1d019d1376300af40f94fa1e4f0e343ee33b623e Mon Sep 17 00:00:00 2001 From: albinAppsmith <87797149+albinAppsmith@users.noreply.github.com> Date: Tue, 26 Apr 2022 19:26:07 +0530 Subject: [PATCH 07/34] fix: Modal padding and color fixes (#12189) * * share modal fixes * * modal ui fixes --- .../src/components/ads/CopyToClipBoard.tsx | 8 ++++++-- .../src/components/ads/DialogComponent.tsx | 17 +++++++++++------ app/client/src/constants/DefaultTheme.tsx | 8 +++++--- .../src/pages/Editor/gitSync/GitSyncModal.tsx | 12 +++++++++--- .../Editor/gitSync/components/DeployedKeyUI.tsx | 2 +- .../gitSync/components/StyledComponents.tsx | 3 ++- .../pages/organization/AppInviteUsersForm.tsx | 12 ++++++------ .../src/pages/organization/ManageUsers.tsx | 2 +- .../pages/organization/OrgInviteUsersForm.tsx | 14 +++++++++++--- 9 files changed, 52 insertions(+), 26 deletions(-) diff --git a/app/client/src/components/ads/CopyToClipBoard.tsx b/app/client/src/components/ads/CopyToClipBoard.tsx index 55d4812a67..447d0862e3 100644 --- a/app/client/src/components/ads/CopyToClipBoard.tsx +++ b/app/client/src/components/ads/CopyToClipBoard.tsx @@ -19,7 +19,11 @@ const Wrapper = styled.div<{ offset?: string }>` } `; -function CopyToClipboard(props: { copyText: string; btnWidth?: string }) { +function CopyToClipboard(props: { + className?: string; + copyText: string; + btnWidth?: string; +}) { const { copyText } = props; const copyURLInput = createRef(); const [isCopied, setIsCopied] = useState(false); @@ -38,7 +42,7 @@ function CopyToClipboard(props: { copyText: string; btnWidth?: string }) { } }; return ( - + { diff --git a/app/client/src/components/ads/DialogComponent.tsx b/app/client/src/components/ads/DialogComponent.tsx index b095ade2f7..2f31e183e4 100644 --- a/app/client/src/components/ads/DialogComponent.tsx +++ b/app/client/src/components/ads/DialogComponent.tsx @@ -26,8 +26,9 @@ const StyledDialog = styled(Dialog)<{ padding: 0; background: ${(props) => props.theme.colors.modal.bg}; box-shadow: none; - .${Classes.ICON} { - color: ${(props) => props.theme.colors.modal.iconColor}; + min-height: unset; + svg { + color: ${Colors.GREY_800}; } .${Classes.BUTTON}.${Classes.MINIMAL}:hover { @@ -39,19 +40,23 @@ const StyledDialog = styled(Dialog)<{ color: ${(props) => props.theme.colors.modal.headerText}; font-weight: ${(props) => props.theme.typography.h1.fontWeight}; font-size: ${(props) => props.theme.typography.h1.fontSize}px; - line-height: ${(props) => props.theme.typography.h1.lineHeight}px; + line-height: unset; letter-spacing: ${(props) => props.theme.typography.h1.letterSpacing}; } .${Classes.DIALOG_CLOSE_BUTTON} { - color: ${Colors.CHARCOAL}; + color: ${Colors.SCORPION}; min-width: 0; padding: 0; svg { - fill: ${Colors.CHARCOAL}; + fill: ${Colors.SCORPION}; width: 24px; height: 24px; + + &:hover { + fill: ${Colors.COD_GRAY}; + } } } @@ -78,7 +83,7 @@ const StyledDialog = styled(Dialog)<{ & .${Classes.DIALOG_BODY} { margin: 0; - margin-top: ${(props) => (props.noModalBodyMarginTop ? "0px" : "24px")}; + margin-top: ${(props) => (props.noModalBodyMarginTop ? "0px" : "16px")}; overflow: auto; } diff --git a/app/client/src/constants/DefaultTheme.tsx b/app/client/src/constants/DefaultTheme.tsx index 154b2b1fce..7ea13ce437 100644 --- a/app/client/src/constants/DefaultTheme.tsx +++ b/app/client/src/constants/DefaultTheme.tsx @@ -652,6 +652,7 @@ const lightShades = [ "#F86A2B", "#FFDEDE", "#575757", + "#191919", ] as const; type ShadeColor = typeof darkShades[number] | typeof lightShades[number]; @@ -1373,13 +1374,14 @@ const editorBottomBar = { const gitSyncModal = { menuBackgroundColor: Colors.ALABASTER_ALT, separator: Colors.ALTO2, - closeIcon: "rgba(29, 28, 29, 0.7);", + closeIcon: Colors.SCORPION, + closeIconHover: Colors.COD_GRAY, }; type GitSyncModalColors = typeof gitSyncModal; const tabItemBackgroundFill = { highlightBackground: Colors.Gallery, - highlightTextColor: Colors.CODE_GRAY, + highlightTextColor: Colors.COD_GRAY, textColor: Colors.CHARCOAL, }; @@ -2612,7 +2614,7 @@ export const light: ColorType = { }, modal: { bg: lightShades[11], - headerText: lightShades[10], + headerText: lightShades[20], iconColor: lightShades[5], iconBg: lightShades[18], user: { diff --git a/app/client/src/pages/Editor/gitSync/GitSyncModal.tsx b/app/client/src/pages/Editor/gitSync/GitSyncModal.tsx index 962b1bf491..cb785269b1 100644 --- a/app/client/src/pages/Editor/gitSync/GitSyncModal.tsx +++ b/app/client/src/pages/Editor/gitSync/GitSyncModal.tsx @@ -35,7 +35,6 @@ const Container = styled.div` flex-direction: column; position: relative; overflow-y: hidden; - padding: 0px 8px 0px 8px; `; const BodyContainer = styled.div` @@ -49,10 +48,17 @@ const MenuContainer = styled.div` const CloseBtnContainer = styled.div` position: absolute; - right: 0; + right: -5px; top: 0; - padding: ${(props) => props.theme.spaces[1]}px; + padding: ${(props) => props.theme.spaces[1]}px 0; border-radius: ${(props) => props.theme.radii[1]}px; + + &:hover { + svg, + svg path { + fill: ${({ theme }) => get(theme, "colors.gitSyncModal.closeIconHover")}; + } + } `; const ComponentsByTab = { diff --git a/app/client/src/pages/Editor/gitSync/components/DeployedKeyUI.tsx b/app/client/src/pages/Editor/gitSync/components/DeployedKeyUI.tsx index 46d9f08fc6..3b3c834f3a 100644 --- a/app/client/src/pages/Editor/gitSync/components/DeployedKeyUI.tsx +++ b/app/client/src/pages/Editor/gitSync/components/DeployedKeyUI.tsx @@ -61,7 +61,7 @@ const KeyText = styled.span` font-size: 10px; font-weight: 600; text-transform: uppercase; - color: ${Colors.CODE_GRAY}; + color: ${Colors.COD_GRAY}; `; const MoreMenuWrapper = styled.div` diff --git a/app/client/src/pages/Editor/gitSync/components/StyledComponents.tsx b/app/client/src/pages/Editor/gitSync/components/StyledComponents.tsx index c20ab15300..ca51e1b081 100644 --- a/app/client/src/pages/Editor/gitSync/components/StyledComponents.tsx +++ b/app/client/src/pages/Editor/gitSync/components/StyledComponents.tsx @@ -6,11 +6,12 @@ export const Title = styled.p` ${(props) => getTypographyByKey(props, "h1")}; margin: ${(props) => `${props.theme.spaces[7]}px 0px ${props.theme.spaces[3]}px 0px`}; + color: ${Colors.COD_GRAY}; `; export const Subtitle = styled.span` ${(props) => getTypographyByKey(props, "p1")}; - color: ${Colors.BLACK}; + color: ${Colors.COD_GRAY}; `; export const Caption = styled.span` diff --git a/app/client/src/pages/organization/AppInviteUsersForm.tsx b/app/client/src/pages/organization/AppInviteUsersForm.tsx index 5e4f0c6b13..cad71ff2a7 100644 --- a/app/client/src/pages/organization/AppInviteUsersForm.tsx +++ b/app/client/src/pages/organization/AppInviteUsersForm.tsx @@ -17,25 +17,25 @@ import { ANONYMOUS_USERNAME } from "constants/userConstants"; import { Colors } from "constants/Colors"; import { viewerURL } from "RouteBuilder"; +const StyledCopyToClipBoard = styled(CopyToClipBoard)` + margin-bottom: 24px; +`; + const CommonTitleTextStyle = css` color: ${Colors.CHARCOAL}; font-weight: normal; `; const Title = styled.div` - padding: 0 0 10px 0; + padding: 0 0 8px 0; & > span[type="h5"] { ${CommonTitleTextStyle} } `; -const StyledCopyToClipBoard = styled(CopyToClipBoard)` - margin-bottom: 24px; -`; - const ShareWithPublicOption = styled.div` display: flex; - margin-bottom: 24px; + margin-bottom: 8px; align-items: center; justify-content: space-between; diff --git a/app/client/src/pages/organization/ManageUsers.tsx b/app/client/src/pages/organization/ManageUsers.tsx index 93b6dcb134..297f7f39eb 100644 --- a/app/client/src/pages/organization/ManageUsers.tsx +++ b/app/client/src/pages/organization/ManageUsers.tsx @@ -7,7 +7,7 @@ import { Classes } from "components/ads/common"; import { useLocation } from "react-router-dom"; const StyledManageUsers = styled("a")` - margin-top: 20px; + margin-top: 12px; display: inline-flex; &&&& { text-decoration: none; diff --git a/app/client/src/pages/organization/OrgInviteUsersForm.tsx b/app/client/src/pages/organization/OrgInviteUsersForm.tsx index ce63e01616..2b65f3feb9 100644 --- a/app/client/src/pages/organization/OrgInviteUsersForm.tsx +++ b/app/client/src/pages/organization/OrgInviteUsersForm.tsx @@ -125,7 +125,7 @@ const UserRole = styled.div` flex-basis: 25%; flex-shrink: 0; .${Classes.TEXT} { - color: ${(props) => props.theme.colors.modal.headerText}; + color: ${Colors.COD_GRAY}; } `; @@ -139,6 +139,14 @@ const UserName = styled.div` &:nth-child(1) { margin-bottom: 1px; } + + &[type="h5"] { + color: ${Colors.COD_GRAY}; + } + + &[type="p2"] { + color: ${Colors.GRAY}; + } } `; @@ -155,8 +163,8 @@ const Loading = styled(Spinner)` const MailConfigContainer = styled.div` display: flex; flex-direction: column; - padding: ${(props) => props.theme.spaces[9]}px - ${(props) => props.theme.spaces[2]}px; + padding: 24px 4px; + padding-bottom: 0; align-items: center; && > span { color: ${(props) => props.theme.colors.modal.email.message}; From cd84b63373c6249f333489ae3eab3cbfccd3bff7 Mon Sep 17 00:00:00 2001 From: Ankita Kinger Date: Tue, 26 Apr 2022 19:51:06 +0530 Subject: [PATCH 08/34] added check for disconenct button clicked once (#13272) --- app/client/cypress/fixtures/githubSource.json | 4 ---- app/client/cypress/support/AdminSettingsCommands.js | 1 - app/client/src/ce/constants/messages.ts | 3 +++ app/client/src/pages/Settings/DisconnectService.tsx | 10 +++++++++- app/client/src/pages/Settings/SettingsForm.tsx | 6 ++++-- 5 files changed, 16 insertions(+), 8 deletions(-) delete mode 100644 app/client/cypress/fixtures/githubSource.json diff --git a/app/client/cypress/fixtures/githubSource.json b/app/client/cypress/fixtures/githubSource.json deleted file mode 100644 index 2b7975ebcc..0000000000 --- a/app/client/cypress/fixtures/githubSource.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "githubClientId": "", - "githubClientSecret": "" -} \ No newline at end of file diff --git a/app/client/cypress/support/AdminSettingsCommands.js b/app/client/cypress/support/AdminSettingsCommands.js index 908dd5dff9..a32f6d094d 100644 --- a/app/client/cypress/support/AdminSettingsCommands.js +++ b/app/client/cypress/support/AdminSettingsCommands.js @@ -7,7 +7,6 @@ require("cypress-file-upload"); const googleForm = require("../locators/GoogleForm.json"); const googleData = require("../fixtures/googleSource.json"); const githubForm = require("../locators/GithubForm.json"); -const githubData = require("../fixtures/githubSource.json"); Cypress.Commands.add("fillGoogleFormPartly", () => { cy.get(googleForm.googleClientId).type( diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index 3c22ada680..8b130a1287 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -999,6 +999,9 @@ export const TEST_EMAIL_SUCCESS = (email: string) => () => `Test email sent, please check the inbox of ${email}`; export const TEST_EMAIL_SUCCESS_TROUBLESHOOT = () => "Troubleshoot"; export const TEST_EMAIL_FAILURE = () => "Sending Test Email Failed"; +export const DISCONNECT_AUTH_ERROR = () => + "Cannot disconnect the only connected authentication method."; +export const MANDATORY_FIELDS_ERROR = () => "Mandatory fields cannot be empty"; //Reflow Beta Screen export const REFLOW_BETA_CHECKBOX_LABEL = () => "Turn on new drag & drop experience"; diff --git a/app/client/src/pages/Settings/DisconnectService.tsx b/app/client/src/pages/Settings/DisconnectService.tsx index 1933746159..a82c195ea9 100644 --- a/app/client/src/pages/Settings/DisconnectService.tsx +++ b/app/client/src/pages/Settings/DisconnectService.tsx @@ -54,6 +54,14 @@ export function DisconnectService(props: { warning: string; }) { const [warnDisconnectAuth, setWarnDisconnectAuth] = useState(false); + const [disconnectCalled, setDisconnectCalled] = useState(false); + + const callDisconnect = () => { + if (!disconnectCalled) { + setDisconnectCalled(true); + props.disconnect(); + } + }; return ( @@ -63,7 +71,7 @@ export function DisconnectService(props: { - warnDisconnectAuth ? props.disconnect() : setWarnDisconnectAuth(true) + warnDisconnectAuth ? callDisconnect() : setWarnDisconnectAuth(true) } text={ warnDisconnectAuth diff --git a/app/client/src/pages/Settings/SettingsForm.tsx b/app/client/src/pages/Settings/SettingsForm.tsx index 6fccd7c6ba..d8e6deeb25 100644 --- a/app/client/src/pages/Settings/SettingsForm.tsx +++ b/app/client/src/pages/Settings/SettingsForm.tsx @@ -25,8 +25,10 @@ import { import { DisconnectService } from "./DisconnectService"; import { createMessage, + DISCONNECT_AUTH_ERROR, DISCONNECT_SERVICE_SUBHEADER, DISCONNECT_SERVICE_WARNING, + MANDATORY_FIELDS_ERROR, } from "@appsmith/constants/messages"; import { Toaster, Variant } from "components/ads"; import { @@ -109,7 +111,7 @@ export function SettingsForm( } } else { Toaster.show({ - text: "Mandatory fields cannot be empty", + text: createMessage(MANDATORY_FIELDS_ERROR), variant: Variant.danger, }); } @@ -160,7 +162,7 @@ export function SettingsForm( const saveBlocked = () => { Toaster.show({ - text: "Cannot disconnect the only connected authentication method.", + text: createMessage(DISCONNECT_AUTH_ERROR), variant: Variant.danger, }); }; From 4dabbd8f13d3ed8ff9d322d2e97217f7be206fe2 Mon Sep 17 00:00:00 2001 From: Arsalan Yaldram Date: Tue, 26 Apr 2022 20:01:23 +0530 Subject: [PATCH 09/34] fix: onSucess & onError action selector () => {} (#12753) --- .../src/components/editorComponents/ActionCreator/index.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/client/src/components/editorComponents/ActionCreator/index.tsx b/app/client/src/components/editorComponents/ActionCreator/index.tsx index b402345d37..aba21c3e3b 100644 --- a/app/client/src/components/editorComponents/ActionCreator/index.tsx +++ b/app/client/src/components/editorComponents/ActionCreator/index.tsx @@ -191,7 +191,7 @@ function getFieldFromValue( const errorArg = args[1] ? args[1][0] : "() => {}"; const successArg = changeValue.endsWith(")") ? `() => ${changeValue}` - : `() => ${changeValue}()`; + : `() => {}`; return value.replace( ACTION_TRIGGER_REGEX, @@ -217,7 +217,8 @@ function getFieldFromValue( const successArg = args[0] ? args[0][0] : "() => {}"; const errorArg = changeValue.endsWith(")") ? `() => ${changeValue}` - : `() => ${changeValue}()`; + : `() => {}`; + return value.replace( ACTION_TRIGGER_REGEX, `{{$1(${successArg}, ${errorArg})}}`, From ea6debab20620ecd4dfeec06676e9fcfe8867cb2 Mon Sep 17 00:00:00 2001 From: Nidhi Date: Tue, 26 Apr 2022 20:03:17 +0530 Subject: [PATCH 10/34] fix: Added contains and not equals options to where clause in Google Sheets (#13208) --- .../constants/ConditionalOperator.java | 6 + .../services/ce/FilterDataServiceCE.java | 134 +++++++++++------- .../services/FilterDataServiceTest.java | 7 +- .../src/main/resources/editor.json | 8 ++ 4 files changed, 105 insertions(+), 50 deletions(-) diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/ConditionalOperator.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/ConditionalOperator.java index 97b0d1f1fc..ca28f8d5ef 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/ConditionalOperator.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/ConditionalOperator.java @@ -73,4 +73,10 @@ public enum ConditionalOperator { return "or"; } }, + CONTAINS { + @Override + public String toString() { + return "like"; + } + } } diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/services/ce/FilterDataServiceCE.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/services/ce/FilterDataServiceCE.java index ca055ad367..ad55fdd818 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/services/ce/FilterDataServiceCE.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/services/ce/FilterDataServiceCE.java @@ -75,6 +75,7 @@ public class FilterDataServiceCE implements IFilterDataServiceCE { ConditionalOperator.NOT_EQ, "<>", ConditionalOperator.GT, ">", ConditionalOperator.GTE, ">=", + ConditionalOperator.CONTAINS, "LIKE", ConditionalOperator.IN, "IN", ConditionalOperator.NOT_IN, "NOT IN" ); @@ -99,6 +100,7 @@ public class FilterDataServiceCE implements IFilterDataServiceCE { * Overloaded Method to handle plugin-based DataType conversion. * Plugins implementing this passes parameter 'dataTypeConversionMap' to instruct how their DataTypes to be processed. * Example: GoogleSheet plugin handled it in a way that Integer, Long and Float DataTypes to be treated as Double. + * * @param items * @param conditionList * @param dataTypeConversionMap - A Map to provide custom Datatype(value) against the actual Datatype(key) found. @@ -137,7 +139,8 @@ public class FilterDataServiceCE implements IFilterDataServiceCE { /** * This filter method is using the new UQI format. - * @param items - data + * + * @param items - data * @param uqiDataFilterParams - filter conditions to apply on data * @return filtered data */ @@ -292,9 +295,9 @@ public class FilterDataServiceCE implements IFilterDataServiceCE { * E.g. if the projectionColumns is a list that contains ["ID, Name"], then this method will add the following * SQL line: `SELECT ID, Name from tableName`, otherwise it will add: `SELECT * FROM tableName` * - * @param sb - SQL query builder + * @param sb - SQL query builder * @param projectionColumns - list of columns that need to be displayed - * @param tableName - table name in database + * @param tableName - table name in database */ private void addProjectionCondition(StringBuilder sb, List projectionColumns, String tableName) { if (!CollectionUtils.isEmpty(projectionColumns)) { @@ -304,8 +307,7 @@ public class FilterDataServiceCE implements IFilterDataServiceCE { sb.setLength(sb.length() - 1); sb.append(" FROM " + tableName); - } - else { + } else { sb.append("SELECT * FROM " + tableName); } } @@ -313,12 +315,12 @@ public class FilterDataServiceCE implements IFilterDataServiceCE { /** * This method adds `ORDER BY` clause to the SQL query. E.g. if the sortBy list is * [ - * {"columnName": "ID", "type": "ASCENDING"}, - * {"columnName": "Name", "type": "DESCENDING"} + * {"columnName": "ID", "type": "ASCENDING"}, + * {"columnName": "Name", "type": "DESCENDING"} * ] * then this method will add the following line to the SQL query: `ORDER BY ID ASC, Name DESC` * - * @param sb - SQL query builder + * @param sb - SQL query builder * @param sortBy - list of columns to sort by and sort type (ascending / descending) * @throws AppsmithPluginException */ @@ -354,8 +356,8 @@ public class FilterDataServiceCE implements IFilterDataServiceCE { /** * Checks if: - * o `sortBy` condition list is null or empty - * o all column names in the sortBy list are empty + * o `sortBy` condition list is null or empty + * o all column names in the sortBy list are empty */ private boolean isSortConditionEmpty(List> sortBy) { if (CollectionUtils.isEmpty(sortBy)) { @@ -368,9 +370,10 @@ public class FilterDataServiceCE implements IFilterDataServiceCE { /** * Filter Query before UQI implementation - * @param tableName - table name in database - * @param conditions - Where Conditions - * @param schema - The Schema + * + * @param tableName - table name in database + * @param conditions - Where Conditions + * @param schema - The Schema * @param dataTypeConversionMap - A Map to provide custom Datatype against the actual Datatype found. * @return */ @@ -459,9 +462,20 @@ public class FilterDataServiceCE implements IFilterDataServiceCE { sb.append(" "); if (value == null || value.equals(StringUtils.EMPTY)) { - if (operator == ConditionalOperator.EQ || operator == ConditionalOperator.IN) { + if (Set.of( + ConditionalOperator.EQ, + ConditionalOperator.IN, + ConditionalOperator.CONTAINS, + ConditionalOperator.LTE, + ConditionalOperator.LT + ).contains(operator)) { sb.append("IS NULL"); - } else if (operator == ConditionalOperator.NOT_IN) { + } else if (Set.of( + ConditionalOperator.NOT_IN, + ConditionalOperator.NOT_EQ, + ConditionalOperator.GTE, + ConditionalOperator.GT + ).contains(operator)) { sb.append("IS NOT NULL"); } isEmptyConditionValue = true; @@ -470,50 +484,62 @@ public class FilterDataServiceCE implements IFilterDataServiceCE { } sb.append(" "); - // These are array operations. Convert value into appropriate format and then append - if (!(value == null || StringUtils.EMPTY.equals(value)) && //value should not be EMPTY or null - (operator == ConditionalOperator.IN || operator == ConditionalOperator.NOT_IN)) { + // value should not be EMPTY or null + if (!isEmptyConditionValue) { + // These are array operations. Convert value into appropriate format and then append + if (operator == ConditionalOperator.IN || operator == ConditionalOperator.NOT_IN) { - StringBuilder valueBuilder = new StringBuilder("("); + StringBuilder valueBuilder = new StringBuilder("("); - try { - List arrayValues = objectMapper.readValue(value, List.class); - List updatedStringValues = arrayValues - .stream() - .map(fieldValue -> { - values.add(new PreparedStatementValueDTO(String.valueOf(fieldValue), schema.get(path))); - return "?"; - }) - .collect(Collectors.toList()); - String finalValues = String.join(",", updatedStringValues); - valueBuilder.append(finalValues); - } catch (IOException e) { - throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, - value + " could not be parsed into an array"); + try { + List arrayValues = objectMapper.readValue(value, List.class); + List updatedStringValues = arrayValues + .stream() + .map(fieldValue -> { + values.add(new PreparedStatementValueDTO(String.valueOf(fieldValue), schema.get(path))); + return "?"; + }) + .collect(Collectors.toList()); + String finalValues = String.join(",", updatedStringValues); + valueBuilder.append(finalValues); + } catch (IOException e) { + throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, + value + " could not be parsed into an array"); + } + + valueBuilder.append(")"); + value = valueBuilder.toString(); + sb.append(value); + + } else if (operator == ConditionalOperator.CONTAINS) { + String escapedLikeValue = value + .replace("!", "!!") + .replace("%", "!%") + .replace("_", "!_") + .replace("[", "!["); + sb.append("? ESCAPE '!'"); + escapedLikeValue = "%" + escapedLikeValue + "%"; + values.add(new PreparedStatementValueDTO(escapedLikeValue, schema.get(path))); + } else { + // Not an array. Simply add a placeholder + sb.append("?"); + values.add(new PreparedStatementValueDTO(value, schema.get(path))); } - - valueBuilder.append(")"); - value = valueBuilder.toString(); - sb.append(value); - - } else if (!isEmptyConditionValue) { - // Not an array. Simply add a placeholder - sb.append("?"); - values.add(new PreparedStatementValueDTO(value, schema.get(path))); } } return sb.toString(); } public void insertAllData(String tableName, ArrayNode items, Map schema) { - insertAllData( tableName, items, schema, null); + insertAllData(tableName, items, schema, null); } /** * Overloaded Method to handle plugin-based DataType conversion. - * @param tableName - table name in database - * @param items - Data - * @param schema - The Schema + * + * @param tableName - table name in database + * @param items - Data + * @param schema - The Schema * @param dataTypeConversionMap - A Map to provide custom Datatype against the actual Datatype found. */ public void insertAllData(String tableName, ArrayNode items, Map schema, Map dataTypeConversionMap) { @@ -714,6 +740,7 @@ public class FilterDataServiceCE implements IFilterDataServiceCE { /** * Overloaded Method to handle plugin-based DataType conversion. + * * @param items * @param dataTypeConversionMap - A Map to provide custom Datatype against the actual Datatype found. * @return @@ -761,7 +788,7 @@ public class FilterDataServiceCE implements IFilterDataServiceCE { } else { DataType foundDataType = stringToKnownDataTypeConverter(value); DataType convertedDataType = foundDataType; - if(name != "rowIndex" && dataTypeConversionMap != null) { + if (name != "rowIndex" && dataTypeConversionMap != null) { convertedDataType = dataTypeConversionMap.getOrDefault(foundDataType, foundDataType); } return convertedDataType; @@ -786,7 +813,7 @@ public class FilterDataServiceCE implements IFilterDataServiceCE { if (!StringUtils.isEmpty(value)) { DataType foundDataType = stringToKnownDataTypeConverter(value); DataType dataType = foundDataType; - if(dataTypeConversionMap != null) { + if (dataTypeConversionMap != null) { dataType = dataTypeConversionMap.getOrDefault(foundDataType, foundDataType); } schema.put(columnName, dataType); @@ -810,6 +837,7 @@ public class FilterDataServiceCE implements IFilterDataServiceCE { /** * Overloaded Method to handle plugin-based DataType conversion. + * * @param preparedStatement * @param index * @param value @@ -825,7 +853,7 @@ public class FilterDataServiceCE implements IFilterDataServiceCE { dataType = dataTypeConversionMap.getOrDefault(topRowDataType, topRowDataType); } - String strNumericValue = value.trim().replaceAll(",",""); + String strNumericValue = value.trim().replaceAll(",", ""); // Override datatype to null for empty values if (StringUtils.isEmpty(value)) { @@ -985,6 +1013,14 @@ public class FilterDataServiceCE implements IFilterDataServiceCE { value = valueBuilder.toString(); sb.append(value); + } else if (operator == ConditionalOperator.CONTAINS) { + final String escapedLikeValue = value + .replace("!", "!!") + .replace("%", "!%") + .replace("_", "!_") + .replace("[", "!["); + sb.append("? ESCAPE '!'"); + values.add(new PreparedStatementValueDTO("%" + escapedLikeValue + "%", schema.get(path))); } else { // Not an array. Simply add a placeholder sb.append("?"); diff --git a/app/server/appsmith-interfaces/src/test/java/com/appsmith/external/services/FilterDataServiceTest.java b/app/server/appsmith-interfaces/src/test/java/com/appsmith/external/services/FilterDataServiceTest.java index 15686b92e7..7677666381 100644 --- a/app/server/appsmith-interfaces/src/test/java/com/appsmith/external/services/FilterDataServiceTest.java +++ b/app/server/appsmith-interfaces/src/test/java/com/appsmith/external/services/FilterDataServiceTest.java @@ -139,10 +139,15 @@ public class FilterDataServiceTest { Condition condition1 = new Condition("anotherKey", "GT", "15"); conditionList.add(condition1); - Condition condition2 = new Condition("orderStatus", "EQ", "READY"); conditionList.add(condition2); + Condition condition3 = new Condition("productName", "CONTAINS", "Chicken"); + conditionList.add(condition3); + + Condition condition4 = new Condition("productName", "NOT_EQ", "Chicken Sub"); + conditionList.add(condition4); + ArrayNode filteredData = filterDataService.filterData(items, conditionList); assertEquals(filteredData.size(), 1); diff --git a/app/server/appsmith-plugins/googleSheetsPlugin/src/main/resources/editor.json b/app/server/appsmith-plugins/googleSheetsPlugin/src/main/resources/editor.json index 15a67177de..c9933e6042 100644 --- a/app/server/appsmith-plugins/googleSheetsPlugin/src/main/resources/editor.json +++ b/app/server/appsmith-plugins/googleSheetsPlugin/src/main/resources/editor.json @@ -402,6 +402,10 @@ "label": "==", "value": "EQ" }, + { + "label": "!=", + "value": "NOT_EQ" + }, { "label": ">=", "value": "GTE" @@ -410,6 +414,10 @@ "label": ">", "value": "GT" }, + { + "label": "contains", + "value": "CONTAINS" + }, { "label": "in", "value": "IN" From 10f83ef69881e386df207678fb88dbd3789ba133 Mon Sep 17 00:00:00 2001 From: Nidhi Date: Tue, 26 Apr 2022 20:04:14 +0530 Subject: [PATCH 11/34] fix: Throw error on trying to access non-existent unpublished action (#13210) * fix: Throw error on trying to access non-existent unpublished action * Fixed NPE at copy policies from page to action --- .../appsmith/server/services/ce/NewActionServiceCEImpl.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/NewActionServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/NewActionServiceCEImpl.java index 853e690700..29f5f4a556 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/NewActionServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/NewActionServiceCEImpl.java @@ -232,6 +232,8 @@ public class NewActionServiceCEImpl extends BaseService documentPolicies = policyGenerator.getAllChildPolicies(page.getPolicies(), Page.class, Action.class); action.setPolicies(documentPolicies); } From c264e1cd89a3478c13a2c3548ab8364bad6948db Mon Sep 17 00:00:00 2001 From: Ayangade Adeoluwa <37867493+Irongade@users.noreply.github.com> Date: Tue, 26 Apr 2022 16:10:59 +0100 Subject: [PATCH 12/34] Add fallback for responseType, in case it does not exist (#13324) --- .../sagas/ActionExecution/PluginActionSaga.ts | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/app/client/src/sagas/ActionExecution/PluginActionSaga.ts b/app/client/src/sagas/ActionExecution/PluginActionSaga.ts index e4cb2bd89b..1d706e4842 100644 --- a/app/client/src/sagas/ActionExecution/PluginActionSaga.ts +++ b/app/client/src/sagas/ActionExecution/PluginActionSaga.ts @@ -105,6 +105,8 @@ import { submitCurlImportForm } from "actions/importActions"; import { getBasePath } from "pages/Editor/Explorer/helpers"; import { isTrueObject } from "workers/evaluationUtils"; import { handleExecuteJSFunctionSaga } from "sagas/JSPaneSagas"; +import { Plugin } from "api/PluginApi"; + enum ActionResponseDataTypes { BINARY = "BINARY", } @@ -835,17 +837,24 @@ function* executePluginActionSaga( response: payload, }), ); - let plugin; + let plugin: Plugin | undefined; if (!!pluginAction.pluginId) { plugin = yield select(getPlugin, pluginAction.pluginId); } - yield put( - setActionResponseDisplayFormat({ - id: actionId, - field: "responseDisplayFormat", - value: plugin && plugin.responseType ? plugin.responseType : "JSON", - }), - ); + + if (!!plugin) { + const responseType = payload?.dataTypes.find( + (type) => + plugin?.responseType && type.dataType === plugin?.responseType, + ); + yield put( + setActionResponseDisplayFormat({ + id: actionId, + field: "responseDisplayFormat", + value: responseType ? responseType?.dataType : "JSON", + }), + ); + } return { payload, isError: isErrorResponse(response), From e73fd0a269270913c4093880ae0ea7482b55c564 Mon Sep 17 00:00:00 2001 From: Nidhi Date: Wed, 27 Apr 2022 08:45:15 +0530 Subject: [PATCH 13/34] fix: Parse nested structures as tables for action execution results (#13328) * fix: Parse nested structures as tables * Updated comments --- .../external/helpers/DataTypeStringUtils.java | 20 +++---- .../helpers/DataTypeStringUtilsTest.java | 56 +++++++++++++++++++ 2 files changed, 65 insertions(+), 11 deletions(-) diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/DataTypeStringUtils.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/DataTypeStringUtils.java index 17dab5ec6e..64b61cc1fa 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/DataTypeStringUtils.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/DataTypeStringUtils.java @@ -310,27 +310,25 @@ public class DataTypeStringUtils { private static boolean isDisplayTypeTable(Object data) { if (data instanceof List) { - // Check if the data is a list of simple json objects i.e. all values in the key value pairs are simple - // objects or their wrappers. - return ((List)data).stream() - .allMatch(item -> item instanceof Map - && ((Map)item).entrySet().stream() - .allMatch(e -> ((Map.Entry)e).getValue() == null || - isPrimitiveOrWrapper(((Map.Entry)e).getValue().getClass()))); + // Check if the data is a list of json objects + return ((List) data).stream() + .allMatch(item -> item instanceof Map); } else if (data instanceof JsonNode) { - // Check if the data is an array of simple json objects + // Check if the data is an array of json objects try { - objectMapper.convertValue(data, new TypeReference>>() {}); + objectMapper.convertValue(data, new TypeReference>>() { + }); return true; } catch (IllegalArgumentException e) { return false; } } else if (data instanceof String) { - // Check if the data is an array of simple json objects + // Check if the data is an array of json objects try { - objectMapper.readValue((String)data, new TypeReference>>() {}); + objectMapper.readValue((String) data, new TypeReference>>() { + }); return true; } catch (IOException e) { return false; diff --git a/app/server/appsmith-interfaces/src/test/java/com/appsmith/external/helpers/DataTypeStringUtilsTest.java b/app/server/appsmith-interfaces/src/test/java/com/appsmith/external/helpers/DataTypeStringUtilsTest.java index a916cad594..23a7a4dc3d 100644 --- a/app/server/appsmith-interfaces/src/test/java/com/appsmith/external/helpers/DataTypeStringUtilsTest.java +++ b/app/server/appsmith-interfaces/src/test/java/com/appsmith/external/helpers/DataTypeStringUtilsTest.java @@ -1,8 +1,19 @@ package com.appsmith.external.helpers; import com.appsmith.external.constants.DataType; +import com.appsmith.external.constants.DisplayDataType; +import com.appsmith.external.models.ParsedDataType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.Test; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.appsmith.external.helpers.DataTypeStringUtils.getDisplayDataTypes; import static com.appsmith.external.helpers.DataTypeStringUtils.stringToKnownDataTypeConverter; import static org.assertj.core.api.Assertions.assertThat; @@ -176,4 +187,49 @@ public class DataTypeStringUtilsTest { assertThat(DataType.JSON_OBJECT).isEqualByComparingTo(stringToKnownDataTypeConverter("{\"a\": \"\"}")); assertThat(DataType.JSON_OBJECT).isEqualByComparingTo(stringToKnownDataTypeConverter("{\"a\": []}")); } + + @Test + public void testGetDisplayDataTypes_withNestedObjectsInList_returnsWithTable() { + + final List data = new ArrayList<>(); + final Map objectMap = new HashMap<>(); + final Map nestedObjectMap = new HashMap<>(); + nestedObjectMap.put("k2", "v2"); + objectMap.put("k", nestedObjectMap); + + data.add(objectMap); + final List displayDataTypes = getDisplayDataTypes(data); + + assertThat(displayDataTypes).anyMatch(parsedDataType -> parsedDataType.getDataType().equals(DisplayDataType.TABLE)); + } + + @Test + public void testGetDisplayDataTypes_withNestedObjectsInArrayNode_returnsWithTable() { + final ObjectMapper objectMapper = new ObjectMapper(); + final ArrayNode data = objectMapper.createArrayNode(); + final ObjectNode objectNode = objectMapper.createObjectNode(); + final ObjectNode nestedObjectNode = objectMapper.createObjectNode(); + nestedObjectNode.put("k2", "v2"); + objectNode.set("k", nestedObjectNode); + + data.add(objectNode); + final List displayDataTypes = getDisplayDataTypes(data); + + assertThat(displayDataTypes).anyMatch(parsedDataType -> parsedDataType.getDataType().equals(DisplayDataType.TABLE)); + } + + @Test + public void testGetDisplayDataTypes_withNestedObjectsInString_returnsWithTable() { + final ObjectMapper objectMapper = new ObjectMapper(); + final ArrayNode data = objectMapper.createArrayNode(); + final ObjectNode objectNode = objectMapper.createObjectNode(); + final ObjectNode nestedObjectNode = objectMapper.createObjectNode(); + nestedObjectNode.put("k2", "v2"); + objectNode.set("k", nestedObjectNode); + + data.add(objectNode); + final List displayDataTypes = getDisplayDataTypes(data.toString()); + + assertThat(displayDataTypes).anyMatch(parsedDataType -> parsedDataType.getDataType().equals(DisplayDataType.TABLE)); + } } From 3b28ce00a26d2f32a32973d5964b6de701bb21aa Mon Sep 17 00:00:00 2001 From: Sumesh Pradhan Date: Wed, 27 Apr 2022 11:33:58 +0530 Subject: [PATCH 14/34] Adding ingress.class parameter to nginx setting in helm chart docs (#12398) --- deploy/helm/Setup-https.md | 1 + 1 file changed, 1 insertion(+) diff --git a/deploy/helm/Setup-https.md b/deploy/helm/Setup-https.md index e86ede1ab0..ded39e1bf0 100644 --- a/deploy/helm/Setup-https.md +++ b/deploy/helm/Setup-https.md @@ -86,6 +86,7 @@ The steps below explain how to use Ingress routes and cert-manager to configure --set ingress.hosts[0].host=DOMAIN \ --set ingress.certManagerTls[0].hosts[0]=DOMAIN \ --set ingress.certManagerTls[0].secretName=letsencrypt-prod + --set ingress.className=nginx ``` After the deployment completes, visit the domain in your browser and you should see the Appsmith site over a secure TLS connection with a valid Let's Encrypt certificate. From 456b5b94ef1f5fc16dbee6d37e68dee07b6abf31 Mon Sep 17 00:00:00 2001 From: Anagh Hegde Date: Wed, 27 Apr 2022 11:45:40 +0530 Subject: [PATCH 15/34] fix: Page order sequence for git import (#13126) * Added page sequence to metadata * Update test resources * Add null check for the pageOrder List * FIx NPE * Add logic to handle page order for deployed version * Add tests for the page order * Add null check for published pages * Fix unpublished page names getting added in the order list for published * update the variable name * Use published page order in tests for published view * Fix NPE --- .../server/domains/ApplicationJson.java | 4 + .../ImportExportApplicationServiceCEImpl.java | 59 ++++++-- .../ImportExportApplicationServiceTests.java | 138 +++++++++++++++++- ...tion-without-pageId-action-collection.json | 4 + .../valid-application-with-custom-themes.json | 4 + .../valid-application-with-page-added.json | 5 + .../valid-application-with-page-removed.json | 3 + ...ication-with-un-configured-datasource.json | 3 + ...application-without-action-collection.json | 4 + .../valid-application-without-theme.json | 4 + .../valid-application.json | 4 + 11 files changed, 216 insertions(+), 16 deletions(-) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ApplicationJson.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ApplicationJson.java index ad9987295a..c08a97522c 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ApplicationJson.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ApplicationJson.java @@ -35,6 +35,10 @@ public class ApplicationJson { List pageList; + List pageOrder; + + List publishedPageOrder; + String publishedDefaultPageName; String unpublishedDefaultPageName; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ImportExportApplicationServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ImportExportApplicationServiceCEImpl.java index 0c65d00ad3..f0ccaa378a 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ImportExportApplicationServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ImportExportApplicationServiceCEImpl.java @@ -77,6 +77,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -233,6 +234,7 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica // Refactor application to remove the ids final String organizationId = application.getOrganizationId(); List pageOrderList = application.getPages().stream().map(applicationPage -> applicationPage.getId()).collect(Collectors.toList()); + List publishedPageOrderList = application.getPublishedPages().stream().map(applicationPage -> applicationPage.getId()).collect(Collectors.toList()); removeUnwantedFieldsFromApplicationDuringExport(application); examplesOrganizationCloner.makePristine(application); applicationJson.setExportedApplication(application); @@ -244,9 +246,24 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica return pageFlux .collectList() - // Maintain the page order while exporting the application + // Save the page order to json while exporting the application .flatMap(newPages -> { Collections.sort(newPages, Comparator.comparing(newPage -> pageOrderList.indexOf(newPage.getId()))); + List pageOrder = new ArrayList<>(); + for (NewPage page: newPages) { + pageOrder.add(page.getUnpublishedPage().getName()); + } + applicationJson.setPageOrder(pageOrder); + + List newPageList = newPages; + // When there is difference in number of pages between edit and view mode + newPageList = newPageList.stream().filter(newPage -> !Optional.ofNullable(newPage.getPublishedPage()).isEmpty()).collect(Collectors.toList()); + Collections.sort(newPageList, Comparator.comparing(newPage -> publishedPageOrderList.indexOf(newPage.getId()))); + pageOrder = new ArrayList<>(); + for (NewPage page: newPageList) { + pageOrder.add(page.getPublishedPage().getName()); + } + applicationJson.setPublishedPageOrder(pageOrder); return Mono.just(newPages); }) .flatMap(newPageList -> { @@ -306,7 +323,7 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica Flux datasourceFlux = Boolean.TRUE.equals(application.getExportWithConfiguration()) ? datasourceRepository.findAllByOrganizationId(organizationId, AclPermission.READ_DATASOURCES) - : datasourceRepository.findAllByOrganizationId(organizationId, AclPermission.MANAGE_DATASOURCES); + : datasourceRepository.findAllByOrganizationId(organizationId, MANAGE_DATASOURCES); return datasourceFlux.collectList(); }) @@ -854,15 +871,17 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica existingPagesMono ).collectList() .map(newPageList -> { - // Check if the pages order match with json file - List pageOrderList = importedNewPageList.stream().map(newPage -> newPage.getUnpublishedPage().getName()).collect(Collectors.toList()); + // Sort the unPublished pages based on the json file order only + List pageOrderList; + if(Optional.ofNullable(applicationJson.getPageOrder()).isEmpty()) { + pageOrderList = importedNewPageList.stream().map(newPage -> newPage.getUnpublishedPage().getName()).collect(Collectors.toList()); + } else { + pageOrderList = applicationJson.getPageOrder(); + } Collections.sort(newPageList, Comparator.comparing(newPage -> pageOrderList.indexOf(newPage.getUnpublishedPage().getName()))); for (NewPage newPage : newPageList) { - ApplicationPage unpublishedAppPage = new ApplicationPage(); - ApplicationPage publishedAppPage = new ApplicationPage(); - if (newPage.getUnpublishedPage() != null && newPage.getUnpublishedPage().getName() != null) { unpublishedAppPage.setIsDefault( StringUtils.equals( @@ -875,6 +894,27 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica } pageNameMap.put(newPage.getUnpublishedPage().getName(), newPage); } + if (unpublishedAppPage.getId() != null && newPage.getUnpublishedPage().getDeletedAt() == null) { + applicationPages.get(PublishType.UNPUBLISHED).add(unpublishedAppPage); + } + } + + // Sort the published pages based on the json file order only + List publishedPageOrderList; + if(Optional.ofNullable(applicationJson.getPublishedPageOrder()).isEmpty()) { + publishedPageOrderList = importedNewPageList.stream() + .filter(newPage -> !Optional.ofNullable(newPage.getPublishedPage()).isEmpty()) + .map(newPage -> newPage.getPublishedPage().getName()).collect(Collectors.toList()); + } else { + publishedPageOrderList = applicationJson.getPublishedPageOrder(); + } + + // When there is difference in number of pages between edit and view mode + newPageList = newPageList.stream().filter(newPage -> !Optional.ofNullable(newPage.getPublishedPage()).isEmpty()).collect(Collectors.toList()); + Collections.sort(newPageList, + Comparator.comparing(newPage -> publishedPageOrderList.indexOf(newPage.getPublishedPage().getName()))); + for (NewPage newPage : newPageList) { + ApplicationPage publishedAppPage = new ApplicationPage(); if (newPage.getPublishedPage() != null && newPage.getPublishedPage().getName() != null) { publishedAppPage.setIsDefault( @@ -888,9 +928,7 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica } pageNameMap.put(newPage.getPublishedPage().getName(), newPage); } - if (unpublishedAppPage.getId() != null && newPage.getUnpublishedPage().getDeletedAt() == null) { - applicationPages.get(PublishType.UNPUBLISHED).add(unpublishedAppPage); - } + if (publishedAppPage.getId() != null && newPage.getPublishedPage().getDeletedAt() == null) { applicationPages.get(PublishType.PUBLISHED).add(publishedAppPage); } @@ -929,6 +967,7 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica }); }) .flatMap(applicationPageMap -> { + // set it based on the order for published and unublished pages importedApplication.setPages(applicationPageMap.get(PublishType.UNPUBLISHED)); importedApplication.setPublishedPages(applicationPageMap.get(PublishType.PUBLISHED)); diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ImportExportApplicationServiceTests.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ImportExportApplicationServiceTests.java index 8f4362ce20..0f720b5f20 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ImportExportApplicationServiceTests.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ImportExportApplicationServiceTests.java @@ -2336,12 +2336,12 @@ public class ImportExportApplicationServiceTests { @Test @WithUserDetails(value = "api_user") - public void exportAndImportApplication_withMultiplePages_PagesOrderIsMaintained() { + public void exportAndImportApplication_withMultiplePagesOrderSameInDeployAndEditMode_PagesOrderIsMaintainedInEditAndViewMode() { Organization newOrganization = new Organization(); newOrganization.setName("template-org-with-ds"); Application testApplication = new Application(); - testApplication.setName("exportApplication_withMultiplePages_PagesOrderIsMaintainedINExportedAppJson"); + testApplication.setName("exportAndImportApplication_withMultiplePagesOrderSameInDeployAndEditMode_PagesOrderIsMaintainedInEditAndViewMode"); testApplication.setExportWithConfiguration(true); testApplication = applicationPageService.createApplication(testApplication, orgId).block(); assert testApplication != null; @@ -2359,22 +2359,30 @@ public class ImportExportApplicationServiceTests { // Set order for the newly created pages applicationPageService.reorderPage(testApplication.getId(), page1.getId(), 0, null).block(); applicationPageService.reorderPage(testApplication.getId(), page2.getId(), 1, null).block(); + // Deploy the current application + applicationPageService.publish(testApplication.getId(), true).block(); Mono applicationJsonMono = importExportApplicationService.exportApplicationById(testApplication.getId(), ""); StepVerifier .create(applicationJsonMono) .assertNext(applicationJson -> { - List pageList = applicationJson.getPageList(); - assertThat(pageList.get(0).getUnpublishedPage().getName()).isEqualTo("123"); - assertThat(pageList.get(1).getUnpublishedPage().getName()).isEqualTo("abc"); - assertThat(pageList.get(2).getUnpublishedPage().getName()).isEqualTo("Page1"); + List pageList = applicationJson.getPageOrder(); + assertThat(pageList.get(0)).isEqualTo("123"); + assertThat(pageList.get(1)).isEqualTo("abc"); + assertThat(pageList.get(2)).isEqualTo("Page1"); + + List publishedPageList = applicationJson.getPublishedPageOrder(); + assertThat(publishedPageList.get(0)).isEqualTo("123"); + assertThat(publishedPageList.get(1)).isEqualTo("abc"); + assertThat(publishedPageList.get(2)).isEqualTo("Page1"); }) .verifyComplete(); ApplicationJson applicationJson = importExportApplicationService.exportApplicationById(testApplication.getId(), "").block(); Application application = importExportApplicationService.importApplicationInOrganization(orgId, applicationJson).block(); + // Get the unpublished pages and verify the order List pageDTOS = application.getPages(); Mono newPageMono1 = newPageService.findById(pageDTOS.get(0).getId(), MANAGE_PAGES); Mono newPageMono2 = newPageService.findById(pageDTOS.get(1).getId(), MANAGE_PAGES); @@ -2396,6 +2404,124 @@ public class ImportExportApplicationServiceTests { }) .verifyComplete(); + // Get the published pages + List publishedPageDTOs = application.getPublishedPages(); + Mono newPublishedPageMono1 = newPageService.findById(publishedPageDTOs.get(0).getId(), MANAGE_PAGES); + Mono newPublishedPageMono2 = newPageService.findById(publishedPageDTOs.get(1).getId(), MANAGE_PAGES); + Mono newPublishedPageMono3 = newPageService.findById(publishedPageDTOs.get(2).getId(), MANAGE_PAGES); + + StepVerifier + .create(Mono.zip(newPublishedPageMono1, newPublishedPageMono2, newPublishedPageMono3)) + .assertNext(objects -> { + NewPage newPage1 = objects.getT1(); + NewPage newPage2 = objects.getT2(); + NewPage newPage3 = objects.getT3(); + assertThat(newPage1.getPublishedPage().getName()).isEqualTo("123"); + assertThat(newPage2.getPublishedPage().getName()).isEqualTo("abc"); + assertThat(newPage3.getPublishedPage().getName()).isEqualTo("Page1"); + + assertThat(newPage1.getId()).isEqualTo(publishedPageDTOs.get(0).getId()); + assertThat(newPage2.getId()).isEqualTo(publishedPageDTOs.get(1).getId()); + assertThat(newPage3.getId()).isEqualTo(publishedPageDTOs.get(2).getId()); + }) + .verifyComplete(); + + } + + + @Test + @WithUserDetails(value = "api_user") + public void exportAndImportApplication_withMultiplePagesOrderDifferentInDeployAndEditMode_PagesOrderIsMaintainedInEditAndViewMode() { + Organization newOrganization = new Organization(); + newOrganization.setName("template-org-with-ds"); + + Application testApplication = new Application(); + testApplication.setName("exportAndImportApplication_withMultiplePagesOrderDifferentInDeployAndEditMode_PagesOrderIsMaintainedInEditAndViewMode"); + testApplication.setExportWithConfiguration(true); + testApplication = applicationPageService.createApplication(testApplication, orgId).block(); + assert testApplication != null; + + PageDTO testPage = new PageDTO(); + testPage.setName("123"); + testPage.setApplicationId(testApplication.getId()); + PageDTO page1 = applicationPageService.createPage(testPage).block(); + + testPage = new PageDTO(); + testPage.setName("abc"); + testPage.setApplicationId(testApplication.getId()); + PageDTO page2 = applicationPageService.createPage(testPage).block(); + + // Deploy the current application so that edit and view mode will have different page order + applicationPageService.publish(testApplication.getId(), true).block(); + + // Set order for the newly created pages + applicationPageService.reorderPage(testApplication.getId(), page1.getId(), 0, null).block(); + applicationPageService.reorderPage(testApplication.getId(), page2.getId(), 1, null).block(); + + Mono applicationJsonMono = importExportApplicationService.exportApplicationById(testApplication.getId(), ""); + + StepVerifier + .create(applicationJsonMono) + .assertNext(applicationJson -> { + List pageList = applicationJson.getPageOrder(); + assertThat(pageList.get(0)).isEqualTo("123"); + assertThat(pageList.get(1)).isEqualTo("abc"); + assertThat(pageList.get(2)).isEqualTo("Page1"); + + List publishedPageOrder = applicationJson.getPageOrder(); + assertThat(publishedPageOrder.get(0)).isEqualTo("123"); + assertThat(publishedPageOrder.get(1)).isEqualTo("abc"); + assertThat(publishedPageOrder.get(2)).isEqualTo("Page1"); + }) + .verifyComplete(); + + ApplicationJson applicationJson = importExportApplicationService.exportApplicationById(testApplication.getId(), "").block(); + Application application = importExportApplicationService.importApplicationInOrganization(orgId, applicationJson).block(); + + // Get the unpublished pages and verify the order + List pageDTOS = application.getPages(); + Mono newPageMono1 = newPageService.findById(pageDTOS.get(0).getId(), MANAGE_PAGES); + Mono newPageMono2 = newPageService.findById(pageDTOS.get(1).getId(), MANAGE_PAGES); + Mono newPageMono3 = newPageService.findById(pageDTOS.get(2).getId(), MANAGE_PAGES); + + StepVerifier + .create(Mono.zip(newPageMono1, newPageMono2, newPageMono3)) + .assertNext(objects -> { + NewPage newPage1 = objects.getT1(); + NewPage newPage2 = objects.getT2(); + NewPage newPage3 = objects.getT3(); + assertThat(newPage1.getUnpublishedPage().getName()).isEqualTo("123"); + assertThat(newPage2.getUnpublishedPage().getName()).isEqualTo("abc"); + assertThat(newPage3.getUnpublishedPage().getName()).isEqualTo("Page1"); + + assertThat(newPage1.getId()).isEqualTo(pageDTOS.get(0).getId()); + assertThat(newPage2.getId()).isEqualTo(pageDTOS.get(1).getId()); + assertThat(newPage3.getId()).isEqualTo(pageDTOS.get(2).getId()); + }) + .verifyComplete(); + + // Get the published pages + List publishedPageDTOs = application.getPublishedPages(); + Mono newPublishedPageMono1 = newPageService.findById(publishedPageDTOs.get(0).getId(), MANAGE_PAGES); + Mono newPublishedPageMono2 = newPageService.findById(publishedPageDTOs.get(1).getId(), MANAGE_PAGES); + Mono newPublishedPageMono3 = newPageService.findById(publishedPageDTOs.get(2).getId(), MANAGE_PAGES); + + StepVerifier + .create(Mono.zip(newPublishedPageMono1, newPublishedPageMono2, newPublishedPageMono3)) + .assertNext(objects -> { + NewPage newPage1 = objects.getT1(); + NewPage newPage2 = objects.getT2(); + NewPage newPage3 = objects.getT3(); + assertThat(newPage1.getPublishedPage().getName()).isEqualTo("Page1"); + assertThat(newPage2.getPublishedPage().getName()).isEqualTo("123"); + assertThat(newPage3.getPublishedPage().getName()).isEqualTo("abc"); + + assertThat(newPage1.getId()).isEqualTo(publishedPageDTOs.get(0).getId()); + assertThat(newPage2.getId()).isEqualTo(publishedPageDTOs.get(1).getId()); + assertThat(newPage3.getId()).isEqualTo(publishedPageDTOs.get(2).getId()); + }) + .verifyComplete(); + } } diff --git a/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/invalid-application-without-pageId-action-collection.json b/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/invalid-application-without-pageId-action-collection.json index 358adbad51..5b4e5d9909 100644 --- a/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/invalid-application-without-pageId-action-collection.json +++ b/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/invalid-application-without-pageId-action-collection.json @@ -380,6 +380,10 @@ "new": true } ], + "pageOrder": [ + "Page1", + "Page2" + ], "actionList": [ { "id": "60aca092136c4b7178f6790a", diff --git a/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/valid-application-with-custom-themes.json b/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/valid-application-with-custom-themes.json index 092bc8846e..82793f2fcb 100644 --- a/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/valid-application-with-custom-themes.json +++ b/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/valid-application-with-custom-themes.json @@ -421,6 +421,10 @@ "new": true } ], + "pageOrder": [ + "Page1", + "Page2" + ], "actionList": [ { "id": "60aca092136c4b7178f6790a", diff --git a/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/valid-application-with-page-added.json b/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/valid-application-with-page-added.json index 5cfdc8aa66..fc9e101e93 100644 --- a/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/valid-application-with-page-added.json +++ b/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/valid-application-with-page-added.json @@ -541,6 +541,11 @@ "new": true } ], + "pageOrder": [ + "Page1", + "Page2", + "Page3" + ], "publishedDefaultPageName": "Page1", "unpublishedDefaultPageName": "Page1", "actionList": [ diff --git a/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/valid-application-with-page-removed.json b/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/valid-application-with-page-removed.json index 3591104636..a7f76d16ab 100644 --- a/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/valid-application-with-page-removed.json +++ b/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/valid-application-with-page-removed.json @@ -253,6 +253,9 @@ "new": true } ], + "pageOrder": [ + "importedPage" + ], "publishedDefaultPageName": "importedPage", "unpublishedDefaultPageName": "importedPage", "actionList": [], diff --git a/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/valid-application-with-un-configured-datasource.json b/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/valid-application-with-un-configured-datasource.json index ac8f1c1e1f..22b4d4f04d 100644 --- a/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/valid-application-with-un-configured-datasource.json +++ b/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/valid-application-with-un-configured-datasource.json @@ -280,6 +280,9 @@ "new": true } ], + "pageOrder": [ + "Page1" + ], "publishedDefaultPageName": "Page1", "unpublishedDefaultPageName": "Page1", "actionList": [ diff --git a/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/valid-application-without-action-collection.json b/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/valid-application-without-action-collection.json index 7b9f5fe662..df53c8f2ba 100644 --- a/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/valid-application-without-action-collection.json +++ b/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/valid-application-without-action-collection.json @@ -421,6 +421,10 @@ "new": true } ], + "pageOrder": [ + "Page1", + "Page2" + ], "actionList": [ { "id": "60aca092136c4b7178f6790a", diff --git a/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/valid-application-without-theme.json b/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/valid-application-without-theme.json index 42705c64e8..d36510772b 100644 --- a/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/valid-application-without-theme.json +++ b/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/valid-application-without-theme.json @@ -421,6 +421,10 @@ "new": true } ], + "pageOrder": [ + "Page1", + "Page2" + ], "actionList": [ { "id": "60aca092136c4b7178f6790a", diff --git a/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/valid-application.json b/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/valid-application.json index fbfe7109a4..92bb277cca 100644 --- a/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/valid-application.json +++ b/app/server/appsmith-server/src/test/resources/test_assets/ImportExportServiceTest/valid-application.json @@ -421,6 +421,10 @@ "new": true } ], + "pageOrder": [ + "Page1", + "Page2" + ], "actionList": [ { "id": "60aca092136c4b7178f6790a", From 7b09976f525278b36b44fe0ec9512dab6b132794 Mon Sep 17 00:00:00 2001 From: haojin111 <63215848+haojin111@users.noreply.github.com> Date: Wed, 27 Apr 2022 14:37:04 +0800 Subject: [PATCH 16/34] feat: 12457 - added application import success modal (#12739) * added application import success modal * updated logic and added cypress test * fixed cypress test * added local storage for refresh * fixed issue - 11721 git import issue * fixed cypress test with new designd modal * fixed cypress test failed issue --- .../Application/AForceMigration_Spec.ts | 32 +++-- .../Application/ReconnectDatasource_spec.js | 24 +++- .../GitImport/GitImport_spec.js | 11 +- .../cypress/locators/ReconnectLocators.js | 2 + .../cypress/support/Pages/DataSources.ts | 4 + app/client/src/ce/constants/messages.ts | 4 + .../gitSync/ImportedAppSuccessModal.tsx | 117 ++++++++++++++++++ .../gitSync/ReconnectDatasourceModal.tsx | 29 ++++- app/client/src/pages/Editor/index.tsx | 2 + 9 files changed, 205 insertions(+), 20 deletions(-) create mode 100644 app/client/src/pages/Editor/gitSync/ImportedAppSuccessModal.tsx diff --git a/app/client/cypress/integration/Smoke_TestSuite/Application/AForceMigration_Spec.ts b/app/client/cypress/integration/Smoke_TestSuite/Application/AForceMigration_Spec.ts index 504250e2c0..1b89612643 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Application/AForceMigration_Spec.ts +++ b/app/client/cypress/integration/Smoke_TestSuite/Application/AForceMigration_Spec.ts @@ -25,17 +25,27 @@ describe("AForce - Community Issues page validations", function () { let reconnect = true, selectedRow: number; it("1. Import application json and validate headers", () => { - - homePage.ImportApp("AForceMigrationExport.json", reconnect) - if (reconnect) - dataSources.ReconnectDataSourcePostgres("AForceDB") - //Validate table is not empty! - table.WaitUntilTableLoad() - //Validating order of header columns! - table.AssertTableHeaderOrder("TypeTitleStatus+1CommentorsVotesAnswerUpVoteStatesupvote_ididgithub_issue_idauthorcreated_atdescriptionlabelsstatelinkupdated_at") - //Validating hidden columns: - table.AssertHiddenColumns(['States', 'upvote_id', 'id', 'github_issue_id', 'author', 'created_at', 'description', 'labels', 'state', 'link', 'updated_at']) - + cy.visit("/applications"); + homePage.ImportApp("AForceMigrationExport.json", reconnect); + cy.wait("@importNewApplication").then((interception) => { + cy.wait(100); + const { isPartialImport } = interception.response.body.data; + if (isPartialImport) { + // should reconnect modal + dataSources.ReconnectDataSourcePostgres("AForceDB") + } else { + cy.get(homePage.toastMessage).should( + "contain", + "Application imported successfully", + ); + } + //Validate table is not empty! + table.WaitUntilTableLoad() + //Validating order of header columns! + table.AssertTableHeaderOrder("TypeTitleStatus+1CommentorsVotesAnswerUpVoteStatesupvote_ididgithub_issue_idauthorcreated_atdescriptionlabelsstatelinkupdated_at") + //Validating hidden columns: + table.AssertHiddenColumns(['States', 'upvote_id', 'id', 'github_issue_id', 'author', 'created_at', 'description', 'labels', 'state', 'link', 'updated_at']) + }); }); it("2. Validate table navigation with Server Side pagination enabled with Default selected row", () => { diff --git a/app/client/cypress/integration/Smoke_TestSuite/Application/ReconnectDatasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Application/ReconnectDatasource_spec.js index 64af819493..ebbd089f93 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Application/ReconnectDatasource_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/Application/ReconnectDatasource_spec.js @@ -6,7 +6,7 @@ describe("Reconnect Datasource Modal validation while importing application", fu let appid; let newOrganizationName; let appName; - it("Import application from json with one postgres", function() { + it("Import application from json with one postgres and success modal", function() { cy.NavigateToHome(); // import application cy.generateUUID().then((uid) => { @@ -49,16 +49,32 @@ describe("Reconnect Datasource Modal validation while importing application", fu cy.get( "[data-cy='datasourceConfiguration.connection.ssl.authType']", ).should("contain", "Default"); - cy.get(reconnectDatasourceModal.SkipToAppBtn).click({ - force: true, - }); + + cy.ReconnectDatasource("Untitled Datasource"); + cy.wait(1000); + cy.fillPostgresDatasourceForm(); + cy.testSaveDatasource(); cy.wait(2000); + + // cy.get(reconnectDatasourceModal.SkipToAppBtn).click({ + // force: true, + // }); + // cy.wait(2000); } else { cy.get(homePage.toastMessage).should( "contain", "Application imported successfully", ); } + // check datasource configured success modal + cy.get(".t--import-app-success-modal").should("be.visible"); + cy.get(".t--import-app-success-modal").should( + "contain", + "All your datasources are configuered and ready to use.", + ); + cy.get(".t--import-success-modal-got-it").click({ force: true }); + cy.get(".t--import-app-success-modal").should("not.exist"); + const uuid = () => Cypress._.random(0, 1e4); const name = uuid(); appName = `app${name}`; diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/GitImport/GitImport_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/GitImport/GitImport_spec.js index fbc0a962e5..e9e5b580ad 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/GitImport/GitImport_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/GitImport/GitImport_spec.js @@ -21,7 +21,7 @@ describe("Git import flow", function() { }); }); it("Import an app from JSON with Postgres, MySQL, Mongo db", () => { - cy.get(homePage.homeIcon).click(); + cy.NavigateToHome(); cy.get(homePage.optionsIcon) .first() .click(); @@ -54,6 +54,10 @@ describe("Git import flow", function() { "contain", "Application imported successfully", ); */ + cy.get(reconnectDatasourceModal.ImportSuccessModal).should("be.visible"); + cy.get(reconnectDatasourceModal.ImportSuccessModalCloseBtn).click({ + force: true, + }); cy.wait(1000); cy.generateUUID().then((uid) => { repoName = uid; @@ -98,6 +102,11 @@ describe("Git import flow", function() { cy.get(datasourceEditor.sectionAuthentication).click(); cy.testSaveDatasource(); cy.wait(2000); + cy.get(reconnectDatasourceModal.ImportSuccessModal).should("be.visible"); + cy.get(reconnectDatasourceModal.ImportSuccessModalCloseBtn).click({ + force: true, + }); + cy.wait(1000); /* cy.get(homePage.toastMessage).should( "contain", "Application imported successfully", diff --git a/app/client/cypress/locators/ReconnectLocators.js b/app/client/cypress/locators/ReconnectLocators.js index 71e9d06619..0f36420058 100644 --- a/app/client/cypress/locators/ReconnectLocators.js +++ b/app/client/cypress/locators/ReconnectLocators.js @@ -2,4 +2,6 @@ export default { Modal: ".reconnect-datasource-modal", ClostBtn: ".t--reconnect-close-btn", SkipToAppBtn: ".t--skip-to-application-btn", + ImportSuccessModal: ".t--import-app-success-modal", + ImportSuccessModalCloseBtn: ".t--import-success-modal-got-it", }; diff --git a/app/client/cypress/support/Pages/DataSources.ts b/app/client/cypress/support/Pages/DataSources.ts index 978456f258..84e31df9a7 100644 --- a/app/client/cypress/support/Pages/DataSources.ts +++ b/app/client/cypress/support/Pages/DataSources.ts @@ -20,6 +20,8 @@ export class DataSources { private _datasourceCard = ".t--datasource" _templateMenu = ".t--template-menu" private _createQuery = ".t--create-query" + private _importSuccessModal = ".t--import-app-success-modal" + private _importSuccessModalClose = ".t--import-success-modal-got-it" _visibleTextSpan = (spanText: string) => "//span[contains(text(),'" + spanText + "')]" _dropdownTitle = (ddTitle: string) => "//p[contains(text(),'" + ddTitle + "')]/parent::label/following-sibling::div/div/div" _reconnectModal = "div.reconnect-datasource-modal" @@ -112,6 +114,8 @@ export class DataSources { this.ValidateNSelectDropdown("Connection Mode", "", "Read / Write") this.FillPostgresDSForm() cy.get(this._saveDs).click(); + cy.get(this._importSuccessModal).should("be.visible"); + cy.get(this._importSuccessModalClose).click({ force: true }); } } \ No newline at end of file diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index 8b130a1287..6a74d7269f 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -688,6 +688,10 @@ export const CONTACT_SALES_MESSAGE_ON_INTERCOM = (orgName: string) => export const REPOSITORY_LIMIT_REACHED = () => "Repository Limit Reached"; export const REPOSITORY_LIMIT_REACHED_INFO = () => "Adding and using upto 3 repositories is free. To add more repositories kindly upgrade."; +export const APPLICATION_IMPORT_SUCCESS = (username: string) => + `${username}! Your application is ready to use.`; +export const APPLICATION_IMPORT_SUCCESS_DESCRIPTION = () => + "All your datasources are configuered and ready to use."; export const NONE_REVERSIBLE_MESSAGE = () => "This action is non reversible. Proceed with caution."; export const CONTACT_SUPPORT_TO_UPGRADE = () => diff --git a/app/client/src/pages/Editor/gitSync/ImportedAppSuccessModal.tsx b/app/client/src/pages/Editor/gitSync/ImportedAppSuccessModal.tsx new file mode 100644 index 0000000000..22c7ae93bc --- /dev/null +++ b/app/client/src/pages/Editor/gitSync/ImportedAppSuccessModal.tsx @@ -0,0 +1,117 @@ +import React, { useState } from "react"; +import { useSelector } from "react-redux"; +import Dialog from "components/ads/DialogComponent"; +import styled, { useTheme } from "styled-components"; +import Text, { TextType } from "components/ads/Text"; +import { Colors } from "constants/Colors"; +import { + createMessage, + APPLICATION_IMPORT_SUCCESS, + APPLICATION_IMPORT_SUCCESS_DESCRIPTION, +} from "@appsmith/constants/messages"; +import Icon from "components/ads/Icon"; +import { Theme } from "constants/DefaultTheme"; +import { getCurrentUser } from "selectors/usersSelectors"; +import { Button, Category, Size } from "components/ads"; + +const Container = styled.div` + height: 461px; + width: 100%; + display: flex; + flex-direction: column; + position: relative; + overflow-y: hidden; + justify-content: center; + align-content: center; +`; + +const BodyContainer = styled.div` + display: flex; + flex-direction: column; + .cs-icon { + margin: auto; + svg { + width: 68px; + height: 68px; + } + } + + .cs-text { + text-align: center; + } +`; + +const ActionButtonWrapper = styled.div` + display: flex; + justify-content: center; + margin: 30px 0px 0px; + position: absolute; + width: 100%; + bottom: 0px; +`; + +const ActionButton = styled(Button)` + margin-right: 16px; +`; + +function ImportedApplicationSuccessModal() { + const importedAppSuccess = localStorage.getItem("importApplicationSuccess"); + // const isOpen = importedAppSuccess === "true"; + const [isOpen, setIsOpen] = useState(importedAppSuccess === "true"); + const currentUser = useSelector(getCurrentUser); + + const onClose = () => { + setIsOpen(false); + localStorage.setItem("importApplicationSuccess", "false"); + }; + const theme = useTheme() as Theme; + return ( + + + + + + {createMessage( + APPLICATION_IMPORT_SUCCESS, + currentUser?.name || currentUser?.username, + )} + + + {createMessage(APPLICATION_IMPORT_SUCCESS_DESCRIPTION)} + + + { + onClose(); + }} + size={Size.medium} + text="GOT IT" + /> + + + + + ); +} + +export default ImportedApplicationSuccessModal; diff --git a/app/client/src/pages/Editor/gitSync/ReconnectDatasourceModal.tsx b/app/client/src/pages/Editor/gitSync/ReconnectDatasourceModal.tsx index 9400deeee7..68f7f7a10d 100644 --- a/app/client/src/pages/Editor/gitSync/ReconnectDatasourceModal.tsx +++ b/app/client/src/pages/Editor/gitSync/ReconnectDatasourceModal.tsx @@ -59,6 +59,7 @@ import { Toaster, Variant } from "components/ads"; import { getOAuthAccessToken } from "actions/datasourceActions"; import { builderURL } from "RouteBuilder"; import { PLACEHOLDER_APP_SLUG } from "constants/routes"; +import localStorage from "utils/localStorage"; const Container = styled.div` height: 765px; @@ -276,13 +277,22 @@ function ReconnectDatasourceModal() { const isDatasourceTesting = useSelector(getIsDatasourceTesting); const isDatasourceUpdating = useSelector(getDatasourceLoading); + // checking refresh modal + const pendingApp = JSON.parse( + localStorage.getItem("importedAppPendingInfo") || "null", + ); // getting query from redirection url const userOrgs = useSelector(getUserApplicationsOrgsList); const queryParams = useQuery(); - const queryAppId = queryParams.get("appId"); - const queryPageId = queryParams.get("pageId"); - const queryDatasourceId = queryParams.get("datasourceId"); - const queryIsImport = JSON.parse(queryParams.get("importForGit") ?? "false"); + const queryAppId = + queryParams.get("appId") || (pendingApp ? pendingApp.appId : null); + const queryPageId = + queryParams.get("pageId") || (pendingApp ? pendingApp.pageId : null); + const queryDatasourceId = + queryParams.get("datasourceId") || + (pendingApp ? pendingApp.datasourceId : null); + const queryIsImport = + queryParams.get("importForGit") === "true" || !!pendingApp; const [selectedDatasourceId, setSelectedDatasourceId] = useState< string | null @@ -471,7 +481,17 @@ function ReconnectDatasourceModal() { next = next || pending[0]; setSelectedDatasourceId(next.id); setDatasource(next); + // when refresh, it should be opened. + const appInfo = { + appId: appId, + pageId: pageId, + datasourceId: next.id, + }; + localStorage.setItem("importedAppPendingInfo", JSON.stringify(appInfo)); } else if (appURL) { + // open application import successfule + localStorage.setItem("importApplicationSuccess", "true"); + localStorage.setItem("importedAppPendingInfo", "null"); window.open(appURL, "_self"); } } @@ -557,6 +577,7 @@ function ReconnectDatasourceModal() { AnalyticsUtil.logEvent( "RECONNECTING_SKIP_TO_APPLICATION_BUTTON_CLICK", ); + localStorage.setItem("importedAppPendingInfo", "null"); }} size={Size.medium} text={createMessage(SKIP_TO_APPLICATION)} diff --git a/app/client/src/pages/Editor/index.tsx b/app/client/src/pages/Editor/index.tsx index c4f6c76c67..3348fb4acf 100644 --- a/app/client/src/pages/Editor/index.tsx +++ b/app/client/src/pages/Editor/index.tsx @@ -52,6 +52,7 @@ import { loading } from "selectors/onboardingSelectors"; import GuidedTourModal from "./GuidedTour/DeviationModal"; import { getPageLevelSocketRoomId } from "sagas/WebsocketSagas/utils"; import RepoLimitExceededErrorModal from "./gitSync/RepoLimitExceededErrorModal"; +import ImportedApplicationSuccessModal from "./gitSync/ImportedAppSuccessModal"; type EditorProps = { currentApplicationId?: string; @@ -229,6 +230,7 @@ class Editor extends Component { + From 0eef3b0bfdb39b3bd6503792416efa1ad3c26777 Mon Sep 17 00:00:00 2001 From: Maulik Date: Wed, 27 Apr 2022 12:37:33 +0530 Subject: [PATCH 17/34] fix: code hidden issue for JS Enabled fields (#13241) * fix code hidden issue for JS Enabled fields * test for code hidden fix --- .../PropertyPaneJsEnabledVisible_spec.js | 42 +++++++++++++++++++ .../editorComponents/CodeEditor/index.tsx | 1 + 2 files changed, 43 insertions(+) create mode 100644 app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/PropertyPane/PropertyPaneJsEnabledVisible_spec.js diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/PropertyPane/PropertyPaneJsEnabledVisible_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/PropertyPane/PropertyPaneJsEnabledVisible_spec.js new file mode 100644 index 0000000000..fbe4890b31 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/PropertyPane/PropertyPaneJsEnabledVisible_spec.js @@ -0,0 +1,42 @@ +const dsl = require("../../../../fixtures/jsonFormDslWithSchema.json"); +const { ObjectsRegistry } = require("../../../../support/Objects/Registry"); +let ee = ObjectsRegistry.EntityExplorer; + +describe("Property pane js enabled field", function() { + before(() => { + cy.addDsl(dsl); + }); + + it("Ensure text is visible for js enabled field when a section is collapsed by default", function() { + cy.openPropertyPane("jsonformwidget"); + + cy.get(".t--property-pane-section-collapse-submitbuttonstyles").click(); + cy.get(".t--property-control-buttonvariant") + .find(".t--js-toggle") + .first() + .click(); + + cy.get(".t--property-control-buttonvariant") + .find(".t--js-toggle") + .first() + .should("have.class", "is-active"); + + cy.get(".t--property-control-buttonvariant .CodeMirror-code").type( + "PRIMARY", + ); + cy.get(".t--property-control-buttonvariant") + .find(".CodeMirror-code") + .invoke("text") + .should("equal", "PRIMARY"); + + cy.closePropertyPane(); + cy.wait(1000); + + cy.openPropertyPane("jsonformwidget"); + cy.get(".t--property-pane-section-collapse-submitbuttonstyles").click(); + cy.get(".t--property-control-buttonvariant") + .find(".CodeMirror-code") + .invoke("text") + .should("equal", "PRIMARY"); + }); +}); diff --git a/app/client/src/components/editorComponents/CodeEditor/index.tsx b/app/client/src/components/editorComponents/CodeEditor/index.tsx index d3e40792aa..dabae61106 100644 --- a/app/client/src/components/editorComponents/CodeEditor/index.tsx +++ b/app/client/src/components/editorComponents/CodeEditor/index.tsx @@ -198,6 +198,7 @@ class CodeEditor extends Component { componentDidMount(): void { if (this.codeEditorTarget.current) { const options: EditorConfiguration = { + autoRefresh: true, mode: this.props.mode, theme: EditorThemes[this.props.theme], viewportMargin: 10, From 63c6c75073363119100c13c0d36b4decebc8e769 Mon Sep 17 00:00:00 2001 From: Shrikant Sharat Kandula Date: Wed, 27 Apr 2022 12:57:57 +0530 Subject: [PATCH 18/34] Disable uid/gid bits for fat container (#13277) --- Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index 8a7196860d..ce92e1505f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -88,6 +88,9 @@ RUN chmod 0644 /etc/cron.d/* RUN chmod +x entrypoint.sh renew-certificate.sh +# Disable setuid/setgid bits for the files inside container. +RUN find / \( -path /proc -prune \) -o \( \( -perm -2000 -o -perm -4000 \) -print -exec chmod -s '{}' + \) || true + # Update path to load appsmith utils tool as default ENV PATH /opt/appsmith/utils/node_modules/.bin:$PATH From fc36a3af94350aa8b07f8cf82e881c532431bd80 Mon Sep 17 00:00:00 2001 From: ashit-rath Date: Wed, 27 Apr 2022 14:04:15 +0530 Subject: [PATCH 19/34] feat: Analytics to log source data limit exceed (#13339) * feat: Analytics to log source data limit exceed * minor change --- .../src/widgets/JSONFormWidget/widget/helper.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/client/src/widgets/JSONFormWidget/widget/helper.ts b/app/client/src/widgets/JSONFormWidget/widget/helper.ts index 1db2661149..a5b64a7082 100644 --- a/app/client/src/widgets/JSONFormWidget/widget/helper.ts +++ b/app/client/src/widgets/JSONFormWidget/widget/helper.ts @@ -1,6 +1,7 @@ import equal from "fast-deep-equal/es6"; import { difference, isEmpty } from "lodash"; import log from "loglevel"; +import AnalyticsUtil from "utils/AnalyticsUtil"; import { isDynamicValue } from "utils/DynamicBindingUtils"; import { MetaInternalFieldState } from "."; @@ -264,6 +265,17 @@ export const computeSchema = ({ const count = countFields(currSourceData); if (count > MAX_ALLOWED_FIELDS) { + AnalyticsUtil.logEvent("WIDGET_PROPERTY_UPDATE", { + widgetType: "JSON_FORM_WIDGET", + widgetName, + propertyName: "sourceData", + updatedValue: currSourceData, + metaInfo: { + limitExceeded: true, + currentLimit: MAX_ALLOWED_FIELDS, + }, + }); + return { status: ComputedSchemaStatus.LIMIT_EXCEEDED, schema: prevSchema, From 5a126bfda212d47d33d33c953c1f731303965831 Mon Sep 17 00:00:00 2001 From: Ayangade Adeoluwa <37867493+Irongade@users.noreply.github.com> Date: Wed, 27 Apr 2022 12:42:29 +0100 Subject: [PATCH 20/34] Add space to both sides of response run button (#13368) --- .../components/editorComponents/ApiResponseView.tsx | 4 ++-- .../src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx | 10 ++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/app/client/src/components/editorComponents/ApiResponseView.tsx b/app/client/src/components/editorComponents/ApiResponseView.tsx index d8cf7acec0..416b091d52 100644 --- a/app/client/src/components/editorComponents/ApiResponseView.tsx +++ b/app/client/src/components/editorComponents/ApiResponseView.tsx @@ -153,9 +153,9 @@ const StyledCallout = styled(Callout)` } `; -const InlineButton = styled(Button)` +export const InlineButton = styled(Button)` display: inline-flex; - margin: 0 4px; + margin: 0 8px; `; const HelpSection = styled.div` diff --git a/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx b/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx index b9b0977d82..7a652b3be7 100644 --- a/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx +++ b/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx @@ -79,7 +79,10 @@ import { ConditionalOutput, FormEvalOutput, } from "reducers/evaluationReducers/formEvaluationReducer"; -import { responseTabComponent } from "components/editorComponents/ApiResponseView"; +import { + responseTabComponent, + InlineButton, +} from "components/editorComponents/ApiResponseView"; const QueryFormContainer = styled.form` flex: 1; @@ -362,11 +365,6 @@ const TabContainerView = styled.div` position: relative; `; -const InlineButton = styled(Button)` - display: inline-flex; - margin: 0 4px; -`; - const Wrapper = styled.div` display: flex; flex-direction: row; From 727889434c74fcf37cf585ab5ba325e4db0f57d5 Mon Sep 17 00:00:00 2001 From: rahulramesha <71900764+rahulramesha@users.noreply.github.com> Date: Wed, 27 Apr 2022 21:13:29 +0530 Subject: [PATCH 21/34] fix: calculating the canvas container height to be exact to not scroll on `scrollIntoView()` call (#13096) * canvas container Fix * add comments --- .../src/pages/Editor/WidgetsEditor/CanvasContainer.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/client/src/pages/Editor/WidgetsEditor/CanvasContainer.tsx b/app/client/src/pages/Editor/WidgetsEditor/CanvasContainer.tsx index 98bbbfd791..e37ed7dd59 100644 --- a/app/client/src/pages/Editor/WidgetsEditor/CanvasContainer.tsx +++ b/app/client/src/pages/Editor/WidgetsEditor/CanvasContainer.tsx @@ -17,6 +17,7 @@ import { useParams } from "react-router"; import classNames from "classnames"; import { forceOpenWidgetPanel } from "actions/widgetSidebarActions"; import { useDispatch } from "react-redux"; +import { getCurrentThemeDetails } from "selectors/themeSelectors"; const Container = styled.section` width: 100%; @@ -38,6 +39,7 @@ function CanvasContainer() { const isFetchingPage = useSelector(getIsFetchingPage); const widgets = useSelector(getCanvasWidgetDsl); const pages = useSelector(getViewModePageList); + const theme = useSelector(getCurrentThemeDetails); const isPreviewMode = useSelector(previewModeSelector); const params = useParams<{ applicationId: string; pageId: string }>(); const shouldHaveTopMargin = !isPreviewMode || pages.length > 1; @@ -63,7 +65,9 @@ function CanvasContainer() { if (!isFetchingPage && widgets) { node = ; } - + // calculating exact height to not allow scroll at this component, + // calculating total height minus margin on top, top bar and bottom bar + const heightWithTopMargin = `calc(100vh - 2.25rem - ${theme.smallHeaderHeight} - ${theme.bottomBarHeight})`; return ( {node} From 251ad6097b3298017ae216a96f2b9ee52ec56816 Mon Sep 17 00:00:00 2001 From: Ankita Kinger Date: Wed, 27 Apr 2022 23:16:24 +0530 Subject: [PATCH 22/34] chore: Add analytic events on Authentication page (#13384) * added analytic events for Authentication page on Admin settings * updated event names --- .../config/authentication/AuthPage.tsx | 36 ++++++--- .../ce/pages/AdminSettings/config/types.ts | 2 + .../components/ads/formFields/SelectField.tsx | 77 +++++++++++++++++++ .../src/pages/Settings/FormGroup/Common.tsx | 19 +++-- .../src/pages/Settings/FormGroup/Dropdown.tsx | 28 +++++++ .../src/pages/Settings/FormGroup/group.tsx | 16 ++++ .../src/pages/Settings/SettingsForm.tsx | 13 ++++ app/client/src/utils/AnalyticsUtil.tsx | 6 ++ 8 files changed, 177 insertions(+), 20 deletions(-) create mode 100644 app/client/src/components/ads/formFields/SelectField.tsx create mode 100644 app/client/src/pages/Settings/FormGroup/Dropdown.tsx diff --git a/app/client/src/ce/pages/AdminSettings/config/authentication/AuthPage.tsx b/app/client/src/ce/pages/AdminSettings/config/authentication/AuthPage.tsx index 13667db915..1f37dd8916 100644 --- a/app/client/src/ce/pages/AdminSettings/config/authentication/AuthPage.tsx +++ b/app/client/src/ce/pages/AdminSettings/config/authentication/AuthPage.tsx @@ -23,6 +23,7 @@ import Icon from "components/ads/Icon"; import TooltipComponent from "components/ads/Tooltip"; import { Position } from "@blueprintjs/core"; import { adminSettingsCategoryUrl } from "RouteBuilder"; +import AnalyticsUtil from "utils/AnalyticsUtil"; const { intercomAppID } = getAppsmithConfigs(); @@ -149,6 +150,30 @@ export function AuthPage({ authMethods }: { authMethods: AuthMethodType[] }) { } }; + const onClickHandler = (method: AuthMethodType) => { + if (!method.needsUpgrade || method.isConnected) { + AnalyticsUtil.logEvent( + method.isConnected + ? "ADMIN_SETTINGS_EDIT_AUTH_METHOD" + : "ADMIN_SETTINGS_ENABLE_AUTH_METHOD", + { + method: method.label, + }, + ); + history.push( + adminSettingsCategoryUrl({ + category: SettingCategories.AUTHENTICATION, + subCategory: method.category, + }), + ); + } else { + AnalyticsUtil.logEvent("ADMIN_SETTINGS_UPGRADE_AUTH_METHOD", { + method: method.label, + }); + triggerIntercom(method.label); + } + }; + return ( @@ -211,16 +236,7 @@ export function AuthPage({ authMethods }: { authMethods: AuthMethodType[] }) { : method.category }`} data-cy="btn-auth-account" - onClick={() => - !method.needsUpgrade || method.isConnected - ? history.push( - adminSettingsCategoryUrl({ - category: SettingCategories.AUTHENTICATION, - subCategory: method.category, - }), - ) - : triggerIntercom(method.label) - } + onClick={() => onClickHandler(method)} text={createMessage( method.isConnected ? EDIT diff --git a/app/client/src/ce/pages/AdminSettings/config/types.ts b/app/client/src/ce/pages/AdminSettings/config/types.ts index b3464eae48..dc5c2ded7b 100644 --- a/app/client/src/ce/pages/AdminSettings/config/types.ts +++ b/app/client/src/ce/pages/AdminSettings/config/types.ts @@ -13,6 +13,7 @@ export enum SettingTypes { UNEDITABLEFIELD = "UNEDITABLEFIELD", ACCORDION = "ACCORDION", TAGINPUT = "TAGINPUT", + DROPDOWN = "DROPDOWN", } export enum SettingSubtype { @@ -52,6 +53,7 @@ export interface Setting { isRequired?: boolean; formName?: string; fieldName?: string; + dropdownOptions?: Array<{ id: string; value: string; label?: string }>; } export interface Category { diff --git a/app/client/src/components/ads/formFields/SelectField.tsx b/app/client/src/components/ads/formFields/SelectField.tsx new file mode 100644 index 0000000000..4c6694cb5b --- /dev/null +++ b/app/client/src/components/ads/formFields/SelectField.tsx @@ -0,0 +1,77 @@ +import React, { useEffect, useState } from "react"; +import { + Field, + WrappedFieldMetaProps, + WrappedFieldInputProps, +} from "redux-form"; +import Dropdown from "components/ads/Dropdown"; + +type DropdownWrapperProps = { + placeholder: string; + input?: { + value?: string; + onChange?: (value?: string) => void; + }; + options: Array<{ id: string; value: string; label?: string }>; + fillOptions?: boolean; +}; + +function DropdownWrapper(props: DropdownWrapperProps) { + const [selectedOption, setSelectedOption] = useState({ + value: props.placeholder, + }); + const onSelectHandler = (value?: string) => { + props.input && props.input.onChange && props.input.onChange(value); + }; + + useEffect(() => { + if (props.input && props.input.value) { + setSelectedOption({ value: props.input.value }); + } else if (props.placeholder) { + setSelectedOption({ value: props.placeholder }); + } + }, [props.input, props.placeholder]); + + return ( + + ); +} + +const renderComponent = ( + componentProps: SelectFieldProps & { + meta: Partial; + input: Partial; + }, +) => { + return ; +}; + +type SelectFieldProps = { + name: string; + placeholder: string; + options: Array<{ id: string; value: string; label?: string }>; + size?: "large" | "small"; + outline?: boolean; + fillOptions?: boolean; +}; + +export function SelectField(props: SelectFieldProps) { + return ( + + ); +} + +export default SelectField; diff --git a/app/client/src/pages/Settings/FormGroup/Common.tsx b/app/client/src/pages/Settings/FormGroup/Common.tsx index b595c6faa2..c4adfa199b 100644 --- a/app/client/src/pages/Settings/FormGroup/Common.tsx +++ b/app/client/src/pages/Settings/FormGroup/Common.tsx @@ -20,16 +20,15 @@ const StyledIcon = styled(Icon)` export const StyledFormGroup = styled.div` width: 40rem; margin-bottom: ${(props) => props.theme.spaces[7]}px; - & span.bp3-popover-target { - display: inline-block; - background: ${(props) => props.theme.colors.menuItem.normalIcon}; - border-radius: ${(props) => props.theme.radii[2]}px; - width: 14px; - padding: 3px 3px; - position: relative; - top: -2px; - left: 6px; - cursor: default; + &.t--admin-settings-dropdown { + div { + width: 100%; + &:hover { + &:hover { + background-color: ${(props) => props.theme.colors.textInput.hover.bg}; + } + } + } } & svg:hover { cursor: default; diff --git a/app/client/src/pages/Settings/FormGroup/Dropdown.tsx b/app/client/src/pages/Settings/FormGroup/Dropdown.tsx new file mode 100644 index 0000000000..d05293ec7c --- /dev/null +++ b/app/client/src/pages/Settings/FormGroup/Dropdown.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { FormGroup, SettingComponentProps } from "./Common"; +import SelectField from "components/ads/formFields/SelectField"; + +export default function DropDown( + props: { + dropdownOptions: Array<{ id: string; value: string; label?: string }>; + } & SettingComponentProps, +) { + const { dropdownOptions, setting } = props; + + return ( + + + + ); +} diff --git a/app/client/src/pages/Settings/FormGroup/group.tsx b/app/client/src/pages/Settings/FormGroup/group.tsx index 0c8d0eca13..b7137f183a 100644 --- a/app/client/src/pages/Settings/FormGroup/group.tsx +++ b/app/client/src/pages/Settings/FormGroup/group.tsx @@ -20,6 +20,7 @@ import { Callout } from "components/ads/CalloutV2"; import { CopyUrlReduxForm } from "components/ads/formFields/CopyUrlForm"; import Accordion from "./Accordion"; import TagInputField from "./TagInputField"; +import Dropdown from "./Dropdown"; import { Classes } from "@blueprintjs/core"; import { Colors } from "constants/Colors"; @@ -228,6 +229,21 @@ export default function Group({ /> ); + case SettingTypes.DROPDOWN: + return ( +
+ {setting.dropdownOptions && ( + + )} +
+ ); } })} diff --git a/app/client/src/pages/Settings/SettingsForm.tsx b/app/client/src/pages/Settings/SettingsForm.tsx index d8e6deeb25..39e39edbea 100644 --- a/app/client/src/pages/Settings/SettingsForm.tsx +++ b/app/client/src/pages/Settings/SettingsForm.tsx @@ -35,6 +35,7 @@ import { connectedMethods, saveAllowed, } from "@appsmith/utils/adminSettingsHelpers"; +import AnalyticsUtil from "utils/AnalyticsUtil"; const Wrapper = styled.div` flex-basis: calc(100% - ${(props) => props.theme.homePage.leftPane.width}px); @@ -105,11 +106,20 @@ export function SettingsForm( const onSave = () => { if (checkMandatoryFileds()) { if (saveAllowed(props.settings)) { + AnalyticsUtil.logEvent("ADMIN_SETTINGS_SAVE", { + method: pageTitle, + }); dispatch(saveSettings(props.settings)); } else { + AnalyticsUtil.logEvent("ADMIN_SETTINGS_ERROR", { + error: createMessage(DISCONNECT_AUTH_ERROR), + }); saveBlocked(); } } else { + AnalyticsUtil.logEvent("ADMIN_SETTINGS_ERROR", { + error: createMessage(MANDATORY_FIELDS_ERROR), + }); Toaster.show({ text: createMessage(MANDATORY_FIELDS_ERROR), variant: Variant.danger, @@ -176,6 +186,9 @@ export function SettingsForm( } }); dispatch(saveSettings(updatedSettings)); + AnalyticsUtil.logEvent("ADMIN_SETTINGS_DISCONNECT_AUTH_METHOD", { + method: pageTitle, + }); } else { saveBlocked(); } diff --git a/app/client/src/utils/AnalyticsUtil.tsx b/app/client/src/utils/AnalyticsUtil.tsx index e35206329a..ef3e2861e6 100644 --- a/app/client/src/utils/AnalyticsUtil.tsx +++ b/app/client/src/utils/AnalyticsUtil.tsx @@ -199,6 +199,12 @@ export type EventName = | "GS_REGENERATE_SSH_KEY_CONFIRM_CLICK" | "GS_REGENERATE_SSH_KEY_MORE_CLICK" | "GS_SWITCH_BRANCH" + | "ADMIN_SETTINGS_SAVE" + | "ADMIN_SETTINGS_ERROR" + | "ADMIN_SETTINGS_DISCONNECT_AUTH_METHOD" + | "ADMIN_SETTINGS_UPGRADE_AUTH_METHOD" + | "ADMIN_SETTINGS_EDIT_AUTH_METHOD" + | "ADMIN_SETTINGS_ENABLE_AUTH_METHOD" | "REFLOW_BETA_FLAG" | "CONTAINER_JUMP" | "CONNECT_GIT_CLICK" From 02d4af3ed7c059c143277f62b79954d551b7b578 Mon Sep 17 00:00:00 2001 From: Favour Ohanekwu Date: Wed, 27 Apr 2022 11:28:52 -0700 Subject: [PATCH 23/34] prevent multiple execution of pageload actions when generating a template (#13361) --- .../ClientSideTests/GenerateCRUD/Mongo_Spec.js | 4 ++++ app/client/src/sagas/PageSagas.tsx | 1 + 2 files changed, 5 insertions(+) diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/GenerateCRUD/Mongo_Spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/GenerateCRUD/Mongo_Spec.js index 027b0f6b73..a25dd4dba4 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/GenerateCRUD/Mongo_Spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/GenerateCRUD/Mongo_Spec.js @@ -2,6 +2,7 @@ const pages = require("../../../../locators/Pages.json"); const generatePage = require("../../../../locators/GeneratePage.json"); import homePage from "../../../../locators/HomePage"; const datasource = require("../../../../locators/DatasourcesEditor.json"); +const commonlocators = require("../../../../locators/commonlocators.json"); describe("Generate New CRUD Page Inside from Mongo as Data Source", function() { let datasourceName; @@ -91,6 +92,9 @@ describe("Generate New CRUD Page Inside from Mongo as Data Source", function() { "response.body.responseMeta.status", 200, ); + cy.get(commonlocators.toastAction) + .should("have.length", 1) + .should("have.text", "Successfully generated a page"); cy.get("span:contains('GOT IT')").click(); }); diff --git a/app/client/src/sagas/PageSagas.tsx b/app/client/src/sagas/PageSagas.tsx index 638be8dbac..a6613dd060 100644 --- a/app/client/src/sagas/PageSagas.tsx +++ b/app/client/src/sagas/PageSagas.tsx @@ -976,6 +976,7 @@ export function* generateTemplatePageSaga( responseMeta: response.responseMeta, }, pageId, + isFirstLoad: true, }); // TODO : Add this to onSuccess (Redux Action) From 234fd33229c27934f162b01c9e523419e128c0c1 Mon Sep 17 00:00:00 2001 From: yatinappsmith <84702014+yatinappsmith@users.noreply.github.com> Date: Thu, 28 Apr 2022 10:23:40 +0530 Subject: [PATCH 24/34] Changed custom actions cache version to 3.0.2 (#13388) We use github actions cache to store jobstate for the previous runs. The default cache is invalidated incase the job fails. To support rerunning only failed tests, we use custom cache martijnhols/actions-cache@v3. Recently new changes into custome cache broke our workflow https://github.com/MartijnHols/actions-cache. Co-authored-by: Yatin --- .github/workflows/TestReuseActions.yml | 2 +- .github/workflows/integration-tests-command.yml | 4 ++-- .github/workflows/test-build-docker-image-fat.yml | 4 ++-- .github/workflows/test-build-docker-image.yml | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/TestReuseActions.yml b/.github/workflows/TestReuseActions.yml index 6a895c3213..91552700ea 100644 --- a/.github/workflows/TestReuseActions.yml +++ b/.github/workflows/TestReuseActions.yml @@ -459,7 +459,7 @@ jobs: # In case this is second attempt try restoring status of the prior attempt from cache - name: Restore the previous run result - uses: martijnhols/actions-cache@v3 + uses: martijnhols/actions-cache@v3.0.2 with: path: | ~/run_result diff --git a/.github/workflows/integration-tests-command.yml b/.github/workflows/integration-tests-command.yml index ce614b761e..c2cfda72fa 100644 --- a/.github/workflows/integration-tests-command.yml +++ b/.github/workflows/integration-tests-command.yml @@ -471,7 +471,7 @@ jobs: # In case this is second attempt try restoring status of the prior attempt from cache - name: Restore the previous run result - uses: martijnhols/actions-cache@v3 + uses: martijnhols/actions-cache@v3.0.2 with: path: | ~/run_result @@ -806,7 +806,7 @@ jobs: # In case this is second attempt try restoring status of the prior attempt from cache - name: Restore the previous run result - uses: martijnhols/actions-cache@v3 + uses: martijnhols/actions-cache@v3.0.2 with: path: | ~/run_result diff --git a/.github/workflows/test-build-docker-image-fat.yml b/.github/workflows/test-build-docker-image-fat.yml index e246704e26..737c8d5f60 100644 --- a/.github/workflows/test-build-docker-image-fat.yml +++ b/.github/workflows/test-build-docker-image-fat.yml @@ -446,7 +446,7 @@ jobs: # In case this is second attempt try restoring status of the prior attempt from cache - name: Restore the previous run result - uses: martijnhols/actions-cache@v3 + uses: martijnhols/actions-cache@v3.0.2 with: path: | ~/run_result @@ -785,7 +785,7 @@ jobs: # In case this is second attempt try restoring status of the prior attempt from cache - name: Restore the previous run result - uses: martijnhols/actions-cache@v3 + uses: martijnhols/actions-cache@v3.0.2 with: path: | ~/run_result diff --git a/.github/workflows/test-build-docker-image.yml b/.github/workflows/test-build-docker-image.yml index d63ac1321d..2df7952157 100644 --- a/.github/workflows/test-build-docker-image.yml +++ b/.github/workflows/test-build-docker-image.yml @@ -455,7 +455,7 @@ jobs: # In case this is second attempt try restoring status of the prior attempt from cache - name: Restore the previous run result - uses: martijnhols/actions-cache@v3 + uses: martijnhols/actions-cache@v3.0.2 with: path: | ~/run_result @@ -829,7 +829,7 @@ jobs: # In case this is second attempt try restoring status of the prior attempt from cache - name: Restore the previous run result - uses: martijnhols/actions-cache@v3 + uses: martijnhols/actions-cache@v3.0.2 with: path: | ~/run_result From 892c76257e0d957b672ee81a4c1b05c769effeb8 Mon Sep 17 00:00:00 2001 From: Hetu Nandu Date: Thu, 28 Apr 2022 14:26:03 +0530 Subject: [PATCH 25/34] Fix integration test action (#13409) --- .github/workflows/integration-tests-command.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/integration-tests-command.yml b/.github/workflows/integration-tests-command.yml index c2cfda72fa..3bd45b936f 100644 --- a/.github/workflows/integration-tests-command.yml +++ b/.github/workflows/integration-tests-command.yml @@ -1218,12 +1218,11 @@ jobs: summary: "https://github.com/" + process.env.repository + "/actions/runs/" + process.env.run_id } }); + console.log({ result }); + return result; } catch(e) { console.error({ error: e.message }); } - - console.log({ result }); - return result; } - name: Dump the client payload context From 3835e81105d1a822fab79776f086bd6b48ebb350 Mon Sep 17 00:00:00 2001 From: Ankita Kinger Date: Thu, 28 Apr 2022 14:46:38 +0530 Subject: [PATCH 26/34] chore: Add analytic event for the Reset button click on the Admin settings page. (#13408) --- app/client/src/pages/Settings/SettingsForm.tsx | 13 +++++++++---- app/client/src/utils/AnalyticsUtil.tsx | 1 + 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/client/src/pages/Settings/SettingsForm.tsx b/app/client/src/pages/Settings/SettingsForm.tsx index 39e39edbea..56b1b82a64 100644 --- a/app/client/src/pages/Settings/SettingsForm.tsx +++ b/app/client/src/pages/Settings/SettingsForm.tsx @@ -111,9 +111,6 @@ export function SettingsForm( }); dispatch(saveSettings(props.settings)); } else { - AnalyticsUtil.logEvent("ADMIN_SETTINGS_ERROR", { - error: createMessage(DISCONNECT_AUTH_ERROR), - }); saveBlocked(); } } else { @@ -150,7 +147,12 @@ export function SettingsForm( return !(requiredFields.length > 0); }; - const onClear = () => { + const onClear = (event?: React.FocusEvent) => { + if (event?.type === "click") { + AnalyticsUtil.logEvent("ADMIN_SETTINGS_RESET", { + method: pageTitle, + }); + } _.forEach(props.settingsConfig, (value, settingName) => { const setting = AdminConfig.settingsMap[settingName]; if (setting && setting.controlType == SettingTypes.TOGGLE) { @@ -171,6 +173,9 @@ export function SettingsForm( }, []); const saveBlocked = () => { + AnalyticsUtil.logEvent("ADMIN_SETTINGS_ERROR", { + error: createMessage(DISCONNECT_AUTH_ERROR), + }); Toaster.show({ text: createMessage(DISCONNECT_AUTH_ERROR), variant: Variant.danger, diff --git a/app/client/src/utils/AnalyticsUtil.tsx b/app/client/src/utils/AnalyticsUtil.tsx index ef3e2861e6..faafd3e8a6 100644 --- a/app/client/src/utils/AnalyticsUtil.tsx +++ b/app/client/src/utils/AnalyticsUtil.tsx @@ -199,6 +199,7 @@ export type EventName = | "GS_REGENERATE_SSH_KEY_CONFIRM_CLICK" | "GS_REGENERATE_SSH_KEY_MORE_CLICK" | "GS_SWITCH_BRANCH" + | "ADMIN_SETTINGS_RESET" | "ADMIN_SETTINGS_SAVE" | "ADMIN_SETTINGS_ERROR" | "ADMIN_SETTINGS_DISCONNECT_AUTH_METHOD" From 6052600d0b870c72c817c3d3357a494c42908030 Mon Sep 17 00:00:00 2001 From: Pawan Kumar Date: Thu, 28 Apr 2022 15:58:51 +0530 Subject: [PATCH 27/34] update theming config --- .../src/main/resources/system-themes.json | 49 +++++++++---------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/app/server/appsmith-server/src/main/resources/system-themes.json b/app/server/appsmith-server/src/main/resources/system-themes.json index 7e319c30ae..304bff0b7d 100644 --- a/app/server/appsmith-server/src/main/resources/system-themes.json +++ b/app/server/appsmith-server/src/main/resources/system-themes.json @@ -4,7 +4,7 @@ "displayName": "Classic", "config": { "colors": { - "primaryColor": "#50AF6C", + "primaryColor": "#03B365", "backgroundColor": "#F6F6F6" }, "borderRadius": { @@ -50,7 +50,6 @@ }, "BUTTON_GROUP_WIDGET": { "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", - "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", "boxShadow": "none", "childStylesheet": { "button": { @@ -100,7 +99,6 @@ "FILE_PICKER_WIDGET_V2": { "buttonColor": "{{appsmith.theme.colors.primaryColor}}", "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", - "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", "boxShadow": "none" }, "FORM_WIDGET": { @@ -119,24 +117,22 @@ }, "IFRAME_WIDGET": { "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", - "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", "boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}" }, "IMAGE_WIDGET": { "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", - "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", "boxShadow": "none" }, "INPUT_WIDGET": { "accentColor": "{{appsmith.theme.colors.primaryColor}}", "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", - "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "boxShadow": "none" }, "INPUT_WIDGET_V2": { "accentColor": "{{appsmith.theme.colors.primaryColor}}", "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", - "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "boxShadow": "none" }, "JSON_FORM_WIDGET": { @@ -338,6 +334,7 @@ "boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}" }, "TEXT_WIDGET": { + "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" }, "VIDEO_WIDGET": { @@ -352,7 +349,7 @@ }, "properties": { "colors": { - "primaryColor": "#50AF6C", + "primaryColor": "#03B365", "backgroundColor": "#F6F6F6" }, "borderRadius": { @@ -417,7 +414,7 @@ }, "BUTTON_GROUP_WIDGET": { "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", - "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "boxShadow": "none", "childStylesheet": { "button": { @@ -467,7 +464,7 @@ "FILE_PICKER_WIDGET_V2": { "buttonColor": "{{appsmith.theme.colors.primaryColor}}", "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", - "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "boxShadow": "none" }, "FORM_WIDGET": { @@ -486,24 +483,24 @@ }, "IFRAME_WIDGET": { "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", - "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}" }, "IMAGE_WIDGET": { "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", - "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "boxShadow": "none" }, "INPUT_WIDGET": { "accentColor": "{{appsmith.theme.colors.primaryColor}}", "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", - "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "boxShadow": "none" }, "INPUT_WIDGET_V2": { "accentColor": "{{appsmith.theme.colors.primaryColor}}", "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", - "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "boxShadow": "none" }, "JSON_FORM_WIDGET": { @@ -705,6 +702,7 @@ "boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}" }, "TEXT_WIDGET": { + "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" }, "VIDEO_WIDGET": { @@ -784,7 +782,7 @@ }, "BUTTON_GROUP_WIDGET": { "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", - "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "boxShadow": "none", "childStylesheet": { "button": { @@ -834,7 +832,7 @@ "FILE_PICKER_WIDGET_V2": { "buttonColor": "{{appsmith.theme.colors.primaryColor}}", "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", - "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "boxShadow": "none" }, "FORM_WIDGET": { @@ -853,24 +851,24 @@ }, "IFRAME_WIDGET": { "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", - "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}" }, "IMAGE_WIDGET": { "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", - "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "boxShadow": "none" }, "INPUT_WIDGET": { "accentColor": "{{appsmith.theme.colors.primaryColor}}", "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", - "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "boxShadow": "none" }, "INPUT_WIDGET_V2": { "accentColor": "{{appsmith.theme.colors.primaryColor}}", "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", - "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "boxShadow": "none" }, "JSON_FORM_WIDGET": { @@ -1072,7 +1070,8 @@ "boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}" }, "TEXT_WIDGET": { - "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" + "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", + "fontFamily": "{{appsmith.theme.fontFamily.appFont}}" }, "VIDEO_WIDGET": { "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", @@ -1151,7 +1150,7 @@ }, "BUTTON_GROUP_WIDGET": { "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", - "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", + "boxShadow": "none", "childStylesheet": { "button": { @@ -1201,7 +1200,6 @@ "FILE_PICKER_WIDGET_V2": { "buttonColor": "{{appsmith.theme.colors.primaryColor}}", "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", - "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", "boxShadow": "none" }, "FORM_WIDGET": { @@ -1220,24 +1218,20 @@ }, "IFRAME_WIDGET": { "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", - "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", "boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}" }, "IMAGE_WIDGET": { "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", - "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", "boxShadow": "none" }, "INPUT_WIDGET": { "accentColor": "{{appsmith.theme.colors.primaryColor}}", "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", - "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", "boxShadow": "none" }, "INPUT_WIDGET_V2": { "accentColor": "{{appsmith.theme.colors.primaryColor}}", "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}", - "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", "boxShadow": "none" }, "JSON_FORM_WIDGET": { @@ -1439,6 +1433,7 @@ "boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}" }, "TEXT_WIDGET": { + "fontFamily": "{{appsmith.theme.fontFamily.appFont}}", "borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}" }, "VIDEO_WIDGET": { From 0aec29d837037e519f9f8bac2942babcb45ad283 Mon Sep 17 00:00:00 2001 From: Pawan Kumar Date: Thu, 28 Apr 2022 16:00:03 +0530 Subject: [PATCH 28/34] update theming config --- .../appsmith-server/src/main/resources/system-themes.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/server/appsmith-server/src/main/resources/system-themes.json b/app/server/appsmith-server/src/main/resources/system-themes.json index 304bff0b7d..32569bd72d 100644 --- a/app/server/appsmith-server/src/main/resources/system-themes.json +++ b/app/server/appsmith-server/src/main/resources/system-themes.json @@ -4,7 +4,7 @@ "displayName": "Classic", "config": { "colors": { - "primaryColor": "#03B365", + "primaryColor": "#16a34a", "backgroundColor": "#F6F6F6" }, "borderRadius": { @@ -349,7 +349,7 @@ }, "properties": { "colors": { - "primaryColor": "#03B365", + "primaryColor": "#16a34a", "backgroundColor": "#F6F6F6" }, "borderRadius": { From 2c484d55388f941c199cab0f08ecbe405a7ec44a Mon Sep 17 00:00:00 2001 From: Anagh Hegde Date: Thu, 28 Apr 2022 16:43:53 +0530 Subject: [PATCH 29/34] fix: Unable to see apps in home page after git connect fails (#13387) --- .../ce/ApplicationFetcherCEImpl.java | 10 +- .../solutions/ApplicationFetcherUnitTest.java | 146 ++++++++++++++++++ 2 files changed, 154 insertions(+), 2 deletions(-) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ApplicationFetcherCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ApplicationFetcherCEImpl.java index 15d8e40288..0f86730f82 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ApplicationFetcherCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ApplicationFetcherCEImpl.java @@ -112,9 +112,15 @@ public class ApplicationFetcherCEImpl implements ApplicationFetcherCE { // Collect all the applications as a map with organization id as a key Flux applicationFlux = applicationRepository .findByMultipleOrganizationIds(orgIds, READ_APPLICATIONS) + // Git connected apps will have gitApplicationMetadat .filter(application -> application.getGitApplicationMetadata() == null - || (!StringUtils.isEmpty(application.getGitApplicationMetadata().getDefaultBranchName()) - && application.getGitApplicationMetadata().getBranchName().equals(application.getGitApplicationMetadata().getDefaultBranchName())) + // 1. When the ssh key is generated by user and then the connect app fails + || (StringUtils.isEmpty(application.getGitApplicationMetadata().getDefaultBranchName()) + && StringUtils.isEmpty(application.getGitApplicationMetadata().getBranchName())) + // 2. When the DefaultBranchName is missing due to branch creation flow failures or corrupted scenarios + || (!StringUtils.isEmpty(application.getGitApplicationMetadata().getBranchName()) + && application.getGitApplicationMetadata().getBranchName().equals(application.getGitApplicationMetadata().getDefaultBranchName()) + ) ) .map(responseUtils::updateApplicationWithDefaultResources); diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ApplicationFetcherUnitTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ApplicationFetcherUnitTest.java index a81f655cd6..fa42f4a5b7 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ApplicationFetcherUnitTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ApplicationFetcherUnitTest.java @@ -2,14 +2,18 @@ package com.appsmith.server.solutions; import com.appsmith.server.domains.Application; import com.appsmith.server.domains.ApplicationPage; +import com.appsmith.server.domains.GitApplicationMetadata; +import com.appsmith.server.domains.GitAuth; import com.appsmith.server.domains.NewPage; import com.appsmith.server.domains.Organization; import com.appsmith.server.domains.User; import com.appsmith.server.domains.UserData; import com.appsmith.server.dtos.OrganizationApplicationsDTO; import com.appsmith.server.dtos.PageDTO; +import com.appsmith.server.dtos.UserHomepageDTO; import com.appsmith.server.helpers.ResponseUtils; import com.appsmith.server.repositories.ApplicationRepository; +import com.appsmith.server.services.ApplicationService; import com.appsmith.server.services.NewPageService; import com.appsmith.server.services.OrganizationService; import com.appsmith.server.services.SessionUserService; @@ -18,7 +22,9 @@ import com.appsmith.server.services.UserService; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.junit4.SpringRunner; import reactor.core.publisher.Flux; @@ -65,6 +71,9 @@ public class ApplicationFetcherUnitTest { ApplicationFetcher applicationFetcher; + @MockBean + ApplicationService applicationService; + User testUser; final static String defaultPageId = "defaultPageId"; @@ -194,6 +203,9 @@ public class ApplicationFetcherUnitTest { .thenReturn(updateDefaultPageIdsWithinApplication(application)); } + Mockito.when(applicationService.createOrUpdateSshKeyPair(Mockito.anyString())) + .thenReturn(Mono.just(new GitAuth())); + StepVerifier.create(applicationFetcher.getAllApplications()) .assertNext(userHomepageDTO -> { List dtos = userHomepageDTO.getOrganizationApplications(); @@ -213,6 +225,140 @@ public class ApplicationFetcherUnitTest { }).verifyComplete(); } + @Test + public void getAllApplications_gitConnectedAppScenarios_OnlyTheDefaultBranchedAppIsReturned() { + initMocks(); + // mock the user data to return recently used orgs and apps + UserData userData = new UserData(); + Mockito.when(userDataService.getForCurrentUser()).thenReturn(Mono.just(userData)); + + // mock the list of applications + List applications = createDummyApplications(4,4); + List pageList = createDummyPages(4, 4); + + Mockito.when(applicationRepository.findByMultipleOrganizationIds( + testUser.getOrganizationIds(), READ_APPLICATIONS) + ).thenReturn(Flux.fromIterable(applications)); + + Mockito.when(newPageService.findPageSlugsByApplicationIds(anyList(), eq(READ_PAGES))) + .thenReturn(Flux.fromIterable(pageList)); + + for (Application application : applications) { + Mockito + .when(responseUtils.updateApplicationWithDefaultResources(application)) + .thenReturn(updateDefaultPageIdsWithinApplication(application)); + } + + Mockito.when(applicationService.createOrUpdateSshKeyPair(Mockito.anyString())) + .thenReturn(Mono.just(new GitAuth())); + + StepVerifier.create(applicationFetcher.getAllApplications()) + .assertNext(userHomepageDTO -> { + List dtos = userHomepageDTO.getOrganizationApplications(); + assertThat(dtos.size()).isEqualTo(4); + for (OrganizationApplicationsDTO dto : dtos) { + assertThat(dto.getApplications().size()).isEqualTo(4); + List applicationList = dto.getApplications(); + for (Application application : applicationList) { + application.getPages().forEach( + page -> assertThat(page.getSlug()).isEqualTo(page.getId()+"-unpublished-slug") + ); + application.getPublishedPages().forEach( + page -> assertThat(page.getSlug()).isEqualTo(page.getId()+"-published-slug") + ); + } + } + }).verifyComplete(); + + // Generate SSH keys for an app - to test if the app is visible in home page when the git connect step is aborted in middle + Mockito.when(applicationService.save(Mockito.any(Application.class))) + .thenReturn(Mono.just(new Application())); + Mono userHomepageDTOMono = applicationFetcher.getAllApplications() + .flatMap(userHomepageDTO -> { + List dtos = userHomepageDTO.getOrganizationApplications(); + List applicationList = dtos.get(0).getApplications(); + return Mono.just(applicationList.get(0)); + }) + // After choosing the any app randomly to connect to git, Generate keys and stop the process + .flatMap(application -> applicationService.createOrUpdateSshKeyPair(application.getId())) + .then(applicationFetcher.getAllApplications()); + + StepVerifier.create(userHomepageDTOMono) + .assertNext(userHomepageDTO -> { + List dtos = userHomepageDTO.getOrganizationApplications(); + assertThat(dtos.size()).isEqualTo(4); + for (OrganizationApplicationsDTO dto : dtos) { + assertThat(dto.getApplications().size()).isEqualTo(4); + List applicationList = dto.getApplications(); + for (Application application : applicationList) { + application.getPages().forEach( + page -> assertThat(page.getSlug()).isEqualTo(page.getId()+"-unpublished-slug") + ); + application.getPublishedPages().forEach( + page -> assertThat(page.getSlug()).isEqualTo(page.getId()+"-published-slug") + ); + } + } + }).verifyComplete(); + + // For connect and create branch flow scenarios where - defaultBranchName is somehow not saved in DB + userHomepageDTOMono = applicationFetcher.getAllApplications() + .flatMap(userHomepageDTO -> { + List dtos = userHomepageDTO.getOrganizationApplications(); + List applicationList = dtos.get(0).getApplications(); + return Mono.just(applicationList.get(0)); + }) + .flatMap(application -> { + // Create a new branched App resource in the same org and verify that branch App does not show up in the response. + Application branchApp = new Application(); + branchApp.setName("branched App"); + branchApp.setOrganizationId(application.getOrganizationId()); + branchApp.setId("org-" + 5 + "-app-" + 5); + GitApplicationMetadata gitApplicationMetadata = new GitApplicationMetadata(); + gitApplicationMetadata.setDefaultApplicationId(application.getId()); + gitApplicationMetadata.setBranchName("master"); + gitApplicationMetadata.setRemoteUrl("remnoteUrl"); + branchApp.setGitApplicationMetadata(gitApplicationMetadata); + + // Set dummy applicationPages + ApplicationPage unpublishedPage = new ApplicationPage(); + unpublishedPage.setId("page" + 5); + unpublishedPage.setDefaultPageId("page" + 5); + unpublishedPage.setIsDefault(true); + + ApplicationPage publishedPage = new ApplicationPage(); + publishedPage.setId("page" + 5); + publishedPage.setDefaultPageId("page" + 5); + publishedPage.setIsDefault(true); + + branchApp.setPages(List.of(unpublishedPage)); + branchApp.setPublishedPages(List.of(publishedPage)); + applications.add(branchApp); + + return applicationService.save(branchApp); + }) + .then(applicationFetcher.getAllApplications()); + + StepVerifier.create(userHomepageDTOMono) + .assertNext(userHomepageDTO -> { + List dtos = userHomepageDTO.getOrganizationApplications(); + assertThat(dtos.size()).isEqualTo(4); + for (OrganizationApplicationsDTO dto : dtos) { + assertThat(dto.getApplications().size()).isEqualTo(4); + List applicationList = dto.getApplications(); + for (Application application : applicationList) { + application.getPages().forEach( + page -> assertThat(page.getSlug()).isEqualTo(page.getId()+"-unpublished-slug") + ); + application.getPublishedPages().forEach( + page -> assertThat(page.getSlug()).isEqualTo(page.getId()+"-published-slug") + ); + } + } + }).verifyComplete(); + + } + @Test public void getAllApplications_WhenUserHasRecentOrgAndApp_RecentEntriesComeFirst() { initMocks(); From 5cdb4426a4fa62e33b74eb2eddc827b22a361815 Mon Sep 17 00:00:00 2001 From: Shrikant Sharat Kandula Date: Thu, 28 Apr 2022 19:56:09 +0530 Subject: [PATCH 31/34] Revert setuid/setgid cleanup in fat image --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index ce92e1505f..b661f28025 100644 --- a/Dockerfile +++ b/Dockerfile @@ -89,7 +89,7 @@ RUN chmod 0644 /etc/cron.d/* RUN chmod +x entrypoint.sh renew-certificate.sh # Disable setuid/setgid bits for the files inside container. -RUN find / \( -path /proc -prune \) -o \( \( -perm -2000 -o -perm -4000 \) -print -exec chmod -s '{}' + \) || true +#RUN find / \( -path /proc -prune \) -o \( \( -perm -2000 -o -perm -4000 \) -print -exec chmod -s '{}' + \) || true # Update path to load appsmith utils tool as default ENV PATH /opt/appsmith/utils/node_modules/.bin:$PATH From 942547287ccc2a15bfe3c70ee29f9592dffdabbe Mon Sep 17 00:00:00 2001 From: Favour Ohanekwu Date: Thu, 28 Apr 2022 09:51:02 -0700 Subject: [PATCH 32/34] feat: js object v1 run and settings redesign (#11456) --- .../JSONForm_FieldChange_spec.js | 22 +- .../JSONForm_FieldProperties_spec.js | 2 +- .../Replay/Replay_Editor_spec.js | 10 +- .../JSOnLoadActions_Spec.ts | 16 +- app/client/cypress/locators/JSEditor.json | 2 +- app/client/cypress/support/Pages/JSEditor.ts | 111 +++-- app/client/src/actions/jsPaneActions.ts | 11 + .../src/ce/constants/ReduxActionConstants.tsx | 1 + app/client/src/ce/constants/messages.ts | 11 + app/client/src/components/ads/Button.tsx | 2 +- app/client/src/components/ads/Dropdown.tsx | 31 +- app/client/src/components/ads/Radio.tsx | 7 +- app/client/src/components/ads/Tabs.tsx | 97 ++++- .../editorComponents/CodeEditor/constants.ts | 3 +- .../editorComponents/CodeEditor/index.tsx | 68 ++- .../Debugger/DebuggerLogs.tsx | 2 +- .../editorComponents/EntityBottomTabs.tsx | 34 +- .../editorComponents/JSResponseView.tsx | 320 ++++++-------- app/client/src/constants/ast.ts | 25 ++ .../src/globalStyles/CodemirrorHintStyles.ts | 4 +- app/client/src/pages/Editor/JSEditor/Form.tsx | 405 +++++++++++------- .../pages/Editor/JSEditor/JSFunctionRun.tsx | 96 +++++ .../Editor/JSEditor/JSFunctionSettings.tsx | 235 +++++++--- .../pages/Editor/JSEditor/JSObjectHotKeys.tsx | 38 ++ .../src/pages/Editor/JSEditor/constants.ts | 81 ++++ .../src/pages/Editor/JSEditor/index.tsx | 20 +- .../src/pages/Editor/JSEditor/readme.md | 9 + .../pages/Editor/JSEditor/styledComponents.ts | 132 ++++++ .../src/pages/Editor/JSEditor/utils.test.ts | 164 +++++++ app/client/src/pages/Editor/JSEditor/utils.ts | 160 +++++++ .../Editor/QueryEditor/EditorJSONtoForm.tsx | 10 +- .../entityReducers/jsActionsReducer.tsx | 48 ++- app/client/src/sagas/JSPaneSagas.ts | 12 +- app/client/src/selectors/entitiesSelector.ts | 55 ++- app/client/src/utils/AnalyticsUtil.tsx | 3 +- .../src/utils/hooks/useResizeObserver.tsx | 1 + app/client/src/workers/ast.ts | 31 +- app/client/src/workers/constants.ts | 1 - app/client/src/workers/lint.ts | 2 +- 39 files changed, 1742 insertions(+), 540 deletions(-) create mode 100644 app/client/src/constants/ast.ts create mode 100644 app/client/src/pages/Editor/JSEditor/JSFunctionRun.tsx create mode 100644 app/client/src/pages/Editor/JSEditor/JSObjectHotKeys.tsx create mode 100644 app/client/src/pages/Editor/JSEditor/constants.ts create mode 100644 app/client/src/pages/Editor/JSEditor/readme.md create mode 100644 app/client/src/pages/Editor/JSEditor/styledComponents.ts create mode 100644 app/client/src/pages/Editor/JSEditor/utils.test.ts create mode 100644 app/client/src/pages/Editor/JSEditor/utils.ts delete mode 100644 app/client/src/workers/constants.ts diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/JSONFormWidget/JSONForm_FieldChange_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/JSONFormWidget/JSONForm_FieldChange_spec.js index 5f11de06e3..666448dfab 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/JSONFormWidget/JSONForm_FieldChange_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/JSONFormWidget/JSONForm_FieldChange_spec.js @@ -19,7 +19,7 @@ describe("JSON Form Widget Field Change", () => { cy.get(`${fieldPrefix}-name`) .find("button") .should("have.length", 2); - cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input$/); + cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input/); cy.closePropertyPane(); }); @@ -36,7 +36,7 @@ describe("JSON Form Widget Field Change", () => { .find("input") .invoke("attr", "type") .should("contain", "checkbox"); - cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input$/); + cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input/); cy.closePropertyPane(); }); @@ -53,7 +53,7 @@ describe("JSON Form Widget Field Change", () => { .find("input") .click({ force: true }); cy.get(".bp3-popover.bp3-dateinput-popover").should("exist"); - cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input$/); + cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input/); cy.closePropertyPane(); }); @@ -71,7 +71,7 @@ describe("JSON Form Widget Field Change", () => { .find(".bp3-control.bp3-switch") .should("exist"); - cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input$/); + cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input/); cy.closePropertyPane(); }); @@ -82,12 +82,12 @@ describe("JSON Form Widget Field Change", () => { cy.get(".bp3-select-popover.select-popover-wrapper").should("not.exist"); cy.openFieldConfiguration("name"); - cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Select$/); + cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Select/); cy.get(`${fieldPrefix}-name label`).click({ force: true }); cy.get(".bp3-select-popover.select-popover-wrapper").should("exist"); - cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input$/); + cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input/); cy.closePropertyPane(); }); @@ -104,7 +104,7 @@ describe("JSON Form Widget Field Change", () => { .find(".rc-select-multiple") .should("exist"); - cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input$/); + cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input/); cy.closePropertyPane(); }); @@ -122,7 +122,7 @@ describe("JSON Form Widget Field Change", () => { .should("exist") .should("have.length", 2); - cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input$/); + cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input/); cy.closePropertyPane(); }); @@ -139,7 +139,7 @@ describe("JSON Form Widget Field Change", () => { .find(".t--jsonformfield-array-add-btn") .should("exist"); - cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input$/); + cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input/); cy.closePropertyPane(); }); @@ -160,7 +160,7 @@ describe("JSON Form Widget Field Change", () => { .find("input") .should("exist"); - cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input$/); + cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input/); cy.closePropertyPane(); }); @@ -185,7 +185,7 @@ describe("JSON Form Widget Field Change", () => { .should("have.length", 2); }); - cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input$/); + cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input/); cy.closePropertyPane(); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/JSONFormWidget/JSONForm_FieldProperties_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/JSONFormWidget/JSONForm_FieldProperties_spec.js index baf9a0e4b6..649ae5c3d0 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/JSONFormWidget/JSONForm_FieldProperties_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/JSONFormWidget/JSONForm_FieldProperties_spec.js @@ -166,7 +166,7 @@ describe("Select Field Property Control", () => { cy.openPropertyPane("jsonformwidget"); cy.testJsontext("sourcedata", JSON.stringify(schema)); cy.openFieldConfiguration("state"); - cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Select$/); + cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Select/); }); it("has valid default value", () => { diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Replay/Replay_Editor_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Replay/Replay_Editor_spec.js index ac4a1d5d1c..203a3e2781 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Replay/Replay_Editor_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Replay/Replay_Editor_spec.js @@ -131,15 +131,15 @@ describe("Undo/Redo functionality", function() { .first() .focus() .type("{downarrow}{downarrow}{downarrow} ") - .type("test:()=>{},"); + .type("testJSFunction:()=>{},"); cy.get("body").type(`{${modifierKey}}z{${modifierKey}}z{${modifierKey}}z`); - // verifying test function is not visible in response tab after undo - cy.get(".function-name").should("not.contain.text", "test"); + // verifying testJSFunction is not visible on page after undo + cy.contains("testJSFunction").should("not.exist"); cy.get("body").type( `{${modifierKey}}{shift}z{${modifierKey}}{shift}z{${modifierKey}}{shift}z`, ); - // verifying test function is visible in response tab after redo - cy.get(".function-name").should("contain.text", "test"); + // verifying testJSFunction is visible on page after redo + cy.contains("testJSFunction").should("exist"); // performing undo from app menu cy.get(".t--application-name").click({ force: true }); cy.get("li:contains(Edit)") diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/LayoutOnLoadActions/JSOnLoadActions_Spec.ts b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/LayoutOnLoadActions/JSOnLoadActions_Spec.ts index 8266023142..49f0f70efd 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/LayoutOnLoadActions/JSOnLoadActions_Spec.ts +++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/LayoutOnLoadActions/JSOnLoadActions_Spec.ts @@ -1,6 +1,6 @@ import { ObjectsRegistry } from "../../../../support/Objects/Registry"; -let guid: any, jsName : any; +let guid: any, jsName: any; const agHelper = ObjectsRegistry.AggregateHelper, ee = ObjectsRegistry.EntityExplorer, dataSources = ObjectsRegistry.DataSources, @@ -24,7 +24,7 @@ describe("JSObjects OnLoad Actions tests", function() { true, false, ); - jsEditor.EnableOnPageLoad("getId", false, true); + jsEditor.AddJSFunctionSettings("getId", false, true); agHelper.GenerateUUID(); cy.get("@guid").then((uid) => { dataSources.NavigateToDSCreateNew(); @@ -39,17 +39,19 @@ describe("JSObjects OnLoad Actions tests", function() { cy.get("@jsObjName").then((jsObjName) => { jsName = jsObjName; agHelper.EnterValue( - "SELECT * FROM public.users where id = {{" + jsObjName + ".getId.data}}", - );; - }) + "SELECT * FROM public.users where id = {{" + + jsObjName + + ".getId.data}}", + ); + }); }); - ee.SelectEntityByName("Table1", 'WIDGETS'); + ee.SelectEntityByName("Table1", "WIDGETS"); jsEditor.EnterJSContext("Table Data", "{{GetUser.data}}"); agHelper.DeployApp(); agHelper.AssertElementPresence(jsEditor._dialog("Confirmation Dialog")); agHelper.ClickButton("Yes"); - agHelper.Sleep(1000) + agHelper.Sleep(1000); agHelper.ValidateNetworkExecutionSuccess("@postExecute"); table.ReadTableRowColumnData(0, 0).then((cellData) => { diff --git a/app/client/cypress/locators/JSEditor.json b/app/client/cypress/locators/JSEditor.json index c89e9aa26a..2b7d95cf69 100644 --- a/app/client/cypress/locators/JSEditor.json +++ b/app/client/cypress/locators/JSEditor.json @@ -1,5 +1,5 @@ { - "runButton": ".run-button", + "runButton": ".run-js-action", "editNameField": ".bp3-editable-text-input", "outputConsole": ".CodeEditorTarget", "jsObjectName": ".t--action-name-edit-field", diff --git a/app/client/cypress/support/Pages/JSEditor.ts b/app/client/cypress/support/Pages/JSEditor.ts index 2750f6f228..750bccbaad 100644 --- a/app/client/cypress/support/Pages/JSEditor.ts +++ b/app/client/cypress/support/Pages/JSEditor.ts @@ -5,18 +5,39 @@ export class JSEditor { public locator = ObjectsRegistry.CommonLocators; public ee = ObjectsRegistry.EntityExplorer; - private _runButton = "//li//*[local-name() = 'svg' and @class='run-button']"; + private _runButton = "button.run-js-action"; + private _settingsTab = ".tab-title:contains('Settings')"; + private _codeTab = ".tab-title:contains('Code')"; + private _onPageLoadRadioButton = (functionName: string, onLoad: boolean) => + `.${functionName}-on-page-load-setting label:contains(${ + onLoad ? "Yes" : "No" + }) span.checkbox`; + private _confirmBeforeExecuteRadioButton = ( + functionName: string, + shouldConfirm: boolean, + ) => + `.${functionName}-confirm-before-execute label:contains(${ + shouldConfirm ? "Yes" : "No" + }) span.checkbox`; private _outputConsole = ".CodeEditorTarget"; private _jsObjName = ".t--js-action-name-edit-field span"; private _jsObjTxt = ".t--js-action-name-edit-field input"; - private _newJSobj = "span:contains('New JS Object')" - private _bindingsClose = ".t--entity-property-close" - private _propertyList = ".t--entity-property" - private _responseTabAction = (funName: string) => "//div[@class='function-name'][text()='" + funName + "']/following-sibling::div//*[local-name()='svg']" - private _functionSetting = (settingTxt: string) => "//span[text()='" + settingTxt + "']/parent::div/following-sibling::input[@type='checkbox']" - _dialog = (dialogHeader: string) => "//div[contains(@class, 'bp3-dialog')]//h4[contains(text(), '" + dialogHeader + "')]" - private _closeSettings = "span[icon='small-cross']" - + private _newJSobj = "span:contains('New JS Object')"; + private _bindingsClose = ".t--entity-property-close"; + private _propertyList = ".t--entity-property"; + private _responseTabAction = (funName: string) => + "//div[@class='function-name'][text()='" + + funName + + "']/following-sibling::div//*[local-name()='svg']"; + private _functionSetting = (settingTxt: string) => + "//span[text()='" + + settingTxt + + "']/parent::div/following-sibling::input[@type='checkbox']"; + _dialog = (dialogHeader: string) => + "//div[contains(@class, 'bp3-dialog')]//h4[contains(text(), '" + + dialogHeader + + "')]"; + private _closeSettings = "span[icon='small-cross']"; public NavigateToJSEditor() { cy.get(this.locator._createNew) @@ -87,7 +108,7 @@ export class JSEditor { if (toRun) { //clicking 1 times & waits for 3 second for result to be populated! Cypress._.times(1, () => { - cy.xpath(this._runButton) + cy.get(this._runButton) .first() .click() .wait(3000); @@ -109,13 +130,21 @@ export class JSEditor { this.agHelper.AssertAutoSave(); //Ample wait due to open bug # 10284 } - public EnterJSContext(endp: string, value: string, paste = true, toToggleOnJS = false, notField = false) { + public EnterJSContext( + endp: string, + value: string, + paste = true, + toToggleOnJS = false, + notField = false, + ) { if (toToggleOnJS) { cy.get(this.locator._jsToggle(endp.replace(/ +/g, "").toLowerCase())) .invoke("attr", "class") .then((classes: any) => { if (!classes.includes("is-active")) { - cy.get(this.locator._jsToggle(endp.replace(/ +/g, "").toLowerCase())) + cy.get( + this.locator._jsToggle(endp.replace(/ +/g, "").toLowerCase()), + ) .first() .click({ force: true }); } @@ -131,20 +160,23 @@ export class JSEditor { // .type("{del}", { force: true }); if (paste) { - this.agHelper.EnterValue(value, endp, notField) - } - else { - cy.get(this.locator._propertyControl + endp.replace(/ +/g, "").toLowerCase() + " " + this.locator._codeMirrorTextArea) + this.agHelper.EnterValue(value, endp, notField); + } else { + cy.get( + this.locator._propertyControl + + endp.replace(/ +/g, "").toLowerCase() + + " " + + this.locator._codeMirrorTextArea, + ) .first() .then((el: any) => { const input = cy.get(el); input.type(value, { parseSpecialCharSequences: false, }); - }) + }); } - // cy.focused().then(($cm: any) => { // if ($cm.contents != "") { // cy.log("The field is not empty"); @@ -175,18 +207,22 @@ export class JSEditor { // }); // }); - this.agHelper.AssertAutoSave()//Allowing time for Evaluate value to capture value - + this.agHelper.AssertAutoSave(); //Allowing time for Evaluate value to capture value } public RemoveText(endp: string) { - cy.get(this.locator._propertyControl + endp + " " + this.locator._codeMirrorTextArea) + cy.get( + this.locator._propertyControl + + endp + + " " + + this.locator._codeMirrorTextArea, + ) .first() .focus() .type("{uparrow}", { force: true }) .type("{ctrl}{shift}{downarrow}", { force: true }) .type("{del}", { force: true }); - this.agHelper.AssertAutoSave() + this.agHelper.AssertAutoSave(); } public RenameJSObjFromForm(renameVal: string) { @@ -216,7 +252,7 @@ export class JSEditor { public validateDefaultJSObjProperties(jsObjName: string) { this.ee.ActionContextMenuByEntityName(jsObjName, "Show Bindings"); - cy.get(this._propertyList).then(function ($lis) { + cy.get(this._propertyList).then(function($lis) { const bindingsLength = $lis.length; expect(bindingsLength).to.be.at.least(4); expect($lis.eq(0).text()).to.be.oneOf([ @@ -239,17 +275,22 @@ export class JSEditor { cy.get(this._bindingsClose).click({ force: true }); } - - public EnableOnPageLoad(funName: string, onLoad = true, bfrCalling = true) { - - this.agHelper.GetNClick(this._responseTabAction(funName)) - this.agHelper.AssertElementPresence(this._dialog('Function settings')) - if (onLoad) - this.agHelper.CheckUncheck(this._functionSetting(Cypress.env("MESSAGES").JS_SETTINGS_ONPAGELOAD()), true) - if (bfrCalling) - this.agHelper.CheckUncheck(this._functionSetting(Cypress.env("MESSAGES").JS_SETTINGS_CONFIRM_EXECUTION()), true) - - this.agHelper.GetNClick(this._closeSettings) + public AddJSFunctionSettings( + funName: string, + onLoad = true, + bfrCalling = true, + ) { + // Navigate to Settings tab + this.agHelper.GetNClick(this._settingsTab); + // Set onPageLoad + cy.get(this._onPageLoadRadioButton(funName, onLoad)) + .first() + .click(); + // Set confirmBeforeExecute + cy.get(this._confirmBeforeExecuteRadioButton(funName, bfrCalling)) + .first() + .click(); + // Return to code tab + this.agHelper.GetNClick(this._codeTab); } - } diff --git a/app/client/src/actions/jsPaneActions.ts b/app/client/src/actions/jsPaneActions.ts index 9eee273646..0eb39c97c4 100644 --- a/app/client/src/actions/jsPaneActions.ts +++ b/app/client/src/actions/jsPaneActions.ts @@ -4,6 +4,7 @@ import { } from "@appsmith/constants/ReduxActionConstants"; import { JSCollection, JSAction } from "entities/JSCollection"; import { RefactorAction, SetFunctionPropertyPayload } from "api/JSActionAPI"; + export const createNewJSCollection = ( pageId: string, ): ReduxAction<{ pageId: string }> => ({ @@ -89,3 +90,13 @@ export const updateJSFunction = (payload: SetFunctionPropertyPayload) => { payload, }; }; + +export const setActiveJSAction = (payload: { + jsCollectionId: string; + jsActionId: string; +}) => { + return { + type: ReduxActionTypes.SET_ACTIVE_JS_ACTION, + payload, + }; +}; diff --git a/app/client/src/ce/constants/ReduxActionConstants.tsx b/app/client/src/ce/constants/ReduxActionConstants.tsx index e5dd26ab38..0254677d1a 100644 --- a/app/client/src/ce/constants/ReduxActionConstants.tsx +++ b/app/client/src/ce/constants/ReduxActionConstants.tsx @@ -699,6 +699,7 @@ export const ReduxActionTypes = { GET_TEMPLATE_SUCCESS: "GET_TEMPLATES_SUCCESS", START_EXECUTE_JS_FUNCTION: "START_EXECUTE_JS_FUNCTION", RESET_PAGE_LIST: "RESET_PAGE_LIST", + SET_ACTIVE_JS_ACTION: "SET_ACTIVE_JS_ACTION", }; export type ReduxActionType = typeof ReduxActionTypes[keyof typeof ReduxActionTypes]; diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index 6a74d7269f..54e4f85be7 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -392,6 +392,8 @@ export const ACTION_CONFIGURATION_UPDATED = () => "Configuration updated"; export const WIDGET_PROPERTIES_UPDATED = () => "Widget properties were updated"; export const EMPTY_RESPONSE_FIRST_HALF = () => "🙌 Click on"; export const EMPTY_RESPONSE_LAST_HALF = () => "to get a response"; +export const EMPTY_JS_RESPONSE_LAST_HALF = () => + "to view response of selected function"; export const INVALID_EMAIL = () => "Please enter a valid email"; export const DEBUGGER_INTERCOM_TEXT = (text: string) => `Hi, \nI'm facing the following error on Appsmith, can you please help? \n\n${text}`; @@ -451,6 +453,8 @@ export const JS_EXECUTION_FAILURE = () => "JS Function execution failed"; export const JS_EXECUTION_FAILURE_TOASTER = () => "There was an error while executing function"; export const JS_SETTINGS_ONPAGELOAD = () => "Run function on page load (Beta)"; +export const JS_EXECUTION_SUCCESS_TOASTER = (actionName: string) => + `${actionName} ran successfully`; export const JS_SETTINGS_ONPAGELOAD_SUBTEXT = () => "Will refresh data every time page is reloaded"; export const JS_SETTINGS_CONFIRM_EXECUTION = () => @@ -459,6 +463,13 @@ export const JS_SETTINGS_CONFIRM_EXECUTION_SUBTEXT = () => "Ask confirmation from the user every time before refreshing data"; export const JS_SETTINGS_EXECUTE_TIMEOUT = () => "Function Timeout (in milliseconds)"; +export const ASYNC_FUNCTION_SETTINGS_HEADING = () => "Async Function Settings"; +export const NO_ASYNC_FUNCTIONS = () => + "There is no asynchronous function in this JSObject"; +export const NO_JS_FUNCTION_TO_RUN = (JSObjectName: string) => + `${JSObjectName} has no function`; +export const NO_JS_FUNCTION_RETURN_VALUE = (JSFunctionName: string) => + `${JSFunctionName} did not return any data. Did you add a return statement?`; // Import/Export Application features export const IMPORT_APPLICATION_MODAL_TITLE = () => "Import application"; diff --git a/app/client/src/components/ads/Button.tsx b/app/client/src/components/ads/Button.tsx index 60c248b5ab..642831e9e3 100644 --- a/app/client/src/components/ads/Button.tsx +++ b/app/client/src/components/ads/Button.tsx @@ -424,7 +424,7 @@ const ButtonStyles = css` } `; -const StyledButton = styled("button")` +export const StyledButton = styled("button")` ${ButtonStyles} `; diff --git a/app/client/src/components/ads/Dropdown.tsx b/app/client/src/components/ads/Dropdown.tsx index 8dce917833..3f25782e21 100644 --- a/app/client/src/components/ads/Dropdown.tsx +++ b/app/client/src/components/ads/Dropdown.tsx @@ -38,6 +38,7 @@ export type DropdownOption = { onSelect?: DropdownOnSelect; data?: any; isSectionHeader?: boolean; + hasCustomBadge?: boolean; }; export interface DropdownSearchProps { enableSearch?: boolean; @@ -101,6 +102,8 @@ export type DropdownProps = CommonComponentProps & defaultIcon?: IconName; allowDeselection?: boolean; //prevents de-selection of the selected option truncateOption?: boolean; // enabled wrapping and adding tooltip on option item of dropdown menu + customBadge?: JSX.Element; + selectedHighlightBg?: string; }; export interface DefaultDropDownValueNodeProps { selected: DropdownOption | DropdownOption[]; @@ -259,6 +262,10 @@ export const DropdownContainer = styled.div<{ width: string; height?: string }>` span.bp3-popover-target { display: inline-block; width: 100%; + height: 100%; + } + span.bp3-popover-target div { + height: 100%; } span.bp3-popover-wrapper { @@ -333,6 +340,7 @@ const DropdownOptionsWrapper = styled.div<{ const OptionWrapper = styled.div<{ selected: boolean; + selectedHighlightBg?: string; }>` padding: ${(props) => props.theme.spaces[2] + 1}px ${(props) => props.theme.spaces[5]}px; @@ -340,7 +348,9 @@ const OptionWrapper = styled.div<{ display: flex; align-items: center; min-height: 36px; - background-color: ${(props) => (props.selected ? Colors.GREEN_3 : null)}; + background-color: ${(props) => + props.selected ? props.selectedHighlightBg || Colors.GREEN_3 : null}; + &&& svg { rect { fill: ${(props) => props.theme.colors.dropdownIconBg}; @@ -371,7 +381,7 @@ const OptionWrapper = styled.div<{ } &:hover { - background-color: ${Colors.GREEN_3}; + background-color: ${(props) => props.selectedHighlightBg || Colors.GREEN_3}; &&& svg { rect { @@ -724,6 +734,7 @@ export function RenderDropdownOptions(props: DropdownOptionsProps) { } role="option" selected={isSelected} + selectedHighlightBg={props.selectedHighlightBg} > {option.leftElement && ( {option.leftElement} @@ -747,12 +758,18 @@ export function RenderDropdownOptions(props: DropdownOptionsProps) { ) : null} {props.showLabelOnly ? ( props.truncateOption ? ( - + <> + + {option.hasCustomBadge && props.customBadge} + ) : ( - {option.label} + <> + {option.label} + {option.hasCustomBadge && props.customBadge} + ) ) : option.label && option.value ? ( diff --git a/app/client/src/components/ads/Radio.tsx b/app/client/src/components/ads/Radio.tsx index f07fcd97dc..816c6b46f2 100644 --- a/app/client/src/components/ads/Radio.tsx +++ b/app/client/src/components/ads/Radio.tsx @@ -3,7 +3,7 @@ import React, { useState, useEffect } from "react"; import styled from "styled-components"; import * as log from "loglevel"; -type OptionProps = { +export type OptionProps = { label: string; value: string; disabled?: boolean; @@ -17,6 +17,9 @@ export type RadioProps = CommonComponentProps & { onSelect?: (value: string) => void; options: OptionProps[]; backgroundColor?: string; + // To prevent interference when there are multiple radio groups, + // options corresponding to the same radio should have same name, which is different from others. + name?: string; }; const RadioGroup = styled.div<{ @@ -149,7 +152,7 @@ export default function RadioComponent(props: RadioProps) { option.onSelect && option.onSelect(e.target.value)} type="radio" value={option.value} diff --git a/app/client/src/components/ads/Tabs.tsx b/app/client/src/components/ads/Tabs.tsx index baf1d67c7a..f6ab87175c 100644 --- a/app/client/src/components/ads/Tabs.tsx +++ b/app/client/src/components/ads/Tabs.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { RefObject, useCallback, useState } from "react"; import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; import "react-tabs/style/react-tabs.css"; import styled from "styled-components"; @@ -6,6 +6,10 @@ import Icon, { IconName, IconSize } from "./Icon"; import { Classes, CommonComponentProps } from "./common"; import { useEffect } from "react"; import { Indices } from "constants/Layers"; +import { theme } from "constants/DefaultTheme"; +import useResizeObserver from "utils/hooks/useResizeObserver"; + +export const TAB_MIN_HEIGHT = `36px`; export type TabProp = { key: string; @@ -28,7 +32,7 @@ const TabsWrapper = styled.div<{ height: 100%; } .react-tabs__tab-panel { - height: calc(100% - 36px); + height: ${() => `calc(100% - ${TAB_MIN_HEIGHT})`}; overflow: auto; } .react-tabs__tab-list { @@ -251,6 +255,13 @@ const TabTitleWrapper = styled.div<{ : ""} `; +const CollapseIconWrapper = styled.div` + position: absolute; + right: 14px; + top: ${() => theme.spaces[3] - 1}px; + cursor: pointer; +`; + export type TabItemProps = { tab: TabProp; selected: boolean; @@ -288,18 +299,88 @@ export type TabbedViewComponentType = CommonComponentProps & { vertical?: boolean; tabItemComponent?: (props: TabItemProps) => JSX.Element; responseViewer?: boolean; + canCollapse?: boolean; + // Reference to container for collapsing or expanding content + containerRef?: RefObject; + // height of container when expanded + expandedHeight?: string; }; -export function TabComponent(props: TabbedViewComponentType) { +// Props required to support a collapsible (foldable) tab component +export type CollapsibleTabProps = { + // Reference to container for collapsing or expanding content + containerRef: RefObject; + // height of container when expanded( usually the default height of the tab component) + expandedHeight: string; +}; + +export type CollapsibleTabbedViewComponentType = TabbedViewComponentType & + CollapsibleTabProps; + +export const collapsibleTabRequiredPropKeys: Array = [ + "containerRef", + "expandedHeight", +]; + +// Tab is considered collapsible only when all required collapsible props are present +export const isCollapsibleTabComponent = ( + props: TabbedViewComponentType | CollapsibleTabbedViewComponentType, +): props is CollapsibleTabbedViewComponentType => + collapsibleTabRequiredPropKeys.every((key) => key in props); + +export function TabComponent( + props: TabbedViewComponentType | CollapsibleTabbedViewComponentType, +) { const TabItem = props.tabItemComponent || DefaultTabItem; // for setting selected state of an uncontrolled component const [selectedIndex, setSelectedIndex] = useState(props.selectedIndex || 0); + const [isExpanded, setIsExpanded] = useState(true); useEffect(() => { if (typeof props.selectedIndex === "number") setSelectedIndex(props.selectedIndex); }, [props.selectedIndex]); + const handleContainerResize = () => { + if (!isCollapsibleTabComponent(props)) return; + const { containerRef, expandedHeight } = props; + if (containerRef?.current && expandedHeight) { + containerRef.current.style.height = isExpanded + ? TAB_MIN_HEIGHT + : expandedHeight; + } + setIsExpanded((prev) => !prev); + }; + + const resizeCallback = useCallback( + (entries: ResizeObserverEntry[]) => { + if (entries && entries.length) { + const { + contentRect: { height }, + } = entries[0]; + if (height > Number(TAB_MIN_HEIGHT.replace("px", "")) + 6) { + !isExpanded && setIsExpanded(true); + } else { + isExpanded && setIsExpanded(false); + } + } + }, + [isExpanded], + ); + + useResizeObserver( + isCollapsibleTabComponent(props) ? props.containerRef?.current : null, + resizeCallback, + ); + + useEffect(() => { + if (!isCollapsibleTabComponent(props)) return; + const { containerRef } = props; + if (!isExpanded && containerRef.current) { + containerRef.current.style.height = TAB_MIN_HEIGHT; + } + }, [isExpanded]); + return ( + {isCollapsibleTabComponent(props) && ( + + + + )} + { props.onSelect && props.onSelect(index); diff --git a/app/client/src/components/editorComponents/CodeEditor/constants.ts b/app/client/src/components/editorComponents/CodeEditor/constants.ts index 3555d4ba57..c63d301ab2 100644 --- a/app/client/src/components/editorComponents/CodeEditor/constants.ts +++ b/app/client/src/components/editorComponents/CodeEditor/constants.ts @@ -7,8 +7,7 @@ export const WARNING_LINT_ERRORS = { export const LINT_TOOLTIP_CLASS = "CodeMirror-lint-tooltip"; -export const LINT_TOOLTIP_JUSTIFIFIED_LEFT_CLASS = - "CodeMirror-lint-tooltip-left"; +export const LINT_TOOLTIP_JUSTIFIED_LEFT_CLASS = "CodeMirror-lint-tooltip-left"; export enum LintTooltipDirection { left = "left", diff --git a/app/client/src/components/editorComponents/CodeEditor/index.tsx b/app/client/src/components/editorComponents/CodeEditor/index.tsx index dabae61106..cebc55a458 100644 --- a/app/client/src/components/editorComponents/CodeEditor/index.tsx +++ b/app/client/src/components/editorComponents/CodeEditor/index.tsx @@ -91,7 +91,7 @@ import { replayHighlightClass } from "globalStyles/portals"; import { LintTooltipDirection, LINT_TOOLTIP_CLASS, - LINT_TOOLTIP_JUSTIFIFIED_LEFT_CLASS, + LINT_TOOLTIP_JUSTIFIED_LEFT_CLASS, } from "./constants"; interface ReduxStateProps { @@ -135,6 +135,31 @@ export type EditorStyleProps = { popperPlacement?: Placement; popperZIndex?: Indices; }; +/** + * line => Line to which the gutter is added + * + * element => HTML Element that gets added to line + * + * isFocusedAction => function called when focused + */ +export type GutterConfig = { + line: number; + element: HTMLElement; + isFocusedAction: () => void; +}; + +export type CodeEditorGutter = { + getGutterConfig: + | ((editorValue: string, cursorLineNumber: number) => GutterConfig | null) + | null; + gutterId: string; +}; + +export type CustomKeyMap = { + // combination of keys + combination: string; + onKeyDown: (cm: CodeMirror.Editor) => void; +}; export type EditorProps = EditorStyleProps & EditorConfig & { @@ -153,6 +178,8 @@ export type EditorProps = EditorStyleProps & handleMouseLeave?: () => void; isReadOnly?: boolean; isRawView?: boolean; + // Custom gutter + customGutter?: CodeEditorGutter; }; type Props = ReduxStateProps & @@ -222,6 +249,8 @@ class CodeEditor extends Component { tabindex: -1, }; + const gutters = new Set(); + if (!this.props.input.onChange || this.props.disabled) { options.readOnly = true; options.scrollbarStyle = "null"; @@ -231,9 +260,13 @@ class CodeEditor extends Component { if (this.props.tabBehaviour === TabBehaviour.INPUT) { options.extraKeys["Tab"] = false; } + if (this.props.customGutter) { + gutters.add(this.props.customGutter.gutterId); + } if (this.props.folding) { options.foldGutter = true; - options.gutters = ["CodeMirror-linenumbers", "CodeMirror-foldgutter"]; + gutters.add("CodeMirror-linenumbers"); + gutters.add("CodeMirror-foldgutter"); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore options.foldOptions = { @@ -242,6 +275,7 @@ class CodeEditor extends Component { }, }; } + options.gutters = Array.from(gutters); // Set value of the editor const inputValue = getInputValue(this.props.input.value) || ""; @@ -263,7 +297,6 @@ class CodeEditor extends Component { // which means CodeMirror recalculates itself only one time, once all CodeMirror // changes here are completed // - editor.on("beforeChange", this.handleBeforeChange); editor.on("change", this.startChange); editor.on("keyup", this.handleAutocompleteKeyup); @@ -271,6 +304,7 @@ class CodeEditor extends Component { editor.on("cursorActivity", this.handleCursorMovement); editor.on("blur", this.handleEditorBlur); editor.on("postPick", () => this.handleAutocompleteVisibility(editor)); + if (this.props.height) { editor.setSize("100%", this.props.height); } else { @@ -331,7 +365,8 @@ class CodeEditor extends Component { }); } - handleMouseMove = () => { + handleMouseMove = (e: React.MouseEvent) => { + this.handleCustomGutter(this.editor.lineAtHeight(e.clientY, "window")); // this code only runs when we want custom tool tip for any highlighted text inside codemirror instance if ( this.props.showCustomToolTipForHighlightedText && @@ -414,7 +449,29 @@ class CodeEditor extends Component { }); } + handleCustomGutter = (lineNumber: number | null, isFocused = false) => { + const { customGutter } = this.props; + const editor = this.editor; + if (!customGutter || !editor) return; + editor.clearGutter(customGutter.gutterId); + + if (lineNumber && customGutter.getGutterConfig) { + const gutterConfig = customGutter.getGutterConfig( + editor.getValue(), + lineNumber, + ); + if (!gutterConfig) return; + editor.setGutterMarker( + gutterConfig.line, + customGutter.gutterId, + gutterConfig.element, + ); + isFocused && gutterConfig.isFocusedAction(); + } + }; + handleCursorMovement = (cm: CodeMirror.Editor) => { + this.handleCustomGutter(cm.getCursor().line, true); // ignore if disabled if (!this.props.input.onChange || this.props.disabled) { return; @@ -446,6 +503,7 @@ class CodeEditor extends Component { this.handleChange(); this.setState({ isFocused: false }); this.editor.setOption("matchBrackets", false); + this.handleCustomGutter(null); }; handleBeforeChange = ( @@ -475,7 +533,7 @@ class CodeEditor extends Component { tooltip && getLintTooltipDirection(tooltip) === LintTooltipDirection.left ) { - tooltip.classList.add(LINT_TOOLTIP_JUSTIFIFIED_LEFT_CLASS); + tooltip.classList.add(LINT_TOOLTIP_JUSTIFIED_LEFT_CLASS); } } }; diff --git a/app/client/src/components/editorComponents/Debugger/DebuggerLogs.tsx b/app/client/src/components/editorComponents/Debugger/DebuggerLogs.tsx index 9a7ed9316a..0303e32702 100644 --- a/app/client/src/components/editorComponents/Debugger/DebuggerLogs.tsx +++ b/app/client/src/components/editorComponents/Debugger/DebuggerLogs.tsx @@ -19,7 +19,7 @@ const ContainerWrapper = styled.div` height: 100%; `; -const ListWrapper = styled.div` +export const ListWrapper = styled.div` overflow: auto; height: calc(100% - ${LIST_HEADER_HEIGHT}); ${thinScrollbar}; diff --git a/app/client/src/components/editorComponents/EntityBottomTabs.tsx b/app/client/src/components/editorComponents/EntityBottomTabs.tsx index 26a62226a9..13834bf1d6 100644 --- a/app/client/src/components/editorComponents/EntityBottomTabs.tsx +++ b/app/client/src/components/editorComponents/EntityBottomTabs.tsx @@ -1,7 +1,12 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, RefObject } from "react"; import { useDispatch, useSelector } from "react-redux"; import { setCurrentTab } from "actions/debuggerActions"; -import { TabComponent, TabProp } from "components/ads/Tabs"; +import { + CollapsibleTabProps, + collapsibleTabRequiredPropKeys, + TabComponent, + TabProp, +} from "components/ads/Tabs"; import { getCurrentDebuggerTab } from "selectors/debuggerSelectors"; import AnalyticsUtil from "utils/AnalyticsUtil"; import { DEBUGGER_TAB_KEYS } from "./Debugger/helpers"; @@ -12,9 +17,26 @@ type EntityBottomTabsProps = { responseViewer?: boolean; onSelect?: (tab: any) => void; selectedTabIndex?: number; // this is used in the event you want to directly control the index changes. + canCollapse?: boolean; + // Reference to container for collapsing or expanding content + containerRef?: RefObject; + // height of container when expanded + expandedHeight?: string; }; + +type CollapsibleEntityBottomTabsProps = EntityBottomTabsProps & + CollapsibleTabProps; + +// Tab is considered collapsible only when all required collapsible props are present +export const isCollapsibleEntityBottomTab = ( + props: EntityBottomTabsProps | CollapsibleEntityBottomTabsProps, +): props is CollapsibleEntityBottomTabsProps => + collapsibleTabRequiredPropKeys.every((key) => key in props); + // Using this if there are debugger related tabs -function EntityBottomTabs(props: EntityBottomTabsProps) { +function EntityBottomTabs( + props: EntityBottomTabsProps | CollapsibleEntityBottomTabsProps, +) { const [selectedIndex, setSelectedIndex] = useState(props.defaultIndex); const currentTab = useSelector(getCurrentDebuggerTab); const dispatch = useDispatch(); @@ -51,6 +73,12 @@ function EntityBottomTabs(props: EntityBottomTabsProps) { props.selectedTabIndex ? props.selectedTabIndex : selectedIndex } tabs={props.tabs} + {...(isCollapsibleEntityBottomTab(props) + ? { + containerRef: props.containerRef, + expandedHeight: props.expandedHeight, + } + : {})} /> ); } diff --git a/app/client/src/components/editorComponents/JSResponseView.tsx b/app/client/src/components/editorComponents/JSResponseView.tsx index 3460a250ac..1c7b7670e3 100644 --- a/app/client/src/components/editorComponents/JSResponseView.tsx +++ b/app/client/src/components/editorComponents/JSResponseView.tsx @@ -1,4 +1,10 @@ -import React, { useState, useRef, RefObject, useCallback } from "react"; +import React, { + useEffect, + useRef, + RefObject, + useCallback, + useState, +} from "react"; import { connect, useDispatch } from "react-redux"; import { withRouter, RouteComponentProps } from "react-router"; import styled from "styled-components"; @@ -9,10 +15,10 @@ import { DEBUGGER_ERRORS, DEBUGGER_LOGS, EXECUTING_FUNCTION, - EMPTY_JS_OBJECT, PARSING_ERROR, EMPTY_RESPONSE_FIRST_HALF, - EMPTY_RESPONSE_LAST_HALF, + EMPTY_JS_RESPONSE_LAST_HALF, + NO_JS_FUNCTION_RETURN_VALUE, } from "@appsmith/constants/messages"; import { EditorTheme } from "./CodeEditor/EditorConfig"; import DebuggerLogs from "./Debugger/DebuggerLogs"; @@ -21,40 +27,35 @@ import Resizer, { ResizerCSS } from "./Debugger/Resizer"; import AnalyticsUtil from "utils/AnalyticsUtil"; import { JSCollection, JSAction } from "entities/JSCollection"; import ReadOnlyEditor from "components/editorComponents/ReadOnlyEditor"; -import { startExecutingJSFunction } from "actions/jsPaneActions"; import Text, { TextType } from "components/ads/Text"; import { Classes } from "components/ads/common"; import LoadingOverlayScreen from "components/editorComponents/LoadingOverlayScreen"; -import { sortBy } from "lodash"; -import { ReactComponent as JSFunction } from "assets/icons/menu/js-function.svg"; -import { ReactComponent as RunFunction } from "assets/icons/menu/run.svg"; import { JSCollectionData } from "reducers/entityReducers/jsActionsReducer"; import Callout from "components/ads/Callout"; import { Variant } from "components/ads/common"; import { EvaluationError } from "utils/DynamicBindingUtils"; -import { Severity } from "entities/AppsmithConsole"; -import { getJSCollectionIdFromURL } from "pages/Editor/Explorer/helpers"; import { DebugButton } from "./Debugger/DebugCTA"; -import { thinScrollbar } from "constants/DefaultTheme"; import { setCurrentTab } from "actions/debuggerActions"; import { DEBUGGER_TAB_KEYS } from "./Debugger/helpers"; import EntityBottomTabs from "./EntityBottomTabs"; import Icon from "components/ads/Icon"; -import { ReactComponent as FunctionSettings } from "assets/icons/menu/settings.svg"; -import JSFunctionSettings from "pages/Editor/JSEditor/JSFunctionSettings"; -import FlagBadge from "components/utils/FlagBadge"; +import { TAB_MIN_HEIGHT } from "components/ads/Tabs"; +import { theme } from "constants/DefaultTheme"; +import { Button, Size } from "components/ads"; +import { CodeEditorWithGutterStyles } from "pages/Editor/JSEditor/constants"; const ResponseContainer = styled.div` ${ResizerCSS} - // Initial height of bottom tabs - height: ${(props) => props.theme.actionsBottomTabInitialHeight}; width: 100%; // Minimum height of bottom tabs as it can be resized - min-height: 36px; + min-height: ${TAB_MIN_HEIGHT}; background-color: ${(props) => props.theme.colors.apiPane.responseBody.bg}; + height: ${({ theme }) => theme.actionsBottomTabInitialHeight}; .react-tabs__tab-panel { - overflow: hidden; + ${CodeEditorWithGutterStyles} + overflow-y: auto; + height: calc(100% - ${TAB_MIN_HEIGHT}); } `; @@ -71,79 +72,39 @@ const ResponseTabWrapper = styled.div` } `; -const ResponseTabActionsList = styled.ul` - height: 100%; - width: 20%; - list-style: none; - padding-left: 0; - ${thinScrollbar}; - scrollbar-width: thin; - overflow: auto; - padding-bottom: 40px; - margin-top: 0; -`; - -const ResponseTabAction = styled.li` - padding: 10px 0px 10px 20px; - display: flex; - align-items: center; - &:hover { - cursor: pointer; - background-color: #f0f0f0; - } - .function-name { - margin-left: 5px; - display: inline-block; - flex: 1; - } - .function-actions { - margin-left: auto; - order: 2; - svg { - display: inline-block; - } - } - .run-button { - margin: 0 15px; - margin-left: 10px; - } - &.active { - background-color: #f0f0f0; - } -`; - const TabbedViewWrapper = styled.div` height: 100%; &&& { ul.react-tabs__tab-list { padding: 0px ${(props) => props.theme.spaces[12]}px; + height: ${TAB_MIN_HEIGHT}; } } `; const ResponseViewer = styled.div` - width: 80%; + width: 100%; `; const NoResponseContainer = styled.div` height: 100%; - width: 100%; + width: max-content; display: flex; align-items: center; justify-content: center; flex-direction: column; + margin: 0 auto; &.empty { background-color: #fafafa; } .${Classes.ICON} { margin-right: 0px; svg { - width: 150px; + width: auto; height: 150px; } } - .${Classes.TEXT} { margin-top: ${(props) => props.theme.spaces[9]}px; color: #090707; @@ -166,6 +127,23 @@ const StyledCallout = styled(Callout)` } `; +const NoReturnValueWrapper = styled.div` + padding-left: ${(props) => props.theme.spaces[12]}px; + padding-top: ${(props) => props.theme.spaces[6]}px; +`; +const InlineButton = styled(Button)` + display: inline-flex; + margin: 0 4px; +`; + +enum JSResponseState { + IsExecuting = "IsExecuting", + IsDirty = "IsDirty", + NoResponse = "NoResponse", + ShowResponse = "ShowResponse", + NoReturnValue = "NoReturnValue", +} + interface ReduxStateProps { responses: Record; isExecuting: Record; @@ -173,26 +151,35 @@ interface ReduxStateProps { type Props = ReduxStateProps & RouteComponentProps & { + currentFunction: JSAction | null; theme?: EditorTheme; jsObject: JSCollection; errors: Array; + disabled: boolean; + isLoading: boolean; + onButtonClick: (e: React.MouseEvent) => void; }; function JSResponseView(props: Props) { - const { errors, isExecuting, jsObject, responses } = props; + const { + currentFunction, + disabled, + errors, + isExecuting, + isLoading, + jsObject, + onButtonClick, + responses, + } = props; + const [responseStatus, setResponseStatus] = useState( + JSResponseState.NoResponse, + ); const panelRef: RefObject = useRef(null); const dispatch = useDispatch(); - const [selectActionId, setSelectActionId] = useState(""); - const actionList = jsObject?.actions; - const sortedActionList = actionList && sortBy(actionList, "name"); const response = - selectActionId && selectActionId in responses - ? responses[selectActionId] + currentFunction && currentFunction.id && currentFunction.id in responses + ? responses[currentFunction.id] : ""; - const isRunning = selectActionId && !!isExecuting[selectActionId]; - const errorsList = errors.filter((er) => { - return er.severity === Severity.ERROR; - }); const onDebugClick = useCallback(() => { AnalyticsUtil.logEvent("OPEN_DEBUGGER", { @@ -200,18 +187,25 @@ function JSResponseView(props: Props) { }); dispatch(setCurrentTab(DEBUGGER_TAB_KEYS.ERROR_TAB)); }, []); - - const [openSettings, setOpenSettings] = useState(false); - const [selectedFunction, setSelectedFunction] = useState< - undefined | JSAction - >(undefined); - const isSelectedFunctionAsync = (id: string) => { - const jsAction = jsObject.actions.find((action) => action.id === id); - if (!!jsAction) { - return jsAction?.actionConfiguration.isAsync; + useEffect(() => { + if (!currentFunction) { + setResponseStatus(JSResponseState.NoResponse); + } else if (isExecuting[currentFunction.id]) { + setResponseStatus(JSResponseState.IsExecuting); + } else if ( + !responses.hasOwnProperty(currentFunction.id) && + !isExecuting.hasOwnProperty(currentFunction.id) + ) { + setResponseStatus(JSResponseState.NoResponse); + } else if ( + responses.hasOwnProperty(currentFunction.id) && + responses[currentFunction.id] === undefined + ) { + setResponseStatus(JSResponseState.NoReturnValue); + } else if (responses.hasOwnProperty(currentFunction.id)) { + setResponseStatus(JSResponseState.ShowResponse); } - return false; - }; + }, [responses, isExecuting, currentFunction]); const tabs = [ { @@ -219,8 +213,8 @@ function JSResponseView(props: Props) { title: "Response", panelComponent: ( <> - - {errorsList.length > 0 ? ( + {errors.length > 0 && ( + - ) : ( - "" - )} - - - {sortedActionList && !sortedActionList?.length ? ( - - {createMessage(EMPTY_JS_OBJECT)} - - ) : ( + + )} + + <> - - {sortedActionList && - sortedActionList?.length > 0 && - sortedActionList.map((action) => { - return ( - { - setSelectActionId(action.id); - }} - > - {" "} -
{action.name}
-
- {action.actionConfiguration.isAsync ? ( - - ) : ( - "" - )} - {isSelectedFunctionAsync(action.id) ? ( - { - setSelectedFunction(action); - setOpenSettings(true); - }} - /> - ) : ( - "" - )} - - { - runAction(action); - }} - /> -
-
- ); - })} -
- - {isRunning ? ( - - {createMessage(EXECUTING_FUNCTION)} - - ) : !responses.hasOwnProperty(selectActionId) ? ( - - - - {EMPTY_RESPONSE_FIRST_HALF()} - - {EMPTY_RESPONSE_LAST_HALF()} - - - ) : ( - - )} - - {openSettings && - !!selectedFunction && - isSelectedFunctionAsync(selectedFunction.id) && ( - { - setOpenSettings(!openSettings); - }} - /> - )} + {responseStatus === JSResponseState.NoResponse && ( + + + + {createMessage(EMPTY_RESPONSE_FIRST_HALF)} + + {createMessage(EMPTY_JS_RESPONSE_LAST_HALF)} + + + )} + {responseStatus === JSResponseState.IsExecuting && ( + + {createMessage(EXECUTING_FUNCTION)} + + )} + {responseStatus === JSResponseState.NoReturnValue && ( + + + {createMessage( + NO_JS_FUNCTION_RETURN_VALUE, + currentFunction?.name, + )} + + + )} + {responseStatus === JSResponseState.ShowResponse && ( + + )} - )} +
), @@ -339,23 +290,16 @@ function JSResponseView(props: Props) { }, ]; - const runAction = (action: JSAction) => { - setSelectActionId(action.id); - const collectionId = getJSCollectionIdFromURL(); - dispatch( - startExecutingJSFunction({ - collectionName: jsObject?.name || "", - action: action, - collectionId: collectionId || "", - }), - ); - }; - return ( - + ); diff --git a/app/client/src/constants/ast.ts b/app/client/src/constants/ast.ts new file mode 100644 index 0000000000..4582a7261e --- /dev/null +++ b/app/client/src/constants/ast.ts @@ -0,0 +1,25 @@ +export const ECMA_VERSION = 11; + +/* Indicates the mode the code should be parsed in. +This influences global strict mode and parsing of import and export declarations. +*/ +export enum SourceType { + script = "script", + module = "module", +} + +// Each node has an attached type property which further defines +// what all properties can the node have. +// We will just define the ones we are working with +export enum NodeTypes { + MemberExpression = "MemberExpression", + Identifier = "Identifier", + VariableDeclarator = "VariableDeclarator", + FunctionDeclaration = "FunctionDeclaration", + FunctionExpression = "FunctionExpression", + AssignmentPattern = "AssignmentPattern", + Literal = "Literal", + ExportDefaultDeclaration = "ExportDefaultDeclaration", + Property = "Property", + ArrowFunctionExpression = "ArrowFunctionExpression", +} diff --git a/app/client/src/globalStyles/CodemirrorHintStyles.ts b/app/client/src/globalStyles/CodemirrorHintStyles.ts index 8b975c4812..eece02db01 100644 --- a/app/client/src/globalStyles/CodemirrorHintStyles.ts +++ b/app/client/src/globalStyles/CodemirrorHintStyles.ts @@ -1,7 +1,7 @@ import { createGlobalStyle } from "styled-components"; import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig"; import { getTypographyByKey, Theme } from "constants/DefaultTheme"; -import { LINT_TOOLTIP_JUSTIFIFIED_LEFT_CLASS } from "components/editorComponents/CodeEditor/constants"; +import { LINT_TOOLTIP_JUSTIFIED_LEFT_CLASS } from "components/editorComponents/CodeEditor/constants"; export const CodemirrorHintStyles = createGlobalStyle<{ editorTheme: EditorTheme; @@ -259,7 +259,7 @@ export const CodemirrorHintStyles = createGlobalStyle<{ padding: 7px 12px; border-radius: 0; - &.${LINT_TOOLTIP_JUSTIFIFIED_LEFT_CLASS}{ + &.${LINT_TOOLTIP_JUSTIFIED_LEFT_CLASS}{ transform: translate(-100%); } diff --git a/app/client/src/pages/Editor/JSEditor/Form.tsx b/app/client/src/pages/Editor/JSEditor/Form.tsx index b17527820f..288b146f92 100644 --- a/app/client/src/pages/Editor/JSEditor/Form.tsx +++ b/app/client/src/pages/Editor/JSEditor/Form.tsx @@ -1,10 +1,14 @@ -import React, { useState } from "react"; -import styled from "styled-components"; -import { JSCollection } from "entities/JSCollection"; +import React, { + ChangeEvent, + useCallback, + useEffect, + useMemo, + useState, +} from "react"; +import { JSAction, JSCollection } from "entities/JSCollection"; import CloseEditor from "components/editorComponents/CloseEditor"; import MoreJSCollectionsMenu from "../Explorer/JSActions/MoreJSActionsMenu"; import { TabComponent } from "components/ads/Tabs"; -import FormLabel from "components/editorComponents/FormLabel"; import CodeEditor from "components/editorComponents/CodeEditor"; import { EditorModes, @@ -14,184 +18,259 @@ import { } from "components/editorComponents/CodeEditor/EditorConfig"; import FormRow from "components/editorComponents/FormRow"; import JSObjectNameEditor from "./JSObjectNameEditor"; -import { updateJSCollectionBody } from "actions/jsPaneActions"; +import { + setActiveJSAction, + startExecutingJSFunction, + updateJSCollectionBody, +} from "actions/jsPaneActions"; import { useDispatch, useSelector } from "react-redux"; import { useParams } from "react-router"; import { ExplorerURLParams } from "../Explorer/helpers"; import JSResponseView from "components/editorComponents/JSResponseView"; -import { EVAL_ERROR_PATH } from "utils/DynamicBindingUtils"; -import { get } from "lodash"; -import { getDataTree } from "selectors/dataTreeSelectors"; -import { EvaluationError } from "utils/DynamicBindingUtils"; +import { isEmpty, isEqual } from "lodash"; import SearchSnippets from "components/ads/SnippetButton"; import { ENTITY_TYPE } from "entities/DataTree/dataTreeFactory"; +import { JSFunctionRun } from "./JSFunctionRun"; +import { AppState } from "reducers"; +import { + getActiveJSActionId, + getIsExecutingJSAction, + getJSActions, + getJSCollectionParseErrors, +} from "selectors/entitiesSelector"; +import { + convertJSActionsToDropdownOptions, + convertJSActionToDropdownOption, + getActionFromJsCollection, + getJSActionOption, + getJSFunctionLineGutter, + JSActionDropdownOption, +} from "./utils"; +import { DropdownOnSelect } from "components/ads"; +import JSFunctionSettingsView from "./JSFunctionSettings"; +import JSObjectHotKeys from "./JSObjectHotKeys"; +import { + ActionButtons, + Form, + FormWrapper, + MainConfiguration, + NameWrapper, + SecondaryWrapper, + TabbedViewContainer, +} from "./styledComponents"; -const Form = styled.form` - display: flex; - flex-direction: column; - height: calc( - 100vh - ${(props) => props.theme.smallHeaderHeight} - - ${(props) => props.theme.backBanner} - ); - overflow: hidden; - width: 100%; - ${FormLabel} { - padding: ${(props) => props.theme.spaces[3]}px; - } - ${FormRow} { - ${FormLabel} { - padding: 0; - width: 100%; - } - } -`; - -const NameWrapper = styled.div` - width: 49%; - display: flex; - align-items: center; - input { - margin: 0; - box-sizing: border-box; - } -`; - -const ActionButtons = styled.div` - justify-self: flex-end; - display: flex; - align-items: center; - - button:last-child { - margin-left: ${(props) => props.theme.spaces[7]}px; - } -`; - -const SecondaryWrapper = styled.div` - display: flex; - flex-direction: column; - height: calc(100% - 50px); -`; -const MainConfiguration = styled.div` - padding: ${(props) => props.theme.spaces[4]}px - ${(props) => props.theme.spaces[10]}px 0px - ${(props) => props.theme.spaces[10]}px; -`; - -export const TabbedViewContainer = styled.div` - flex: 1; - overflow: auto; - position: relative; - height: 100%; - border-top: 2px solid ${(props) => props.theme.colors.apiPane.dividerBg}; - ${FormRow} { - min-height: auto; - padding: ${(props) => props.theme.spaces[0]}px; - & > * { - margin-right: 0px; - } - } - - &&& { - ul.react-tabs__tab-list { - padding: 0px ${(props) => props.theme.spaces[12]}px; - background-color: ${(props) => - props.theme.colors.apiPane.responseBody.bg}; - } - .react-tabs__tab-panel { - height: calc(100% - 36px); - margin-top: 2px; - background-color: ${(props) => props.theme.colors.apiPane.bg}; - } - } -`; interface JSFormProps { - jsAction: JSCollection; - settingsConfig: any; + jsCollection: JSCollection; } type Props = JSFormProps; -function JSEditorForm(props: Props) { +function JSEditorForm({ jsCollection: currentJSCollection }: Props) { const theme = EditorTheme.LIGHT; const [mainTabIndex, setMainTabIndex] = useState(0); const dispatch = useDispatch(); - const currentJSAction = props.jsAction; - const dataTree = useSelector(getDataTree); - const handleOnChange = (event: string) => { - if (currentJSAction) { - dispatch(updateJSCollectionBody(event, currentJSAction.id)); - } - }; const { pageId } = useParams(); - const getErrors = get( - dataTree, - `${currentJSAction.name}.${EVAL_ERROR_PATH}.body`, - [], - ) as EvaluationError[]; + const [disableRunFunctionality, setDisableRunFunctionality] = useState(false); + + // Currently active response (only changes upon execution) + const [activeResponse, setActiveResponse] = useState(null); + const parseErrors = useSelector( + (state: AppState) => + getJSCollectionParseErrors(state, currentJSCollection.name), + isEqual, + ); + const jsActions = useSelector( + (state: AppState) => getJSActions(state, currentJSCollection.id), + isEqual, + ); + const activeJSActionId = useSelector((state: AppState) => + getActiveJSActionId(state, currentJSCollection.id), + ); + + const activeJSAction = getActionFromJsCollection( + activeJSActionId, + currentJSCollection, + ); + + const [selectedJSActionOption, setSelectedJSActionOption] = useState< + JSActionDropdownOption + >(getJSActionOption(activeJSAction, jsActions)); + + const isExecutingCurrentJSAction = useSelector((state: AppState) => + getIsExecutingJSAction( + state, + currentJSCollection.id, + selectedJSActionOption.data?.id || "", + ), + ); + + // Triggered when there is a change in the code editor + const handleEditorChange = (valueOrEvent: ChangeEvent | string) => { + const value: string = + typeof valueOrEvent === "string" + ? valueOrEvent + : valueOrEvent.target.value; + + dispatch(updateJSCollectionBody(value, currentJSCollection.id)); + }; + + // Executes JS action + const executeJSAction = (jsAction: JSAction) => { + setActiveResponse(jsAction); + if (jsAction.id !== selectedJSActionOption.data?.id) + setSelectedJSActionOption(convertJSActionToDropdownOption(jsAction)); + dispatch( + setActiveJSAction({ + jsCollectionId: currentJSCollection.id || "", + jsActionId: jsAction.id || "", + }), + ); + dispatch( + startExecutingJSFunction({ + collectionName: currentJSCollection.name || "", + action: jsAction, + collectionId: currentJSCollection.id || "", + }), + ); + }; + + const handleActiveActionChange = useCallback( + (jsAction: JSAction) => { + if (!jsAction) return; + + // only update when there is a new active action + if (jsAction.id !== selectedJSActionOption.data?.id) { + setSelectedJSActionOption(convertJSActionToDropdownOption(jsAction)); + } + }, + [selectedJSActionOption], + ); + + const JSGutters = useMemo( + () => + getJSFunctionLineGutter( + jsActions, + executeJSAction, + !parseErrors.length, + handleActiveActionChange, + ), + [jsActions, parseErrors, handleActiveActionChange], + ); + + const handleJSActionOptionSelection: DropdownOnSelect = ( + value, + dropDownOption: JSActionDropdownOption, + ) => { + dropDownOption.data && + setSelectedJSActionOption( + convertJSActionToDropdownOption(dropDownOption.data), + ); + }; + + const handleRunAction = ( + event: React.MouseEvent | KeyboardEvent, + ) => { + event.preventDefault(); + selectedJSActionOption.data && executeJSAction(selectedJSActionOption.data); + }; + + useEffect(() => { + if (parseErrors.length || isEmpty(jsActions)) { + setDisableRunFunctionality(true); + } else { + setDisableRunFunctionality(false); + } + setSelectedJSActionOption(getJSActionOption(activeJSAction, jsActions)); + }, [parseErrors, jsActions, activeJSActionId]); + return ( - <> - -
- - - - - - - + + + + + + + + + + + + + + + + + + + ), + }, + { + key: "settings", + title: "Settings", + panelComponent: ( + + ), + }, + ]} /> - - - - - - - handleOnChange(event), - }} - mode={EditorModes.JAVASCRIPT} - placeholder="Let's write some code!" - showLightningMenu={false} - showLineNumbers - size={EditorSize.EXTENDED} - tabBehaviour={TabBehaviour.INDENT} - theme={theme} - /> - ), - }, - ]} + + - - - -
- + + + + ); } diff --git a/app/client/src/pages/Editor/JSEditor/JSFunctionRun.tsx b/app/client/src/pages/Editor/JSEditor/JSFunctionRun.tsx new file mode 100644 index 0000000000..28804d9f9e --- /dev/null +++ b/app/client/src/pages/Editor/JSEditor/JSFunctionRun.tsx @@ -0,0 +1,96 @@ +import React from "react"; +import styled from "styled-components"; +import Dropdown, { + DropdownOnSelect, + DropdownContainer, +} from "components/ads/Dropdown"; +import Button from "components/ads/Button"; +import FlagBadge from "components/utils/FlagBadge"; +import { JSCollection } from "entities/JSCollection"; +import Tooltip from "components/ads/Tooltip"; +import { createMessage, NO_JS_FUNCTION_TO_RUN } from "ce/constants/messages"; +import { StyledButton } from "components/ads/Button"; +import { JSActionDropdownOption } from "./utils"; +import { RUN_BUTTON_DEFAULTS, testLocators } from "./constants"; + +type Props = { + disabled: boolean; + isLoading: boolean; + jsCollection: JSCollection; + onButtonClick: (event: React.MouseEvent) => void; + onSelect: DropdownOnSelect; + options: JSActionDropdownOption[]; + selected: JSActionDropdownOption; + showTooltip: boolean; +}; + +export type DropdownWithCTAWrapperProps = { + isDisabled: boolean; +}; +const disabledStyles = ` +opacity: 0.5; +pointer-events:none; +`; + +const DropdownWithCTAWrapper = styled.div` + display: flex; + + ${StyledButton} { + margin-left: ${RUN_BUTTON_DEFAULTS.GAP_SIZE}; + padding: 0px 20px; + + ${(props) => + props.isDisabled && + ` + ${disabledStyles} + `} + } + ${DropdownContainer} { + ${(props) => + props.isDisabled && + ` + ${disabledStyles} + `} + } +`; + +export function JSFunctionRun({ + disabled, + isLoading, + jsCollection, + onButtonClick, + onSelect, + options, + selected, + showTooltip, +}: Props) { + return ( + + } + height={RUN_BUTTON_DEFAULTS.HEIGHT} + onSelect={onSelect} + options={options} + selected={selected} + selectedHighlightBg={RUN_BUTTON_DEFAULTS.DROPDOWN_HIGHLIGHT_BG} + showLabelOnly + truncateOption + /> + + +