From ffd0595330d8f526d48e18635891f1dcd8402c55 Mon Sep 17 00:00:00 2001 From: Abhijeet <41686026+abhvsn@users.noreply.github.com> Date: Mon, 4 Apr 2022 13:23:00 +0530 Subject: [PATCH 1/6] fix: Fix importing datasource with same name but different plugin issue (#12512) Co-authored-by: Anagh Hegde --- .../ImportExportApplicationServiceCEImpl.java | 37 +++--- .../server/services/GitServiceTest.java | 7 +- .../ImportExportApplicationServiceTests.java | 106 ++++++++++++++++++ 3 files changed, 126 insertions(+), 24 deletions(-) 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 da9412dc4a..90e800d91f 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 @@ -591,9 +591,9 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica /** * This function will take the application reference object to hydrate the application in mongoDB * - * @param organizationId organization to which application is going to be stored - * @param applicationJson application resource which contains necessary information to save the application - * @param applicationId application which needs to be saved with the updated resources + * @param organizationId organization to which application is going to be stored + * @param applicationJson application resource which contains necessary information to import the application + * @param applicationId application which needs to be saved with the updated resources * @return Updated application */ public Mono importApplicationInOrganization(String organizationId, @@ -689,6 +689,7 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica // Check for duplicate datasources to avoid duplicates in target organization .flatMap(datasource -> { + final String importedDatasourceName = datasource.getName(); // Check if the datasource has gitSyncId and if it's already in DB if (datasource.getGitSyncId() != null && savedDatasourcesGitIdToDatasourceMap.containsKey(datasource.getGitSyncId())) { @@ -702,7 +703,11 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica datasource.setPluginId(null); AppsmithBeanUtils.copyNestedNonNullProperties(datasource, existingDatasource); existingDatasource.setStructure(null); - return datasourceService.update(existingDatasource.getId(), existingDatasource); + return datasourceService.update(existingDatasource.getId(), existingDatasource) + .map(datasource1 -> { + datasourceMap.put(importedDatasourceName, datasource1.getId()); + return datasource1; + }); } // This is explicitly copied over from the map we created before @@ -719,11 +724,11 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica updateAuthenticationDTO(datasource, decryptedFields); } - return createUniqueDatasourceIfNotPresent(existingDatasourceFlux, datasource, organizationId, applicationId); - }) - .map(datasource -> { - datasourceMap.put(datasource.getName(), datasource.getId()); - return datasource; + return createUniqueDatasourceIfNotPresent(existingDatasourceFlux, datasource, organizationId) + .map(datasource1 -> { + datasourceMap.put(importedDatasourceName, datasource1.getId()); + return datasource1; + }); }) .collectList(); }) @@ -1726,9 +1731,7 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica */ private Mono createUniqueDatasourceIfNotPresent(Flux existingDatasourceFlux, Datasource datasource, - String organizationId, - String applicationId) { - + String organizationId) { /* 1. If same datasource is present return 2. If unable to find the datasource create a new datasource with unique name and return @@ -1743,16 +1746,8 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica } return existingDatasourceFlux - .map(ds -> { - final DatasourceConfiguration dsAuthConfig = ds.getDatasourceConfiguration(); - if (dsAuthConfig != null && dsAuthConfig.getAuthentication() != null) { - dsAuthConfig.getAuthentication().setAuthenticationResponse(null); - dsAuthConfig.getAuthentication().setAuthenticationType(null); - } - return ds; - }) // For git import exclude datasource configuration - .filter(ds -> applicationId != null ? ds.getName().equals(datasource.getName()) : ds.softEquals(datasource)) + .filter(ds -> ds.getName().equals(datasource.getName()) && datasource.getPluginId().equals(ds.getPluginId())) .next() // Get the first matching datasource, we don't need more than one here. .switchIfEmpty(Mono.defer(() -> { if (datasourceConfig != null && datasourceConfig.getAuthentication() != null) { diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/GitServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/GitServiceTest.java index f6d32b84bd..00bd79ff3b 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/GitServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/GitServiceTest.java @@ -1702,7 +1702,7 @@ public class GitServiceTest { StepVerifier .create(applicationMono) .expectErrorMatches(throwable -> throwable instanceof AppsmithException - && throwable.getMessage().equals(AppsmithError.GIT_ACTION_FAILED.getMessage("checkout", "origin/branchInLocal already exists in remote"))) + && throwable.getMessage().equals(AppsmithError.GIT_ACTION_FAILED.getMessage("checkout", "origin/branchInLocal already exists in local - branchInLocal"))) .verify(); } @@ -2486,7 +2486,7 @@ public class GitServiceTest { @Test @WithUserDetails(value = "api_user") - public void importApplicationFromGit_validRequestWithDuplicateDatasourceOfSameTypeCancelledMidway_Success() throws GitAPIException, IOException { + public void importApplicationFromGit_validRequestWithDuplicateDatasourceOfSameTypeCancelledMidway_Success() { Organization organization = new Organization(); organization.setName("gitImportOrgCancelledMidway"); final String testOrgId = organizationService.create(organization) @@ -2497,6 +2497,7 @@ public class GitServiceTest { GitAuth gitAuth = gitService.generateSSHKey().block(); ApplicationJson applicationJson = createAppJson(filePath).block(); + applicationJson.getExportedApplication().setName(null); applicationJson.getDatasourceList().get(0).setName("db-auth-testGitImportRepo"); String pluginId = pluginRepository.findByPackageName("mongo-plugin").block().getId(); @@ -2521,7 +2522,7 @@ public class GitServiceTest { // Wait for git clone to complete Mono gitConnectedAppFromDbMono = Mono.just(testOrgId) - .flatMap(application -> { + .flatMap(ignore -> { try { // Before fetching the git connected application, sleep for 5 seconds to ensure that the clone // completes 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 3d2068df89..962faf40cf 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 @@ -2182,5 +2182,111 @@ public class ImportExportApplicationServiceTests { }) .verifyComplete(); } + + @Test + @WithUserDetails(value = "api_user") + public void importApplication_datasourceWithSameNameAndDifferentPlugin_importedWithValidActionsAndSuffixedDatasource() { + + ApplicationJson applicationJson = createAppJson("test_assets/ImportExportServiceTest/valid-application.json").block(); + + Organization testOrganization = new Organization(); + testOrganization.setName("Duplicate datasource with different plugin org"); + testOrganization = organizationService.create(testOrganization).block(); + + Datasource testDatasource = new Datasource(); + // Chose any plugin except for mongo, as json static file has mongo plugin for datasource + Plugin postgreSQLPlugin = pluginRepository.findByName("PostgreSQL").block(); + testDatasource.setPluginId(postgreSQLPlugin.getId()); + testDatasource.setOrganizationId(testOrganization.getId()); + final String datasourceName = applicationJson.getDatasourceList().get(0).getName(); + testDatasource.setName(datasourceName); + datasourceService.create(testDatasource).block(); + + final Mono resultMono = importExportApplicationService.importApplicationInOrganization(testOrganization.getId(), applicationJson); + + StepVerifier + .create(resultMono + .flatMap(application -> Mono.zip( + Mono.just(application), + datasourceService.findAllByOrganizationId(application.getOrganizationId(), MANAGE_DATASOURCES).collectList(), + newActionService.findAllByApplicationIdAndViewMode(application.getId(), false, READ_ACTIONS, null).collectList() + ))) + .assertNext(tuple -> { + final Application application = tuple.getT1(); + final List datasourceList = tuple.getT2(); + final List actionList = tuple.getT3(); + + assertThat(application.getName()).isEqualTo("valid_application"); + + List datasourceNameList = new ArrayList<>(); + assertThat(datasourceList).isNotEmpty(); + datasourceList.forEach(datasource -> { + assertThat(datasource.getOrganizationId()).isEqualTo(application.getOrganizationId()); + datasourceNameList.add(datasource.getName()); + }); + // Check if both suffixed and newly imported datasource are present + assertThat(datasourceNameList).contains(datasourceName, datasourceName + " #1"); + + assertThat(actionList).isNotEmpty(); + actionList.forEach(newAction -> { + ActionDTO actionDTO = newAction.getUnpublishedAction(); + assertThat(actionDTO.getDatasource()).isNotNull(); + }); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void importApplication_datasourceWithSameNameAndPlugin_importedWithValidActionsWithoutSuffixedDatasource() { + + ApplicationJson applicationJson = createAppJson("test_assets/ImportExportServiceTest/valid-application.json").block(); + + Organization testOrganization = new Organization(); + testOrganization.setName("Duplicate datasource with same plugin org"); + testOrganization = organizationService.create(testOrganization).block(); + + Datasource testDatasource = new Datasource(); + // Chose plugin same as mongo, as json static file has mongo plugin for datasource + Plugin postgreSQLPlugin = pluginRepository.findByName("MongoDB").block(); + testDatasource.setPluginId(postgreSQLPlugin.getId()); + testDatasource.setOrganizationId(testOrganization.getId()); + final String datasourceName = applicationJson.getDatasourceList().get(0).getName(); + testDatasource.setName(datasourceName); + datasourceService.create(testDatasource).block(); + + final Mono resultMono = importExportApplicationService.importApplicationInOrganization(testOrganization.getId(), applicationJson); + + StepVerifier + .create(resultMono + .flatMap(application -> Mono.zip( + Mono.just(application), + datasourceService.findAllByOrganizationId(application.getOrganizationId(), MANAGE_DATASOURCES).collectList(), + newActionService.findAllByApplicationIdAndViewMode(application.getId(), false, READ_ACTIONS, null).collectList() + ))) + .assertNext(tuple -> { + final Application application = tuple.getT1(); + final List datasourceList = tuple.getT2(); + final List actionList = tuple.getT3(); + + assertThat(application.getName()).isEqualTo("valid_application"); + + List datasourceNameList = new ArrayList<>(); + assertThat(datasourceList).isNotEmpty(); + datasourceList.forEach(datasource -> { + assertThat(datasource.getOrganizationId()).isEqualTo(application.getOrganizationId()); + datasourceNameList.add(datasource.getName()); + }); + // Check that there are no datasources are created with suffix names as datasource's are of same plugin + assertThat(datasourceNameList).contains(datasourceName); + + assertThat(actionList).isNotEmpty(); + actionList.forEach(newAction -> { + ActionDTO actionDTO = newAction.getUnpublishedAction(); + assertThat(actionDTO.getDatasource()).isNotNull(); + }); + }) + .verifyComplete(); + } } From 6653adc30ebdfbfff55f6f057db17b5c29abecd3 Mon Sep 17 00:00:00 2001 From: Parthvi12 <80334441+Parthvi12@users.noreply.github.com> Date: Mon, 4 Apr 2022 13:27:34 +0530 Subject: [PATCH 2/6] test: add tests for git import flow (#12427) * adding basic test for git import * add git import test --- app/client/cypress/fixtures/gitImport.json | 1045 +++++++++++++++++ .../GitImport/GitImport_spec.js | 126 ++ app/client/cypress/support/commands.js | 79 +- 3 files changed, 1248 insertions(+), 2 deletions(-) create mode 100644 app/client/cypress/fixtures/gitImport.json create mode 100644 app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/GitImport/GitImport_spec.js diff --git a/app/client/cypress/fixtures/gitImport.json b/app/client/cypress/fixtures/gitImport.json new file mode 100644 index 0000000000..0584463161 --- /dev/null +++ b/app/client/cypress/fixtures/gitImport.json @@ -0,0 +1,1045 @@ +{ + "clientSchemaVersion": 1, + "serverSchemaVersion": 3, + "exportedApplication": { + "name": "datasourcesWithTED", + "isPublic": false, + "appIsExample": false, + "unreadCommentThreads": 0, + "color": "#C2DAF0", + "icon": "bicycle", + "slug": "datasourceswithted", + "evaluationVersion": 2, + "applicationVersion": 2, + "isManualUpdate": false, + "new": true + }, + "datasourceList": [ + { + "userPermissions": [ + "execute:datasources", + "manage:datasources", + "read:datasources" + ], + "gitSyncId": "6243ea137b9ea67ca53653b0_62453e60377813071bab4e32", + "name": "TEDMongo", + "pluginId": "mongo-plugin", + "invalids": [], + "messages": [], + "isConfigured": true, + "isValid": true, + "new": true + }, + { + "userPermissions": [ + "execute:datasources", + "manage:datasources", + "read:datasources" + ], + "gitSyncId": "6243ea137b9ea67ca53653b0_62453ef0377813071bab4e3a", + "name": "TEDMySQL", + "pluginId": "mysql-plugin", + "invalids": [], + "messages": [], + "isConfigured": true, + "isValid": true, + "new": true + }, + { + "userPermissions": [ + "execute:datasources", + "manage:datasources", + "read:datasources" + ], + "gitSyncId": "6243ea137b9ea67ca53653b0_62453d94377813071bab4e2e", + "name": "TEDPostgres", + "pluginId": "postgres-plugin", + "invalids": [], + "messages": [], + "isConfigured": true, + "isValid": true, + "new": true + } + ], + "pageList": [ + { + "userPermissions": ["read:pages", "manage:pages"], + "gitSyncId": "62453cc2377813071bab4e16_62453cc2377813071bab4e18", + "unpublishedPage": { + "name": "Page1", + "slug": "page1", + "layouts": [ + { + "id": "Page1", + "userPermissions": [], + "dsl": { + "widgetName": "MainContainer", + "backgroundColor": "none", + "rightColumn": 816, + "snapColumns": 64, + "detachFromLayout": true, + "widgetId": "0", + "topRow": 0, + "bottomRow": 1320, + "containerStyle": "none", + "snapRows": 125, + "parentRowSpace": 1, + "type": "CANVAS_WIDGET", + "canExtend": true, + "version": 54, + "minHeight": 1292, + "parentColumnSpace": 1, + "dynamicBindingPathList": [], + "leftColumn": 0, + "children": [ + { + "widgetName": "Table1", + "defaultPageSize": 0, + "columnOrder": [ + "id", + "name", + "createdAt", + "updatedAt", + "status", + "gender", + "avatar", + "email", + "address", + "role", + "dob", + "phoneNo" + ], + "isVisibleDownload": true, + "dynamicPropertyPathList": [], + "displayName": "Table", + "iconSVG": "/static/media/icon.db8a9cbd.svg", + "topRow": 7, + "bottomRow": 36, + "isSortable": true, + "parentRowSpace": 10, + "type": "TABLE_WIDGET", + "defaultSelectedRow": "0", + "hideCard": false, + "animateLoading": true, + "parentColumnSpace": 12.5625, + "dynamicTriggerPathList": [], + "dynamicBindingPathList": [ + { "key": "primaryColumns.status.computedValue" }, + { "key": "tableData" }, + { "key": "primaryColumns.id.computedValue" }, + { "key": "primaryColumns.name.computedValue" }, + { "key": "primaryColumns.createdAt.computedValue" }, + { "key": "primaryColumns.updatedAt.computedValue" }, + { "key": "primaryColumns.gender.computedValue" }, + { "key": "primaryColumns.avatar.computedValue" }, + { "key": "primaryColumns.email.computedValue" }, + { "key": "primaryColumns.address.computedValue" }, + { "key": "primaryColumns.role.computedValue" }, + { "key": "primaryColumns.dob.computedValue" }, + { "key": "primaryColumns.phoneNo.computedValue" } + ], + "leftColumn": 3, + "primaryColumns": { + "status": { + "index": 2, + "width": 150, + "id": "status", + "horizontalAlignment": "LEFT", + "verticalAlignment": "CENTER", + "columnType": "text", + "textSize": "PARAGRAPH", + "enableFilter": true, + "enableSort": true, + "isVisible": true, + "isCellVisible": true, + "isDerived": false, + "label": "status", + "computedValue": "{{Table1.sanitizedTableData.map((currentRow) => ( currentRow.status))}}", + "buttonColor": "#03B365", + "menuColor": "#03B365", + "labelColor": "#FFFFFF" + }, + "id": { + "index": 0, + "width": 150, + "id": "id", + "horizontalAlignment": "LEFT", + "verticalAlignment": "CENTER", + "columnType": "text", + "textSize": "PARAGRAPH", + "enableFilter": true, + "enableSort": true, + "isVisible": true, + "isDisabled": false, + "isCellVisible": true, + "isDerived": false, + "label": "id", + "computedValue": "{{Table1.sanitizedTableData.map((currentRow) => ( currentRow.id))}}" + }, + "name": { + "index": 1, + "width": 150, + "id": "name", + "horizontalAlignment": "LEFT", + "verticalAlignment": "CENTER", + "columnType": "text", + "textSize": "PARAGRAPH", + "enableFilter": true, + "enableSort": true, + "isVisible": true, + "isDisabled": false, + "isCellVisible": true, + "isDerived": false, + "label": "name", + "computedValue": "{{Table1.sanitizedTableData.map((currentRow) => ( currentRow.name))}}" + }, + "createdAt": { + "index": 2, + "width": 150, + "id": "createdAt", + "horizontalAlignment": "LEFT", + "verticalAlignment": "CENTER", + "columnType": "text", + "textSize": "PARAGRAPH", + "enableFilter": true, + "enableSort": true, + "isVisible": true, + "isDisabled": false, + "isCellVisible": true, + "isDerived": false, + "label": "createdAt", + "computedValue": "{{Table1.sanitizedTableData.map((currentRow) => ( currentRow.createdAt))}}" + }, + "updatedAt": { + "index": 3, + "width": 150, + "id": "updatedAt", + "horizontalAlignment": "LEFT", + "verticalAlignment": "CENTER", + "columnType": "text", + "textSize": "PARAGRAPH", + "enableFilter": true, + "enableSort": true, + "isVisible": true, + "isDisabled": false, + "isCellVisible": true, + "isDerived": false, + "label": "updatedAt", + "computedValue": "{{Table1.sanitizedTableData.map((currentRow) => ( currentRow.updatedAt))}}" + }, + "gender": { + "index": 5, + "width": 150, + "id": "gender", + "horizontalAlignment": "LEFT", + "verticalAlignment": "CENTER", + "columnType": "text", + "textSize": "PARAGRAPH", + "enableFilter": true, + "enableSort": true, + "isVisible": true, + "isDisabled": false, + "isCellVisible": true, + "isDerived": false, + "label": "gender", + "computedValue": "{{Table1.sanitizedTableData.map((currentRow) => ( currentRow.gender))}}" + }, + "avatar": { + "index": 6, + "width": 150, + "id": "avatar", + "horizontalAlignment": "LEFT", + "verticalAlignment": "CENTER", + "columnType": "text", + "textSize": "PARAGRAPH", + "enableFilter": true, + "enableSort": true, + "isVisible": true, + "isDisabled": false, + "isCellVisible": true, + "isDerived": false, + "label": "avatar", + "computedValue": "{{Table1.sanitizedTableData.map((currentRow) => ( currentRow.avatar))}}" + }, + "email": { + "index": 7, + "width": 150, + "id": "email", + "horizontalAlignment": "LEFT", + "verticalAlignment": "CENTER", + "columnType": "text", + "textSize": "PARAGRAPH", + "enableFilter": true, + "enableSort": true, + "isVisible": true, + "isDisabled": false, + "isCellVisible": true, + "isDerived": false, + "label": "email", + "computedValue": "{{Table1.sanitizedTableData.map((currentRow) => ( currentRow.email))}}" + }, + "address": { + "index": 8, + "width": 150, + "id": "address", + "horizontalAlignment": "LEFT", + "verticalAlignment": "CENTER", + "columnType": "text", + "textSize": "PARAGRAPH", + "enableFilter": true, + "enableSort": true, + "isVisible": true, + "isDisabled": false, + "isCellVisible": true, + "isDerived": false, + "label": "address", + "computedValue": "{{Table1.sanitizedTableData.map((currentRow) => ( currentRow.address))}}" + }, + "role": { + "index": 9, + "width": 150, + "id": "role", + "horizontalAlignment": "LEFT", + "verticalAlignment": "CENTER", + "columnType": "text", + "textSize": "PARAGRAPH", + "enableFilter": true, + "enableSort": true, + "isVisible": true, + "isDisabled": false, + "isCellVisible": true, + "isDerived": false, + "label": "role", + "computedValue": "{{Table1.sanitizedTableData.map((currentRow) => ( currentRow.role))}}" + }, + "dob": { + "index": 10, + "width": 150, + "id": "dob", + "horizontalAlignment": "LEFT", + "verticalAlignment": "CENTER", + "columnType": "text", + "textSize": "PARAGRAPH", + "enableFilter": true, + "enableSort": true, + "isVisible": true, + "isDisabled": false, + "isCellVisible": true, + "isDerived": false, + "label": "dob", + "computedValue": "{{Table1.sanitizedTableData.map((currentRow) => ( currentRow.dob))}}" + }, + "phoneNo": { + "index": 11, + "width": 150, + "id": "phoneNo", + "horizontalAlignment": "LEFT", + "verticalAlignment": "CENTER", + "columnType": "text", + "textSize": "PARAGRAPH", + "enableFilter": true, + "enableSort": true, + "isVisible": true, + "isDisabled": false, + "isCellVisible": true, + "isDerived": false, + "label": "phoneNo", + "computedValue": "{{Table1.sanitizedTableData.map((currentRow) => ( currentRow.phoneNo))}}" + } + }, + "delimiter": ",", + "key": "pvzcjhkpgi", + "derivedColumns": {}, + "rightColumn": 28, + "textSize": "PARAGRAPH", + "widgetId": "bf8sp3t39v", + "isVisibleFilters": true, + "tableData": "{{PostgresQuery.data}}", + "isVisible": true, + "label": "Data", + "searchKey": "", + "enableClientSideSearch": true, + "version": 3, + "totalRecordsCount": 0, + "parentId": "0", + "renderMode": "CANVAS", + "isLoading": false, + "horizontalAlignment": "LEFT", + "isVisibleSearch": true, + "isVisiblePagination": true, + "verticalAlignment": "CENTER", + "columnSizeMap": { "task": 245, "step": 62, "status": 75 } + }, + { + "widgetName": "Table2", + "defaultPageSize": 0, + "columnOrder": [ + "id", + "configName", + "configJson", + "configVersion", + "updatedAt", + "updatedBy" + ], + "isVisibleDownload": true, + "dynamicPropertyPathList": [], + "displayName": "Table", + "iconSVG": "/static/media/icon.db8a9cbd.svg", + "topRow": 7, + "bottomRow": 36, + "isSortable": true, + "parentRowSpace": 10, + "type": "TABLE_WIDGET", + "defaultSelectedRow": "0", + "hideCard": false, + "animateLoading": true, + "parentColumnSpace": 12.5625, + "dynamicTriggerPathList": [], + "dynamicBindingPathList": [ + { "key": "tableData" }, + { "key": "primaryColumns.id.computedValue" }, + { "key": "primaryColumns.updatedAt.computedValue" }, + { "key": "primaryColumns.configName.computedValue" }, + { "key": "primaryColumns.configJson.computedValue" }, + { "key": "primaryColumns.configVersion.computedValue" }, + { "key": "primaryColumns.updatedBy.computedValue" } + ], + "leftColumn": 35, + "primaryColumns": { + "id": { + "index": 0, + "width": 150, + "id": "id", + "horizontalAlignment": "LEFT", + "verticalAlignment": "CENTER", + "columnType": "text", + "textSize": "PARAGRAPH", + "enableFilter": true, + "enableSort": true, + "isVisible": true, + "isDisabled": false, + "isCellVisible": true, + "isDerived": false, + "label": "id", + "computedValue": "{{Table2.sanitizedTableData.map((currentRow) => ( currentRow.id))}}" + }, + "updatedAt": { + "index": 3, + "width": 150, + "id": "updatedAt", + "horizontalAlignment": "LEFT", + "verticalAlignment": "CENTER", + "columnType": "text", + "textSize": "PARAGRAPH", + "enableFilter": true, + "enableSort": true, + "isVisible": true, + "isDisabled": false, + "isCellVisible": true, + "isDerived": false, + "label": "updatedAt", + "computedValue": "{{Table2.sanitizedTableData.map((currentRow) => ( currentRow.updatedAt))}}" + }, + "configName": { + "index": 1, + "width": 150, + "id": "configName", + "horizontalAlignment": "LEFT", + "verticalAlignment": "CENTER", + "columnType": "text", + "textSize": "PARAGRAPH", + "enableFilter": true, + "enableSort": true, + "isVisible": true, + "isDisabled": false, + "isCellVisible": true, + "isDerived": false, + "label": "configName", + "computedValue": "{{Table2.sanitizedTableData.map((currentRow) => ( currentRow.configName))}}" + }, + "configJson": { + "index": 2, + "width": 150, + "id": "configJson", + "horizontalAlignment": "LEFT", + "verticalAlignment": "CENTER", + "columnType": "text", + "textSize": "PARAGRAPH", + "enableFilter": true, + "enableSort": true, + "isVisible": true, + "isDisabled": false, + "isCellVisible": true, + "isDerived": false, + "label": "configJson", + "computedValue": "{{Table2.sanitizedTableData.map((currentRow) => ( currentRow.configJson))}}" + }, + "configVersion": { + "index": 3, + "width": 150, + "id": "configVersion", + "horizontalAlignment": "LEFT", + "verticalAlignment": "CENTER", + "columnType": "text", + "textSize": "PARAGRAPH", + "enableFilter": true, + "enableSort": true, + "isVisible": true, + "isDisabled": false, + "isCellVisible": true, + "isDerived": false, + "label": "configVersion", + "computedValue": "{{Table2.sanitizedTableData.map((currentRow) => ( currentRow.configVersion))}}" + }, + "updatedBy": { + "index": 5, + "width": 150, + "id": "updatedBy", + "horizontalAlignment": "LEFT", + "verticalAlignment": "CENTER", + "columnType": "text", + "textSize": "PARAGRAPH", + "enableFilter": true, + "enableSort": true, + "isVisible": true, + "isDisabled": false, + "isCellVisible": true, + "isDerived": false, + "label": "updatedBy", + "computedValue": "{{Table2.sanitizedTableData.map((currentRow) => ( currentRow.updatedBy))}}" + } + }, + "delimiter": ",", + "key": "l5v3879d0x", + "derivedColumns": {}, + "rightColumn": 60, + "textSize": "PARAGRAPH", + "widgetId": "kmwa2of08l", + "isVisibleFilters": true, + "tableData": "{{MySQLQuery.data}}", + "isVisible": true, + "label": "Data", + "searchKey": "", + "enableClientSideSearch": true, + "version": 3, + "totalRecordsCount": 0, + "parentId": "0", + "renderMode": "CANVAS", + "isLoading": false, + "horizontalAlignment": "LEFT", + "isVisibleSearch": true, + "isVisiblePagination": true, + "verticalAlignment": "CENTER", + "columnSizeMap": { "task": 245, "step": 62, "status": 75 } + }, + { + "widgetName": "Input1", + "displayName": "Input", + "iconSVG": "/static/media/icon.9f505595.svg", + "topRow": 40, + "bottomRow": 44, + "parentRowSpace": 10, + "autoFocus": false, + "type": "INPUT_WIDGET_V2", + "hideCard": false, + "animateLoading": true, + "parentColumnSpace": 12.5625, + "dynamicTriggerPathList": [], + "resetOnSubmit": true, + "leftColumn": 21, + "dynamicBindingPathList": [{ "key": "defaultText" }], + "labelStyle": "", + "inputType": "TEXT", + "isDisabled": false, + "key": "r3pmi5zs37", + "isRequired": false, + "rightColumn": 41, + "widgetId": "evbu7a7ape", + "isVisible": true, + "label": "", + "version": 2, + "parentId": "0", + "renderMode": "CANVAS", + "isLoading": false, + "iconAlign": "left", + "defaultText": "{{echoAPI.data.headers.info}}" + }, + { + "widgetName": "Input2", + "displayName": "Input", + "iconSVG": "/static/media/icon.9f505595.svg", + "topRow": 49, + "bottomRow": 53, + "parentRowSpace": 10, + "autoFocus": false, + "type": "INPUT_WIDGET_V2", + "hideCard": false, + "animateLoading": true, + "parentColumnSpace": 12.5625, + "dynamicTriggerPathList": [], + "resetOnSubmit": true, + "leftColumn": 21, + "dynamicBindingPathList": [{ "key": "defaultText" }], + "labelStyle": "", + "inputType": "TEXT", + "isDisabled": false, + "key": "r3pmi5zs37", + "isRequired": false, + "rightColumn": 41, + "widgetId": "66waagimtm", + "isVisible": true, + "label": "", + "version": 2, + "parentId": "0", + "renderMode": "CANVAS", + "isLoading": false, + "iconAlign": "left", + "defaultText": "{{JSObject1.myFun1()}}" + } + ] + }, + "layoutOnLoadActions": [ + [ + { + "id": "Page1_MySQLQuery", + "name": "MySQLQuery", + "confirmBeforeExecute": false, + "pluginType": "DB", + "jsonPathKeys": [], + "timeoutInMillisecond": 10000 + }, + { + "id": "Page1_PostgresQuery", + "name": "PostgresQuery", + "confirmBeforeExecute": false, + "pluginType": "DB", + "jsonPathKeys": [], + "timeoutInMillisecond": 10000 + } + ], + [ + { + "id": "Page1_echoAPI", + "name": "echoAPI", + "confirmBeforeExecute": false, + "pluginType": "API", + "jsonPathKeys": [], + "timeoutInMillisecond": 10000 + } + ] + ], + "new": false + } + ], + "userPermissions": [] + }, + "publishedPage": { + "name": "Page1", + "slug": "page1", + "layouts": [ + { + "id": "Page1", + "userPermissions": [], + "dsl": { + "widgetName": "MainContainer", + "backgroundColor": "none", + "rightColumn": 1224.0, + "snapColumns": 16.0, + "detachFromLayout": true, + "widgetId": "0", + "topRow": 0.0, + "bottomRow": 1254.0, + "containerStyle": "none", + "snapRows": 33.0, + "parentRowSpace": 1.0, + "type": "CANVAS_WIDGET", + "canExtend": true, + "version": 4.0, + "minHeight": 1292.0, + "parentColumnSpace": 1.0, + "dynamicBindingPathList": [], + "leftColumn": 0.0, + "children": [] + }, + "new": false + } + ], + "userPermissions": [] + }, + "new": true + } + ], + "publishedDefaultPageName": "Page1", + "unpublishedDefaultPageName": "Page1", + "actionList": [ + { + "id": "Page1_MongoQuery", + "userPermissions": ["read:actions", "execute:actions", "manage:actions"], + "gitSyncId": "62453cc2377813071bab4e16_62453ecd377813071bab4e38", + "pluginType": "DB", + "pluginId": "mongo-plugin", + "unpublishedAction": { + "name": "MongoQuery", + "datasource": { + "id": "TEDMongo", + "userPermissions": [], + "pluginId": "mongo-plugin", + "messages": [], + "isValid": true, + "new": false + }, + "pageId": "Page1", + "actionConfiguration": { + "timeoutInMillisecond": 10000, + "paginationType": "NONE", + "encodeParamsToggle": true, + "formData": { + "command": { "data": "FIND" }, + "aggregate": { "limit": { "data": "10" } }, + "delete": { "limit": { "data": "SINGLE" } }, + "updateMany": { "limit": { "data": "SINGLE" } }, + "smartSubstitution": { "data": true }, + "collection": { "data": "{}" }, + "misc": { + "formToNativeQuery": { + "data": "{\n \"find\": \"{}\",\n \"limit\": 10,\n \"batchSize\": 10\n}\n", + "status": "SUCCESS" + } + } + } + }, + "executeOnLoad": false, + "dynamicBindingPathList": [], + "isValid": true, + "invalids": [], + "messages": [], + "jsonPathKeys": [], + "confirmBeforeExecute": false, + "userPermissions": [], + "validName": "MongoQuery" + }, + "publishedAction": { + "datasource": { + "userPermissions": [], + "messages": [], + "isValid": true, + "new": true + }, + "messages": [], + "confirmBeforeExecute": false, + "userPermissions": [] + }, + "new": false + }, + { + "id": "Page1_echoAPI", + "userPermissions": ["read:actions", "execute:actions", "manage:actions"], + "gitSyncId": "62453cc2377813071bab4e16_62453f75377813071bab4e54", + "pluginType": "API", + "pluginId": "restapi-plugin", + "unpublishedAction": { + "name": "echoAPI", + "datasource": { + "userPermissions": [], + "name": "DEFAULT_REST_DATASOURCE", + "pluginId": "restapi-plugin", + "datasourceConfiguration": { "url": "https://mock-api.appsmith.com" }, + "invalids": [], + "messages": [], + "isValid": true, + "new": true + }, + "pageId": "Page1", + "actionConfiguration": { + "timeoutInMillisecond": 10000, + "paginationType": "NONE", + "path": "/echo/get", + "headers": [{ "key": "info", "value": "this is a test" }], + "encodeParamsToggle": true, + "queryParameters": [], + "httpMethod": "GET", + "pluginSpecifiedTemplates": [{ "value": true }], + "formData": { "apiContentType": "none" } + }, + "executeOnLoad": true, + "dynamicBindingPathList": [], + "isValid": true, + "invalids": [], + "messages": [], + "jsonPathKeys": [], + "confirmBeforeExecute": false, + "userPermissions": [], + "validName": "echoAPI" + }, + "publishedAction": { + "datasource": { + "userPermissions": [], + "messages": [], + "isValid": true, + "new": true + }, + "messages": [], + "confirmBeforeExecute": false, + "userPermissions": [] + }, + "new": false + }, + { + "id": "Page1_PostgresQuery", + "userPermissions": ["read:actions", "execute:actions", "manage:actions"], + "gitSyncId": "6249ed44068394009c56232d_6249f439068394009c56234f", + "pluginType": "DB", + "pluginId": "postgres-plugin", + "unpublishedAction": { + "name": "PostgresQuery", + "datasource": { + "id": "TEDPostgres", + "userPermissions": [], + "pluginId": "postgres-plugin", + "messages": [], + "isValid": true, + "new": false + }, + "pageId": "Page1", + "actionConfiguration": { + "timeoutInMillisecond": 10000, + "paginationType": "NONE", + "encodeParamsToggle": true, + "body": "SELECT * FROM public.\"users\" LIMIT 10;", + "pluginSpecifiedTemplates": [{ "value": true }] + }, + "executeOnLoad": true, + "isValid": true, + "invalids": [], + "messages": [], + "jsonPathKeys": [], + "confirmBeforeExecute": false, + "userPermissions": [], + "validName": "PostgresQuery" + }, + "publishedAction": { + "datasource": { + "userPermissions": [], + "messages": [], + "isValid": true, + "new": true + }, + "messages": [], + "confirmBeforeExecute": false, + "userPermissions": [] + }, + "new": false + }, + { + "id": "Page1_MySQLQuery", + "userPermissions": ["read:actions", "execute:actions", "manage:actions"], + "gitSyncId": "6249ed44068394009c56232d_6249f5af068394009c562351", + "pluginType": "DB", + "pluginId": "mysql-plugin", + "unpublishedAction": { + "name": "MySQLQuery", + "datasource": { + "id": "TEDMySQL", + "userPermissions": [], + "pluginId": "mysql-plugin", + "messages": [], + "isValid": true, + "new": false + }, + "pageId": "Page1", + "actionConfiguration": { + "timeoutInMillisecond": 10000, + "paginationType": "NONE", + "encodeParamsToggle": true, + "body": "SELECT * FROM configs LIMIT 10;", + "pluginSpecifiedTemplates": [{ "value": true }] + }, + "executeOnLoad": true, + "isValid": true, + "invalids": [], + "messages": [], + "jsonPathKeys": [], + "confirmBeforeExecute": false, + "userPermissions": [], + "validName": "MySQLQuery" + }, + "publishedAction": { + "datasource": { + "userPermissions": [], + "messages": [], + "isValid": true, + "new": true + }, + "messages": [], + "confirmBeforeExecute": false, + "userPermissions": [] + }, + "new": false + }, + { + "id": "Page1_JSObject1.myFun1", + "userPermissions": ["read:actions", "execute:actions", "manage:actions"], + "gitSyncId": "6249ed44068394009c56232d_6249f822068394009c562359", + "pluginType": "JS", + "pluginId": "js-plugin", + "unpublishedAction": { + "name": "myFun1", + "fullyQualifiedName": "JSObject1.myFun1", + "datasource": { + "userPermissions": [], + "name": "UNUSED_DATASOURCE", + "pluginId": "js-plugin", + "messages": [], + "isValid": true, + "new": true + }, + "pageId": "Page1", + "collectionId": "Page1_JSObject1", + "actionConfiguration": { + "timeoutInMillisecond": 10000, + "paginationType": "NONE", + "encodeParamsToggle": true, + "body": "() => {\n\t\treturn \"Success\";\n\t}", + "jsArguments": [], + "isAsync": false + }, + "executeOnLoad": false, + "dynamicBindingPathList": [{ "key": "body" }], + "isValid": true, + "invalids": [], + "messages": [], + "jsonPathKeys": ["() => {\n\t\treturn \"Success\";\n\t}"], + "confirmBeforeExecute": false, + "userPermissions": [], + "validName": "JSObject1.myFun1" + }, + "publishedAction": { + "datasource": { + "userPermissions": [], + "messages": [], + "isValid": true, + "new": true + }, + "messages": [], + "confirmBeforeExecute": false, + "userPermissions": [] + }, + "new": false + }, + { + "id": "Page1_JSObject1.myFun2", + "userPermissions": ["read:actions", "execute:actions", "manage:actions"], + "gitSyncId": "6249ed44068394009c56232d_6249f822068394009c56235b", + "pluginType": "JS", + "pluginId": "js-plugin", + "unpublishedAction": { + "name": "myFun2", + "fullyQualifiedName": "JSObject1.myFun2", + "datasource": { + "userPermissions": [], + "name": "UNUSED_DATASOURCE", + "pluginId": "js-plugin", + "messages": [], + "isValid": true, + "new": true + }, + "pageId": "Page1", + "collectionId": "Page1_JSObject1", + "actionConfiguration": { + "timeoutInMillisecond": 10000, + "paginationType": "NONE", + "encodeParamsToggle": true, + "body": "async () => {\n\t\t//use async-await or promises\n\t}", + "jsArguments": [], + "isAsync": true + }, + "executeOnLoad": false, + "dynamicBindingPathList": [{ "key": "body" }], + "isValid": true, + "invalids": [], + "messages": [], + "jsonPathKeys": [ + "async () => {\n\t\t//use async-await or promises\n\t}" + ], + "confirmBeforeExecute": false, + "userPermissions": [], + "validName": "JSObject1.myFun2" + }, + "publishedAction": { + "datasource": { + "userPermissions": [], + "messages": [], + "isValid": true, + "new": true + }, + "messages": [], + "confirmBeforeExecute": false, + "userPermissions": [] + }, + "new": false + } + ], + "actionCollectionList": [ + { + "id": "Page1_JSObject1", + "userPermissions": ["read:actions", "execute:actions", "manage:actions"], + "gitSyncId": "6249ed44068394009c56232d_6249f7e3068394009c562357", + "unpublishedCollection": { + "name": "JSObject1", + "pageId": "Page1", + "pluginId": "js-plugin", + "pluginType": "JS", + "actionIds": [], + "archivedActionIds": [], + "actions": [], + "archivedActions": [], + "body": "export default {\n\tmyVar1: [],\n\tmyVar2: {},\n\tmyFun1: () => {\n\t\treturn \"Success\";\n\t},\n\tmyFun2: async () => {\n\t\t//use async-await or promises\n\t}\n}", + "variables": [ + { "name": "myVar1", "value": [] }, + { "name": "myVar2", "value": {} } + ] + }, + "new": false + } + ], + "invisibleActionFields": { + "Page1_MongoQuery": { + "unpublishedUserSetOnLoad": false, + "publishedUserSetOnLoad": false + }, + "Page1_echoAPI": { + "unpublishedUserSetOnLoad": false, + "publishedUserSetOnLoad": false + }, + "Page1_PostgresQuery": { + "unpublishedUserSetOnLoad": false, + "publishedUserSetOnLoad": false + }, + "Page1_JSObject1.myFun2": { + "unpublishedUserSetOnLoad": false, + "publishedUserSetOnLoad": false + }, + "Page1_JSObject1.myFun1": { + "unpublishedUserSetOnLoad": false, + "publishedUserSetOnLoad": false + }, + "Page1_MySQLQuery": { + "unpublishedUserSetOnLoad": false, + "publishedUserSetOnLoad": false + } + }, + "editModeTheme": { + "name": "Classic", + "displayName": "Classic", + "new": true, + "isSystemTheme": true + }, + "publishedTheme": { + "name": "Classic", + "displayName": "Classic", + "new": true, + "isSystemTheme": true + }, + "publishedLayoutmongoEscapedWidgets": {}, + "unpublishedLayoutmongoEscapedWidgets": {} +} 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 new file mode 100644 index 0000000000..56cef39ba3 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/GitImport/GitImport_spec.js @@ -0,0 +1,126 @@ +import gitSyncLocators from "../../../../locators/gitSyncLocators"; +const homePage = require("../../../../locators/HomePage"); +const reconnectDatasourceModal = require("../../../../locators/ReconnectLocators"); +let repoName; +let appName; + +describe("Git import flow", function() { + before(() => { + cy.NavigateToHome(); + cy.createOrg(); + cy.wait("@createOrg").then((interception) => { + const newOrganizationName = interception.response.body.data.name; + cy.CreateAppForOrg(newOrganizationName, newOrganizationName); + }); + }); + it("Import an app from JSON with Postgres, MySQL, Mongo db", () => { + cy.get(homePage.homeIcon).click(); + cy.get(homePage.optionsIcon) + .first() + .click(); + cy.get(homePage.orgImportAppOption).click({ force: true }); + cy.get(homePage.orgImportAppModal).should("be.visible"); + cy.xpath(homePage.uploadLogo).attachFile("gitImport.json"); + cy.wait("@importNewApplication").then((interception) => { + cy.wait(100); + // should check reconnect modal opening + // const { isPartialImport } = interception.response.body.data; + // if (isPartialImport) { + // should reconnect button + cy.get(reconnectDatasourceModal.Modal).should("be.visible"); + cy.ReconnectDatasource("TEDPostgres"); + cy.wait(1000); + cy.fillPostgresDatasourceForm(); + cy.testSaveDatasource(); + cy.wait(2000); + // commenting until bug12535 is closed + /* cy.ReconnectDatasource("TEDMySQL"); + cy.wait(2000); + cy.fillMySQLDatasourceForm(); + cy.testSaveDatasource(); + cy.wait(2000); + cy.ReconnectDatasource("TEDMongo"); + cy.wait(2000); + cy.fillMongoDatasourceForm(); + cy.testSaveDatasource(); + cy.wait(2000); + // } else { + cy.get(homePage.toastMessage).should( + "contain", + "Application imported successfully", + ); */ + cy.get(reconnectDatasourceModal.SkipToAppBtn).click({ force: true }); + cy.wait(2000); + cy.get(".tbody") + .first() + .should("contain.text", "Test user 7"); + cy.generateUUID().then((uid) => { + repoName = uid; + cy.createTestGithubRepo(repoName); + cy.connectToGitRepo(repoName); + }); + }); + }); + it("Import an app from Git and reconnect Postgres, MySQL and Mongo db ", () => { + cy.NavigateToHome(); + cy.createOrg(); + cy.wait("@createOrg").then((interception) => { + const newOrganizationName = interception.response.body.data.name; + cy.CreateAppForOrg(newOrganizationName, "gitImport"); + }); + cy.get(homePage.homeIcon).click(); + cy.get(homePage.optionsIcon) + .first() + .click(); + cy.get(homePage.orgImportAppOption).click({ force: true }); + cy.get(".t--import-json-card") + .next() + .click(); + cy.importAppFromGit(repoName); + // cy.wait("@importNewApplication").then((interception) => { + cy.wait(100); + // should check reconnect modal opening + // const { isPartialImport } = interception.response.body.data; + // if (isPartialImport) { + // should reconnect button + cy.get(reconnectDatasourceModal.Modal).should("be.visible"); + cy.ReconnectDatasource("TEDPostgres"); + cy.wait(1000); + cy.fillPostgresDatasourceForm(); + cy.testSaveDatasource(); + cy.wait(1000); + /* cy.ReconnectDatasource("TEDMySQL"); + cy.wait(1000); + cy.fillMySQLDatasourceForm(); + cy.testSaveDatasource(); + cy.wait(1000); + cy.ReconnectDatasource("TEDMongo"); + cy.wait(1000); + cy.fillMongoDatasourceForm(); + cy.testSaveDatasource(); + cy.wait(2000); + } else { + cy.get(homePage.toastMessage).should( + "contain", + "Application imported successfully", + ); + } */ + cy.get(reconnectDatasourceModal.SkipToAppBtn).click({ force: true }); + }); + it("Verfiy imported app should have all the data binding visible", () => { + // verify postgres data binded to table + cy.get(".tbody") + .first() + .should("contain.text", "Test user 7"); + // verify MySQL data binded to table + // cy.get(".tbody").last().should("contain.text", "New Config") + // verify api response binded to input widget + cy.xpath("//input[@value='this is a test']"); + // verify js object binded to input widget + cy.xpath("//input[@value='Success']"); + }); + + after(() => { + cy.deleteTestGithubRepo(repoName); + }); +}); diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index 1c9d4af8b2..9f43032d77 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -2341,6 +2341,7 @@ Cypress.Commands.add( cy.get(datasourceEditor["databaseName"]) .clear() .type(datasourceFormData["mongo-databaseName"]); + cy.get(datasourceEditor.sectionAuthentication).click(); // cy.get(datasourceEditor["username"]).type( // datasourceFormData["mongo-username"], // ); @@ -2374,7 +2375,6 @@ Cypress.Commands.add( cy.get(datasourceEditor.databaseName) .clear() .type(databaseName); - cy.get(datasourceEditor.sectionAuthentication).click(); cy.get(datasourceEditor.username).type( datasourceFormData["postgres-username"], @@ -2382,6 +2382,7 @@ Cypress.Commands.add( cy.get(datasourceEditor.password).type( datasourceFormData["postgres-password"], ); + cy.get(datasourceEditor.sectionAuthentication).click(); }, ); @@ -2408,6 +2409,7 @@ Cypress.Commands.add( cy.get(datasourceEditor.password).type( datasourceFormData["mysql-password"], ); + cy.get(datasourceEditor.sectionAuthentication).click(); }, ); @@ -2531,6 +2533,7 @@ Cypress.Commands.add( : datasourceFormData["smtp-host"]; cy.get(datasourceEditor.host).type(hostAddress); cy.get(datasourceEditor.port).type(datasourceFormData["smtp-port"]); + cy.get(datasourceEditor.sectionAuthentication).click(); cy.get(datasourceEditor.username).type(datasourceFormData["smtp-username"]); cy.get(datasourceEditor.password).type(datasourceFormData["smtp-password"]); @@ -3067,7 +3070,7 @@ Cypress.Commands.add("startServerAndRoutes", () => { cy.route("POST", "api/v1/git/connect/*").as("connectGitRepo"); cy.route("POST", "api/v1/git/commit/*").as("commit"); - + cy.route("POST", "/api/v1/git/import/*").as("importFromGit"); cy.route("PUT", "api/v1/collections/actions/refactor").as("renameJsAction"); cy.route("POST", "/api/v1/collections/actions").as("createNewJSCollection"); @@ -3811,6 +3814,78 @@ Cypress.Commands.add( }, ); +Cypress.Commands.add( + "importAppFromGit", + (repo, shouldCommit = true, assertConnectFailure) => { + const testEmail = "test@test.com"; + const testUsername = "testusername"; + const owner = Cypress.env("TEST_GITHUB_USER_NAME"); + + let generatedKey; + cy.intercept( + { + url: "api/v1/git/connect/*", + hostname: window.location.host, + }, + (req) => { + req.headers["origin"] = "Cypress"; + }, + ); + cy.intercept("GET", "api/v1/git/import/keys").as(`generateKey-${repo}`); + cy.get(gitSyncLocators.gitRepoInput).type( + `git@github.com:${owner}/${repo}.git`, + ); + cy.get(gitSyncLocators.generateDeployKeyBtn).click(); + cy.wait(`@generateKey-${repo}`).then((result) => { + generatedKey = result.response.body.data.publicKey; + generatedKey = generatedKey.slice(0, generatedKey.length - 1); + // fetch the generated key and post to the github repo + cy.request({ + method: "POST", + url: `${GITHUB_API_BASE}/repos/${Cypress.env( + "TEST_GITHUB_USER_NAME", + )}/${repo}/keys`, + headers: { + Authorization: `token ${Cypress.env("GITHUB_PERSONAL_ACCESS_TOKEN")}`, + }, + body: { + title: "key0", + key: generatedKey, + }, + }); + + cy.get(gitSyncLocators.useGlobalGitConfig).click(); + + cy.get(gitSyncLocators.gitConfigNameInput).type( + `{selectall}${testUsername}`, + ); + cy.get(gitSyncLocators.gitConfigEmailInput).type( + `{selectall}${testEmail}`, + ); + // click on the connect button and verify + cy.get(gitSyncLocators.connectSubmitBtn).click(); + + if (!assertConnectFailure) { + // check for connect success + cy.wait("@importFromGit").should( + "have.nested.property", + "response.body.responseMeta.status", + 201, + ); + } else { + cy.wait("@importFromGit").then((interception) => { + const status = interception.response.body.responseMeta.status; + expect(status).to.be.gte(400); + }); + } + }); + }, +); + +Cypress.Commands.add("ReconnectDatasource", (datasource) => { + cy.xpath(`//span[text()='${datasource}']`).click(); +}); + Cypress.Commands.add("clearPropertyValue", (value) => { cy.get(".CodeMirror textarea") .eq(value) From 0d7b9df3bd6158efb5906b1f04dd3a31d3c8e29f Mon Sep 17 00:00:00 2001 From: arunvjn <32433245+arunvjn@users.noreply.github.com> Date: Mon, 4 Apr 2022 16:02:51 +0530 Subject: [PATCH 3/6] chore: Changing the clean URLs update description in product updates modal (#12408) --- app/client/src/ce/constants/messages.ts | 8 ++ app/client/src/constants/Colors.tsx | 1 + .../pages/Editor/BottomBar/ManualUpgrades.tsx | 92 ++++++++++++++++--- 3 files changed, 87 insertions(+), 14 deletions(-) diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index ffa207d4c7..8f7d8c6429 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -1149,4 +1149,12 @@ export const CLEAN_URL_UPDATE = { name: () => "Update URLs", shortDesc: () => "All URLs in your applications will update to a new readable format that includes the application and page names.", + description: [ + () => + "All URLs in your applications will be updated to match our new style. This will make your apps easier to find, and URLs easier to remember.", + (url: string) => + `The current app’s URL will be:
${url}`, + ], + disclaimer: () => + "Existing references to appsmith.URL.fullpath and appsmith.URL.pathname properties will behave differently.", }; diff --git a/app/client/src/constants/Colors.tsx b/app/client/src/constants/Colors.tsx index 32e559e521..6d4cee2307 100644 --- a/app/client/src/constants/Colors.tsx +++ b/app/client/src/constants/Colors.tsx @@ -110,6 +110,7 @@ export const Colors = { WARNING_SOLID: "#FEB811", WARNING_SOLID_HOVER: "#EFA903", + WARNING_ORANGE: "#FFF8E2", WARNING_OUTLINE_HOVER: "#FFFAE9", WARNING_GHOST_HOVER: "#FBEED0", diff --git a/app/client/src/pages/Editor/BottomBar/ManualUpgrades.tsx b/app/client/src/pages/Editor/BottomBar/ManualUpgrades.tsx index f4f4a9870c..cab6a96654 100644 --- a/app/client/src/pages/Editor/BottomBar/ManualUpgrades.tsx +++ b/app/client/src/pages/Editor/BottomBar/ManualUpgrades.tsx @@ -7,6 +7,7 @@ import { Category, Icon, IconSize, + IconWrapper, Size, Text, TextType, @@ -18,23 +19,15 @@ import React, { useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { getCurrentApplicationId, + getCurrentPageId, selectApplicationVersion, + selectURLSlugs, } from "selectors/editorSelectors"; import styled from "styled-components"; import { useLocalStorage } from "utils/hooks/localstorage"; import { createMessage, CLEAN_URL_UPDATE } from "@appsmith/constants/messages"; - -const updates = [ - { - name: createMessage(CLEAN_URL_UPDATE.name), - shortDesc: createMessage(CLEAN_URL_UPDATE.shortDesc), - description: [ - "All URLs in your applications will update to a new readable format that includes the application and page names.", - 'Existing references to appsmith.URL.fullpath and appsmith.URL.pathname properties will behave differently.', - ], - version: ApplicationVersion.SLUG_URL, - }, -]; +import { useLocation } from "react-router"; +import DisclaimerIcon from "remixicon-react/ErrorWarningLineIcon"; function RedDot() { return ( @@ -51,6 +44,9 @@ const StyledList = styled.ul` li { font-size: 14px; font-weight: 400; + line-height: 19px; + letter-spacing: -0.24px; + margin: 4px 0; a { color: rgb(248, 106, 43); } @@ -70,16 +66,44 @@ const StyledIconContainer = styled.div` border-radius: 50%; `; +const DisclaimerContainer = styled.div` + padding: 8px 16px; + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: center; + background: ${Colors.WARNING_ORANGE}; + color: ${Colors.BROWN}; + margin: 24px 0 0; +`; + +const BodyContainer = styled.div` + .close-modal > svg { + height: 28px; + width: 28px; + } +`; + function UpdatesModal({ applicationVersion, closeModal, latestVersion, showModal, + updates, }: { showModal: boolean; closeModal: () => void; latestVersion: ApplicationVersion; applicationVersion: ApplicationVersion; + updates: { + name: string; + shortDesc: string; + description: string[]; + version: ApplicationVersion; + disclaimer: { + desc: string; + }; + }[]; }) { const dispatch = useDispatch(); const applicationId = useSelector(getCurrentApplicationId); @@ -97,7 +121,7 @@ function UpdatesModal({ scrollContents width={600} > -
+
@@ -110,6 +134,7 @@ function UpdatesModal({ Product Updates
))} + + + + + +
))}
@@ -157,7 +190,7 @@ function UpdatesModal({ text="Update" />
-
+ ); } @@ -168,6 +201,36 @@ function ManualUpgrades() { "", ); const applicationVersion = useSelector(selectApplicationVersion); + const applicationId = useSelector(getCurrentApplicationId); + const pageId = useSelector(getCurrentPageId); + const { applicationSlug, pageSlug } = useSelector(selectURLSlugs); + const location = useLocation(); + + const updates = React.useMemo( + () => [ + { + name: createMessage(CLEAN_URL_UPDATE.name), + shortDesc: createMessage(CLEAN_URL_UPDATE.shortDesc), + description: CLEAN_URL_UPDATE.description.map((formatter) => + createMessage( + formatter.bind( + null, + window.location.href.replace( + `/applications/${applicationId}/pages/${pageId}`, + `/${applicationSlug}/${pageSlug}-${pageId}`, + ), + ), + ), + ), + disclaimer: { + severity: "MODERATE", + desc: createMessage(CLEAN_URL_UPDATE.disclaimer), + }, + version: ApplicationVersion.SLUG_URL, + }, + ], + [location, applicationSlug, pageSlug, pageId, applicationId], + ); const latestVersion = React.useMemo( () => updates.reduce((max, u) => (max > u.version ? max : u.version), 0), [], @@ -225,6 +288,7 @@ function ManualUpgrades() { }} latestVersion={latestVersion} showModal={showModal} + updates={updates} /> ); From 0f1273aa579b836c1efbb46a69d9c76fff3fae64 Mon Sep 17 00:00:00 2001 From: yatinappsmith <84702014+yatinappsmith@users.noreply.github.com> Date: Mon, 4 Apr 2022 16:37:58 +0530 Subject: [PATCH 4/6] change load docker condition (#12548) --- .github/workflows/test-build-docker-image.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-build-docker-image.yml b/.github/workflows/test-build-docker-image.yml index 842c7c243c..6273e2435b 100644 --- a/.github/workflows/test-build-docker-image.yml +++ b/.github/workflows/test-build-docker-image.yml @@ -530,13 +530,13 @@ jobs: path: app/rts/node_modules/ - name: Build docker image - if: success() && github.ref == 'refs/heads/release' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') + if: steps.run_result.outputs.run_result != 'success' working-directory: "." run: | docker build -t fatcontainer . - name: Load docker image - if: success() && github.ref == 'refs/heads/release' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') + if: steps.run_result.outputs.run_result != 'success' env: APPSMITH_LICENSE_KEY: ${{ secrets.APPSMITH_LICENSE_KEY }} working-directory: "." From 1af6ef2be829418a3506a4526ad8b3625e9a4643 Mon Sep 17 00:00:00 2001 From: arunvjn <32433245+arunvjn@users.noreply.github.com> Date: Mon, 4 Apr 2022 16:41:52 +0530 Subject: [PATCH 5/6] fix: 404 on renaming new pages in apps with legacy URLs (#12547) --- .../Applications/ApplicationURL_spec.js | 27 ++++++++++++------- app/client/cypress/support/commands.js | 1 + app/client/src/RouteBuilder.ts | 2 ++ app/client/src/sagas/ApplicationSagas.tsx | 4 --- app/client/src/sagas/PageSagas.tsx | 11 ++------ app/client/src/store.ts | 15 +++++++++-- app/client/src/utils/helpers.tsx | 2 ++ 7 files changed, 38 insertions(+), 24 deletions(-) diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/ApplicationURL_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/ApplicationURL_spec.js index 342725b93f..9c4637ff37 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/ApplicationURL_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/ApplicationURL_spec.js @@ -94,19 +94,28 @@ describe("Slug URLs", () => { ); }); - cy.get(".t--upgrade").click({ force: true }); - - cy.get(".t--upgrade-confirm").click({ force: true }); - - cy.wait("@getPagesForCreateApp").then((intercept) => { - const { application, pages } = intercept.response.body.data; - const defaultPage = pages.find((p) => p.isDefault); - + cy.Createpage("NewPage"); + cy.get("@currentPageId").then((currentPageId) => { cy.location().should((loc) => { expect(loc.pathname).includes( - `/${application.slug}/${defaultPage.slug}-${defaultPage.id}`, + `/applications/${application.id}/pages/${currentPageId}`, ); }); + + cy.get(".t--upgrade").click({ force: true }); + + cy.get(".t--upgrade-confirm").click({ force: true }); + + cy.wait("@getPagesForCreateApp").then((intercept) => { + const { application, pages } = intercept.response.body.data; + const currentPage = pages.find((p) => p.id === currentPageId); + + cy.location().should((loc) => { + expect(loc.pathname).includes( + `/${application.slug}/${currentPage.slug}-${currentPage.id}`, + ); + }); + }); }); }); }); diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index 9f43032d77..012263f2d4 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -1908,6 +1908,7 @@ Cypress.Commands.add("Createpage", (pageName) => { cy.get(pages.editName).click({ force: true }); cy.get(pages.editInput).type(pageName + "{enter}"); pageidcopy = pageName; + cy.wrap(pageId).as("currentPageId"); } cy.get(generatePage.buildFromScratchActionCard).click(); cy.get("#loading").should("not.exist"); diff --git a/app/client/src/RouteBuilder.ts b/app/client/src/RouteBuilder.ts index bcb5257b5e..f17d8df9e6 100644 --- a/app/client/src/RouteBuilder.ts +++ b/app/client/src/RouteBuilder.ts @@ -80,6 +80,8 @@ export function updateURLFactory(params: Optional) { BASE_URL_BUILDER_PARAMS = { ...BASE_URL_BUILDER_PARAMS, ...params }; } +export const getRouteBuilderParams = () => BASE_URL_BUILDER_PARAMS; + /** * Do not export this method directly. Please write wrappers for your URLs. * Uses applicationVersion attribute to determine whether to use slug URLs or legacy URLs. diff --git a/app/client/src/sagas/ApplicationSagas.tsx b/app/client/src/sagas/ApplicationSagas.tsx index 30a689bb73..8b925aa091 100644 --- a/app/client/src/sagas/ApplicationSagas.tsx +++ b/app/client/src/sagas/ApplicationSagas.tsx @@ -87,7 +87,6 @@ import { failFastApiCalls } from "./InitSagas"; import { Datasource } from "entities/Datasource"; import { GUIDED_TOUR_STEPS } from "pages/Editor/GuidedTour/constants"; import { PLACEHOLDER_APP_SLUG, PLACEHOLDER_PAGE_SLUG } from "constants/routes"; -import { updateSlugNamesInURL } from "utils/helpers"; import { builderURL, generateTemplateURL, viewerURL } from "RouteBuilder"; import { getDefaultPageId as selectDefaultPageId } from "./selectors"; import PageApi from "api/PageApi"; @@ -357,9 +356,6 @@ export function* updateApplicationSaga( type: ReduxActionTypes.CURRENT_APPLICATION_NAME_UPDATE, payload: response.data, }); - updateSlugNamesInURL({ - applicationSlug: response.data.slug, - }); } } } catch (error) { diff --git a/app/client/src/sagas/PageSagas.tsx b/app/client/src/sagas/PageSagas.tsx index e9dc42f33e..74302950ab 100644 --- a/app/client/src/sagas/PageSagas.tsx +++ b/app/client/src/sagas/PageSagas.tsx @@ -52,11 +52,7 @@ import { takeLeading, } from "redux-saga/effects"; import history from "utils/history"; -import { - captureInvalidDynamicBindingPath, - isNameValid, - updateSlugNamesInURL, -} from "utils/helpers"; +import { captureInvalidDynamicBindingPath, isNameValid } from "utils/helpers"; import { extractCurrentDSL } from "utils/WidgetPropsUtils"; import { checkIfMigrationIsNeeded } from "utils/DSLMigrations"; import { @@ -319,7 +315,7 @@ export function* fetchPublishedPageSaga( // Update the canvas yield put(initCanvasLayout(canvasWidgetsPayload)); // set current page - yield put(updateCurrentPage(pageId)); + yield put(updateCurrentPage(pageId, response.data.slug)); // dispatch fetch page success yield put( fetchPublishedPageSuccess( @@ -599,9 +595,6 @@ export function* updatePageSaga(action: ReduxAction) { payload: response.data, }); } - updateSlugNamesInURL({ - pageSlug: response.data.slug, - }); } catch (error) { yield put({ type: ReduxActionErrorTypes.UPDATE_PAGE_ERROR, diff --git a/app/client/src/store.ts b/app/client/src/store.ts index 313f0ed929..49f30b43a3 100644 --- a/app/client/src/store.ts +++ b/app/client/src/store.ts @@ -10,7 +10,8 @@ import { rootSaga } from "sagas"; import { composeWithDevTools } from "redux-devtools-extension/logOnlyInProduction"; import * as Sentry from "@sentry/react"; import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants"; -import { updateURLFactory } from "RouteBuilder"; +import { getRouteBuilderParams, updateURLFactory } from "RouteBuilder"; +import { updateSlugNamesInURL } from "utils/helpers"; const sagaMiddleware = createSagaMiddleware(); const sentryReduxEnhancer = Sentry.createReduxEnhancer({ @@ -42,11 +43,21 @@ const routeParamsMiddleware: Middleware = () => (next: any) => ( case ReduxActionTypes.CURRENT_APPLICATION_NAME_UPDATE: { const { slug } = action.payload; updateURLFactory({ applicationSlug: slug }); + updateSlugNamesInURL({ + applicationSlug: slug, + }); break; } case ReduxActionTypes.SWITCH_CURRENT_PAGE_ID: case ReduxActionTypes.UPDATE_PAGE_SUCCESS: { - const { id, slug } = action.payload; + const id = action.payload.id; + const slug = action.payload.slug; + const { pageId } = getRouteBuilderParams(); + // Update page slug in URL only if the current page is renamed + if (pageId === id) + updateSlugNamesInURL({ + pageSlug: slug, + }); updateURLFactory({ pageId: id, pageSlug: slug }); break; } diff --git a/app/client/src/utils/helpers.tsx b/app/client/src/utils/helpers.tsx index 5c770cf503..7a829a66cb 100644 --- a/app/client/src/utils/helpers.tsx +++ b/app/client/src/utils/helpers.tsx @@ -710,6 +710,8 @@ export const getUpdatedRoute = ( export const updateSlugNamesInURL = (params: Record) => { const { pathname, search } = window.location; + // Do not update old URLs + if (isURLDeprecated(pathname)) return; const newURL = getUpdatedRoute(pathname, params); history.replace(newURL + search); }; From 057817d393e9014db659e7a78fa2a85305bee51e Mon Sep 17 00:00:00 2001 From: f0c1s Date: Mon, 4 Apr 2022 17:11:49 +0530 Subject: [PATCH 6/6] feat: migration changes hint (#12071) * Initial changes * feat: adding new icon and refactoring * feat: add text and test ids * chore: add tests --- app/client/src/assets/icons/ads/js.svg | 5 + app/client/src/ce/constants/messages.test.ts | 18 +++ app/client/src/ce/constants/messages.ts | 12 +- app/client/src/components/ads/Icon.tsx | 10 +- .../src/constants/ReduxActionConstants.tsx | 7 +- .../QuickGitActions/BranchButton.test.tsx | 17 +++ .../gitSync/QuickGitActions/BranchButton.tsx | 17 ++- .../Editor/gitSync/QuickGitActions/index.tsx | 20 +-- .../src/pages/Editor/gitSync/Tabs/Deploy.tsx | 47 ++++-- .../Editor/gitSync/components/GitChanged.tsx | 139 +++++++++++------- .../src/reducers/uiReducers/gitSyncReducer.ts | 1 + 11 files changed, 205 insertions(+), 88 deletions(-) create mode 100644 app/client/src/assets/icons/ads/js.svg create mode 100644 app/client/src/pages/Editor/gitSync/QuickGitActions/BranchButton.test.tsx diff --git a/app/client/src/assets/icons/ads/js.svg b/app/client/src/assets/icons/ads/js.svg new file mode 100644 index 0000000000..8dbdbebc91 --- /dev/null +++ b/app/client/src/assets/icons/ads/js.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/client/src/ce/constants/messages.test.ts b/app/client/src/ce/constants/messages.test.ts index f7780e4fb2..2f29afcf17 100644 --- a/app/client/src/ce/constants/messages.test.ts +++ b/app/client/src/ce/constants/messages.test.ts @@ -1,6 +1,9 @@ import { CANNOT_MERGE_DUE_TO_UNCOMMITTED_CHANGES, CANNOT_PULL_WITH_LOCAL_UNCOMMITTED_CHANGES, + CHANGES_ONLY_MIGRATION, + CHANGES_ONLY_USER, + CHANGES_USER_AND_MIGRATION, COMMIT_AND_PUSH, COMMIT_CHANGES, COMMIT_TO, @@ -256,10 +259,25 @@ describe("git-sync messages", () => { key: "ERROR_GIT_INVALID_REMOTE", value: "Remote repo doesn't exist or is unreachable.", }, + { + key: "CHANGES_ONLY_USER", + value: "Changes since last commit", + }, + { + key: "CHANGES_ONLY_MIGRATION", + value: "Appsmith update changes since last commit", + }, + { + key: "CHANGES_USER_AND_MIGRATION", + value: "Appsmith update and user changes since last commit", + }, ]; const functions = [ CANNOT_MERGE_DUE_TO_UNCOMMITTED_CHANGES, CANNOT_PULL_WITH_LOCAL_UNCOMMITTED_CHANGES, + CHANGES_ONLY_MIGRATION, + CHANGES_ONLY_USER, + CHANGES_USER_AND_MIGRATION, COMMITTING_AND_PUSHING_CHANGES, COMMIT_AND_PUSH, COMMIT_CHANGES, diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index 8f7d8c6429..0799f7d751 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -588,7 +588,6 @@ export const GIT_DISCONNECT_POPUP_MAIN_HEADING = () => `Are you sure ?`; export const GIT_CONNECTION = () => "Git Connection"; export const GIT_IMPORT = () => "Git Import"; -export const DEPLOY = () => "Deploy"; export const MERGE = () => "Merge"; export const GIT_SETTINGS = () => "Git Settings"; export const CONNECT_TO_GIT = () => "Connect to git repository"; @@ -618,7 +617,6 @@ export const CHECK_DP = () => "CHECK"; export const DEPLOY_TO_CLOUD = () => "Deploy to cloud"; export const DEPLOY_WITHOUT_GIT = () => "Deploy your application without version control"; -export const DEPLOY_YOUR_APPLICATION = () => "Deploy your application"; export const COMMIT_CHANGES = () => "Commit changes"; export const COMMIT_TO = () => "Commit to"; export const COMMIT_AND_PUSH = () => "Commit & push"; @@ -735,6 +733,16 @@ export const CONNECTING_TO_REPO_DISABLED = () => export const DURING_ONBOARDING_TOUR = () => "during the onboarding tour"; export const MERGED_SUCCESSFULLY = () => "Merged successfully"; +// GIT DEPLOY begin +export const DEPLOY = () => "Deploy"; +export const DEPLOY_YOUR_APPLICATION = () => "Deploy your application"; +export const CHANGES_ONLY_USER = () => "Changes since last commit"; +export const CHANGES_ONLY_MIGRATION = () => + "Appsmith update changes since last commit"; +export const CHANGES_USER_AND_MIGRATION = () => + "Appsmith update and user changes since last commit"; +// GIT DEPLOY end + // GIT ERRORS begin export const ERROR_GIT_AUTH_FAIL = () => "Please make sure that regenerated SSH key is added and has write access to the repo."; diff --git a/app/client/src/components/ads/Icon.tsx b/app/client/src/components/ads/Icon.tsx index 99a435127e..e3f62ae100 100644 --- a/app/client/src/components/ads/Icon.tsx +++ b/app/client/src/components/ads/Icon.tsx @@ -70,9 +70,10 @@ import { ReactComponent as WorkspaceIcon } from "assets/icons/ads/organizationIc import { ReactComponent as SettingIcon } from "assets/icons/control/settings.svg"; import { ReactComponent as DropdownIcon } from "assets/icons/ads/dropdown.svg"; import { ReactComponent as ChatIcon } from "assets/icons/ads/app-icons/chat.svg"; +import { ReactComponent as JsIcon } from "assets/icons/ads/js.svg"; import styled from "styled-components"; -import { CommonComponentProps, Classes } from "./common"; +import { Classes, CommonComponentProps } from "./common"; import { noop } from "lodash"; import { theme } from "constants/DefaultTheme"; import Spinner from "./Spinner"; @@ -90,6 +91,7 @@ import CheckLineIcon from "remixicon-react/CheckLineIcon"; import CloseLineIcon from "remixicon-react/CloseLineIcon"; import CloseCircleIcon from "remixicon-react/CloseCircleFillIcon"; import CommentContextMenu from "remixicon-react/More2FillIcon"; +import More2FillIcon from "remixicon-react/More2FillIcon"; import CompassesLine from "remixicon-react/CompassesLineIcon"; import ContextMenuIcon from "remixicon-react/MoreFillIcon"; import CreateNewIcon from "remixicon-react/AddLineIcon"; @@ -102,8 +104,10 @@ import Download from "remixicon-react/DownloadCloud2LineIcon"; import DuplicateIcon from "remixicon-react/FileCopyLineIcon"; import EditIcon from "remixicon-react/PencilFillIcon"; import EditLineIcon from "remixicon-react/EditLineIcon"; +import EditUnderlineIcon from "remixicon-react/EditLineIcon"; import Emoji from "remixicon-react/EmotionLineIcon"; import ExpandMore from "remixicon-react/ArrowDownSLineIcon"; +import DownArrowIcon from "remixicon-react/ArrowDownSLineIcon"; import ExpandLess from "remixicon-react/ArrowUpSLineIcon"; import EyeOn from "remixicon-react/EyeLineIcon"; import EyeOff from "remixicon-react/EyeOffLineIcon"; @@ -122,7 +126,6 @@ import KeyIcon from "remixicon-react/Key2LineIcon"; import LeftArrowIcon2 from "remixicon-react/ArrowLeftSLineIcon"; import Link2 from "remixicon-react/LinkIcon"; import LeftArrowIcon from "remixicon-react/ArrowLeftLineIcon"; -import More2FillIcon from "remixicon-react/More2FillIcon"; import NewsPaperLine from "remixicon-react/NewspaperLineIcon"; import OvalCheck from "remixicon-react/CheckboxCircleLineIcon"; import OvalCheckFill from "remixicon-react/CheckboxCircleFillIcon"; @@ -137,10 +140,8 @@ import Trash from "remixicon-react/DeleteBinLineIcon"; import UpArrow from "remixicon-react/ArrowUpSFillIcon"; import WarningIcon from "remixicon-react/ErrorWarningFillIcon"; import WarningLineIcon from "remixicon-react/ErrorWarningLineIcon"; -import EditUnderlineIcon from "remixicon-react/EditLineIcon"; import LogoutIcon from "remixicon-react/LogoutBoxRLineIcon"; import ShareLineIcon from "remixicon-react/ShareLineIcon"; -import DownArrowIcon from "remixicon-react/ArrowDownSLineIcon"; import LoaderLineIcon from "remixicon-react/LoaderLineIcon"; import WidgetIcon from "remixicon-react/FunctionLineIcon"; import RefreshLineIcon from "remixicon-react/RefreshLineIcon"; @@ -344,6 +345,7 @@ const ICON_LOOKUP = { hamburger: , help: , info: , + js: , key: , lightning: , link: , diff --git a/app/client/src/constants/ReduxActionConstants.tsx b/app/client/src/constants/ReduxActionConstants.tsx index d99c022acb..84030fc0db 100644 --- a/app/client/src/constants/ReduxActionConstants.tsx +++ b/app/client/src/constants/ReduxActionConstants.tsx @@ -683,8 +683,8 @@ export const ReduxActionTypes = { SET_TEMPLATE_NOTIFICATION_SEEN: "SET_TEMPLATE_NOTIFICATION_SEEN", GET_TEMPLATE_NOTIFICATION_SEEN: "GET_TEMPLATE_NOTIFICATION_SEEN", GET_SIMILAR_TEMPLATES_INIT: "GET_SIMILAR_TEMPLATES_INIT", - GET_SIMILAR_TEMPLATES_SUCCESS: "GET_SIMILAR_TEMPLATES_SUCCESS", - /* This action constants is for identifying the status of the updates of the entities */ + GET_SIMILAR_TEMPLATES_SUCCESS: + "GET_SIMILAR_TEMPLATES_SUCCESS" /* This action constants is for identifying the status of the updates of the entities */, ENTITY_UPDATE_STARTED: "ENTITY_UPDATE_STARTED", ENTITY_UPDATE_SUCCESS: "ENTITY_UPDATE_SUCCESS", FETCH_PLUGIN_AND_JS_ACTIONS_SUCCESS: "FETCH_PLUGIN_AND_JS_ACTIONS_SUCCESS", @@ -925,6 +925,7 @@ export interface PromisePayload { reject: any; resolve: any; } + export interface ReduxActionWithPromise extends ReduxAction { payload: T & PromisePayload; } @@ -989,6 +990,8 @@ export interface ApplicationPayload { modifiedAt?: string; pages: ApplicationPagePayload[]; applicationVersion: ApplicationVersion; + isAutoUpdate?: boolean; + isManualUpdate?: boolean; } export type OrganizationDetails = { diff --git a/app/client/src/pages/Editor/gitSync/QuickGitActions/BranchButton.test.tsx b/app/client/src/pages/Editor/gitSync/QuickGitActions/BranchButton.test.tsx new file mode 100644 index 0000000000..58fa702ca0 --- /dev/null +++ b/app/client/src/pages/Editor/gitSync/QuickGitActions/BranchButton.test.tsx @@ -0,0 +1,17 @@ +import { render, screen } from "test/testUtils"; +import BranchButton from "./BranchButton"; +import React from "react"; + +describe("BranchButton", () => { + it("renders properly", async () => { + render(); + const buttonContainer = await screen.queryByTestId( + "t--branch-button-container", + ); + expect(buttonContainer).not.toBeNull(); + const currentBranch = await screen.queryByTestId( + "t--branch-button-currentBranch", + ); + expect(currentBranch?.innerHTML).toContain("*"); + }); +}); diff --git a/app/client/src/pages/Editor/gitSync/QuickGitActions/BranchButton.tsx b/app/client/src/pages/Editor/gitSync/QuickGitActions/BranchButton.tsx index c59e31fd4b..241b1cfd2d 100644 --- a/app/client/src/pages/Editor/gitSync/QuickGitActions/BranchButton.tsx +++ b/app/client/src/pages/Editor/gitSync/QuickGitActions/BranchButton.tsx @@ -20,19 +20,24 @@ import AnalyticsUtil from "utils/AnalyticsUtil"; const ButtonContainer = styled.div` display: flex; align-items: center; + & .label { color: ${(props) => props.theme.colors.editorBottomBar.branchBtnText}; ${(props) => getTypographyByKey(props, "p1")}; line-height: 18px; } + & .icon { height: 24px; } + margin: 0 ${(props) => props.theme.spaces[4]}px; cursor: pointer; + &:hover svg path { fill: ${Colors.CHARCOAL}; } + & .label { width: 100px; overflow: hidden; @@ -57,6 +62,7 @@ function BranchButton() { return ( } + data-testid={"t--git-branch-button-popover"} hasBackdrop isOpen={isOpen} minimal @@ -78,11 +84,18 @@ function BranchButton() { hoverOpenDelay={1} position={Position.TOP_LEFT} > - +
-
+
{currentBranch} {!status?.isClean && "*"}
diff --git a/app/client/src/pages/Editor/gitSync/QuickGitActions/index.tsx b/app/client/src/pages/Editor/gitSync/QuickGitActions/index.tsx index d76bd9c6ec..2ade4e6bdd 100644 --- a/app/client/src/pages/Editor/gitSync/QuickGitActions/index.tsx +++ b/app/client/src/pages/Editor/gitSync/QuickGitActions/index.tsx @@ -72,18 +72,19 @@ const QuickActionButtonContainer = styled.div<{ disabled?: boolean }>` .count { position: absolute; - width: 20px; - height: 20px; + height: ${(props) => props.theme.spaces[7]}px; display: flex; justify-content: center; align-items: center; color: ${Colors.WHITE}; background-color: ${Colors.BLACK}; - top: -8px; - left: 18px; - border-radius: 50%; + top: ${(props) => -1 * props.theme.spaces[3]}px; + left: ${(props) => props.theme.spaces[8]}px; + border-radius: ${(props) => props.theme.spaces[3]}px; ${(props) => getTypographyByKey(props, "p3")}; z-index: 1; + padding: ${(props) => props.theme.spaces[1]}px + ${(props) => props.theme.spaces[2]}px; } `; @@ -91,15 +92,6 @@ const capitalizeFirstLetter = (string = " ") => { return string.charAt(0).toUpperCase() + string.toLowerCase().slice(1); }; -// const SpinnerContainer = styled.div` -// margin-left: ${(props) => props.theme.spaces[2]}px; -// display: flex; -// align-items: center; -// justify-content: center; -// width: 29px; -// height: 26px; -// `; - function QuickActionButton({ className = "", count = 0, diff --git a/app/client/src/pages/Editor/gitSync/Tabs/Deploy.tsx b/app/client/src/pages/Editor/gitSync/Tabs/Deploy.tsx index e541d46869..ec9f92ba31 100644 --- a/app/client/src/pages/Editor/gitSync/Tabs/Deploy.tsx +++ b/app/client/src/pages/Editor/gitSync/Tabs/Deploy.tsx @@ -1,11 +1,14 @@ import React, { useEffect, useRef, useState } from "react"; -import { Title } from "../components/StyledComponents"; +import { Space, Title } from "../components/StyledComponents"; import { - DEPLOY_YOUR_APPLICATION, - COMMIT_TO, - createMessage, + CHANGES_ONLY_MIGRATION, + CHANGES_ONLY_USER, + CHANGES_USER_AND_MIGRATION, COMMIT_AND_PUSH, + COMMIT_TO, COMMITTING_AND_PUSHING_CHANGES, + createMessage, + DEPLOY_YOUR_APPLICATION, FETCH_GIT_STATUS, GIT_NO_UPDATED_TOOLTIP, GIT_UPSTREAM_CHANGES, @@ -18,18 +21,17 @@ import Button, { Size } from "components/ads/Button"; import { LabelContainer } from "components/ads/Checkbox"; import { + getConflictFoundDocUrlDeploy, + getGitCommitAndPushError, getGitStatus, - getIsFetchingGitStatus, + getIsCommitSuccessful, getIsCommittingInProgress, + getIsFetchingGitStatus, getIsPullingProgress, getPullFailed, - getGitCommitAndPushError, getUpstreamErrorDocUrl, - getConflictFoundDocUrlDeploy, } from "selectors/gitSyncSelectors"; import { useDispatch, useSelector } from "react-redux"; - -import { Space } from "../components/StyledComponents"; import { Colors } from "constants/Colors"; import { getTypographyByKey, Theme } from "constants/DefaultTheme"; @@ -40,7 +42,6 @@ import { fetchGitStatusInit, gitPullInit, } from "actions/gitSyncActions"; -import { getIsCommitSuccessful } from "selectors/gitSyncSelectors"; import StatusLoader from "../components/StatusLoader"; import { clearCommitSuccessfulState } from "../../../../actions/gitSyncActions"; import Statusbar, { @@ -56,11 +57,15 @@ import Icon, { IconSize } from "components/ads/Icon"; import { isMac } from "utils/helpers"; import AnalyticsUtil from "utils/AnalyticsUtil"; -import { getApplicationLastDeployedAt } from "selectors/editorSelectors"; +import { + getApplicationLastDeployedAt, + getCurrentApplication, +} from "selectors/editorSelectors"; import GIT_ERROR_CODES from "constants/GitErrorCodes"; import useAutoGrow from "utils/hooks/useAutoGrow"; const Section = styled.div` + margin-top: ${(props) => props.theme.spaces[11]}px; margin-bottom: ${(props) => props.theme.spaces[11]}px; `; @@ -132,9 +137,21 @@ function Deploy() { const currentBranch = gitMetaData?.branchName; const dispatch = useDispatch(); + const currentApplication = useSelector(getCurrentApplication); + const isAutoUpdate = currentApplication?.isAutoUpdate || false; + const isManualUpdate = currentApplication?.isManualUpdate || true; + const changeReason = isAutoUpdate + ? isManualUpdate + ? CHANGES_USER_AND_MIGRATION + : CHANGES_ONLY_MIGRATION + : CHANGES_ONLY_USER; + const changeReasonText = createMessage(changeReason); + const handleCommit = (doPush: boolean) => { AnalyticsUtil.logEvent("GS_COMMIT_AND_PUSH_BUTTON_CLICK", { source: "GIT_DEPLOY_MODAL", + isAutoUpdate, + isManualUpdate, }); if (currentBranch) { dispatch( @@ -197,9 +214,15 @@ function Deploy() { const autogrowHeight = useAutoGrow(commitMessageDisplay, 37); return ( - + {createMessage(DEPLOY_YOUR_APPLICATION)}
+ + {changeReasonText} + diff --git a/app/client/src/pages/Editor/gitSync/components/GitChanged.tsx b/app/client/src/pages/Editor/gitSync/components/GitChanged.tsx index fd741df21d..ecab2133b7 100644 --- a/app/client/src/pages/Editor/gitSync/components/GitChanged.tsx +++ b/app/client/src/pages/Editor/gitSync/components/GitChanged.tsx @@ -3,97 +3,132 @@ import styled from "constants/DefaultTheme"; import { Classes } from "components/ads/common"; import Text, { TextType } from "components/ads/Text"; import { Colors } from "constants/Colors"; -import Icon, { IconName, IconSize } from "components/ads/Icon"; +import Icon, { IconSize } from "components/ads/Icon"; import { useSelector } from "react-redux"; import { getGitStatus, getIsFetchingGitStatus, } from "selectors/gitSyncSelectors"; +import { GitStatusData } from "../../../../reducers/uiReducers/gitSyncReducer"; -const Skeleton = styled.div` - width: 135px; +const DummyChange = styled.div` + width: 50%; height: ${(props) => props.theme.spaces[9]}px; background: linear-gradient( 90deg, ${Colors.GREY_2} 0%, rgba(240, 240, 240, 0) 100% ); - margin-right: ${(props) => props.theme.spaces[8] + 5}px; + margin-top: ${(props) => props.theme.spaces[7]}px; + margin-bottom: ${(props) => props.theme.spaces[7]}px; `; const Wrapper = styled.div` - width: 178px; height: ${(props) => props.theme.spaces[9]}px; + margin-bottom: ${(props) => props.theme.spaces[7]}px; display: flex; + .${Classes.ICON} { margin-right: ${(props) => props.theme.spaces[3]}px; } + .${Classes.TEXT} { padding-top: ${(props) => props.theme.spaces[1] - 2}px; } `; -const GitChangedRow = styled.div` - display: flex; - align-items: center; +const Statuses = styled.div` + margin-top: ${(props) => props.theme.spaces[7]}px; margin-bottom: ${(props) => props.theme.spaces[11]}px; `; export enum Kind { - widget = "widget", - query = "query", - commit = "commit", - // pullRequest = "pullRequest", + WIDGET = "WIDGET", + QUERY = "QUERY", + COMMIT = "COMMIT", + JS_OBJECT = "JS_OBJECT", } -type GitSyncProps = { - type: Kind; +type StatusProps = { + iconName: string; + message: string; + hasValue: boolean; }; -function GitStatus(props: GitSyncProps) { - const { type } = props; - const status: any = useSelector(getGitStatus); - const loading = useSelector(getIsFetchingGitStatus); - // const loading = true; - let message = "", - iconName: IconName; - switch (type) { - case Kind.widget: - message = `${status?.modifiedPages || 0} page${ - (status?.modifiedPages || 0) === 1 ? "" : "s" - } updated`; - iconName = "widget"; - break; - case Kind.query: - message = `${status?.modifiedQueries || 0} ${ - (status?.modifiedQueries || 0) === 1 ? "query" : "queries" - } modified`; - iconName = "query"; - break; - case Kind.commit: - message = `${status?.aheadCount || 0} commit${ - (status?.aheadCount || 0) === 1 ? "" : "s" - } to push`; - iconName = "git-commit"; - break; - } - return loading ? ( - - ) : ( +type StatusMap = { + [key in Kind]: (status: GitStatusData) => StatusProps; +}; + +const STATUS_MAP: StatusMap = { + [Kind.WIDGET]: (status: GitStatusData) => ({ + message: `${status?.modifiedPages || 0} ${ + (status?.modifiedPages || 0) <= 1 ? "page" : "pages" + } updated`, + iconName: "widget", + hasValue: (status?.modifiedPages || 0) > 0, + }), + [Kind.QUERY]: (status: GitStatusData) => ({ + message: `${status?.modifiedQueries || 0} ${ + (status?.modifiedQueries || 0) <= 1 ? "query" : "queries" + } modified`, + iconName: "query", + hasValue: (status?.modifiedQueries || 0) > 0, + }), + [Kind.COMMIT]: (status: GitStatusData) => ({ + message: commitMessage(status), + iconName: "git-commit", + hasValue: (status?.aheadCount || 0) > 0 || (status?.behindCount || 0) > 0, + }), + [Kind.JS_OBJECT]: (status: GitStatusData) => ({ + message: `${status?.modifiedJSObjects || 0} JS ${ + (status?.modifiedJSObjects || 0) <= 1 ? "Object" : "Objects" + } modified`, + iconName: "js", + hasValue: (status?.modifiedJSObjects || 0) > 0, + }), +}; + +function commitMessage(status: GitStatusData) { + const aheadCount = status?.aheadCount || 0; + const behindCount = status?.behindCount || 0; + const aheadMessage = + aheadCount > 0 + ? (aheadCount || 0) === 1 + ? `${aheadCount || 0} commit ahead` + : `${aheadCount || 0} commits ahead` + : null; + const behindMessage = + behindCount > 0 + ? (behindCount || 0) === 1 + ? `${behindCount || 0} commit behind` + : `${behindCount || 0} commits behind ` + : null; + return [aheadMessage, behindMessage].filter((i) => i !== null).join(" and "); +} + +function Status(props: Partial) { + const { iconName, message } = props; + + return ( - + {message} ); } export default function GitChanged() { - const gitStatus: any = useSelector(getGitStatus); - return ( - - - - {gitStatus?.aheadCount > 0 && } - + const status: GitStatusData = useSelector(getGitStatus) as GitStatusData; + const loading = useSelector(getIsFetchingGitStatus); + const statuses = [Kind.WIDGET, Kind.QUERY, Kind.COMMIT, Kind.JS_OBJECT] + .map((type: Kind) => STATUS_MAP[type](status)) + .map((s) => + s.hasValue ? : null, + ) + .filter((s) => !!s); + return loading ? ( + + ) : ( + {statuses} ); } diff --git a/app/client/src/reducers/uiReducers/gitSyncReducer.ts b/app/client/src/reducers/uiReducers/gitSyncReducer.ts index 751a1a65c7..05ca1ecff5 100644 --- a/app/client/src/reducers/uiReducers/gitSyncReducer.ts +++ b/app/client/src/reducers/uiReducers/gitSyncReducer.ts @@ -450,6 +450,7 @@ export type GitStatusData = { modifiedPages: number; modifiedQueries: number; remoteBranch: string; + modifiedJSObjects: number; }; type GitErrorPayloadType = {