From 88ec7c7c5008955297a45df7d814fbe93e76acbe Mon Sep 17 00:00:00 2001 From: sneha122 Date: Fri, 13 Dec 2024 14:09:53 +0530 Subject: [PATCH 1/8] fix: gsheets all sheets option enabled behind a feature flag (#37942) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description This PR adds back all sheets option which had been removed in PR: https://github.com/appsmithorg/appsmith/pull/36125 Fixes #38002 _or_ Fixes `Issue URL` > [!WARNING] > _If no issue exists, please create an issue first, and check with the maintainers if the issue is valid._ ## Automation /ok-to-test tags="@tag.Datasource, @tag.Widget" ### :mag: Cypress test results > [!TIP] > 🟒 🟒 🟒 All cypress tests have passed! πŸŽ‰ πŸŽ‰ πŸŽ‰ > Workflow run: > Commit: e177b648fd593b8ccc8af88c6d90c3958d7c9752 > Cypress dashboard. > Tags: `@tag.Datasource, @tag.Widget` > Spec: >
Fri, 13 Dec 2024 08:27:08 UTC ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [x] No ## Summary by CodeRabbit - **New Features** - Added new options for Google Sheets permissions, enhancing user selection capabilities. - Introduced a feature flag to control the visibility of new Google Sheets options. - **Bug Fixes** - Improved handling of feature flags in the datasource section rendering, ensuring correct visibility based on flags. - **Documentation** - Updated test specifications for Google Sheets datasource to include new dropdown options. - **Chores** - Enhanced the logic for setting default values in the RadioButtonControl component. --------- Co-authored-by: β€œsneha122” <β€œsneha@appsmith.com”> Co-authored-by: Alex Golovanov --- .../ServerSide/QueryPane/GoogleSheets_spec.ts | 8 ++++ .../support/Pages/IDE/BottomTabs/Response.ts | 2 + app/client/src/ce/entities/FeatureFlag.ts | 3 ++ .../formControls/RadioButtonControl.tsx | 5 ++- .../DataSourceEditor/DatasourceSection.tsx | 17 ++++++++- .../external/enums/FeatureFlagEnum.java | 1 + .../src/main/resources/form.json | 37 +++++++++++++++++++ 7 files changed, 70 insertions(+), 3 deletions(-) diff --git a/app/client/cypress/e2e/Regression/ServerSide/QueryPane/GoogleSheets_spec.ts b/app/client/cypress/e2e/Regression/ServerSide/QueryPane/GoogleSheets_spec.ts index 0c81622a800e..22db9f0dc7ae 100644 --- a/app/client/cypress/e2e/Regression/ServerSide/QueryPane/GoogleSheets_spec.ts +++ b/app/client/cypress/e2e/Regression/ServerSide/QueryPane/GoogleSheets_spec.ts @@ -1,3 +1,4 @@ +import { featureFlagIntercept } from "../../../../support/Objects/FeatureFlags"; import { dataSources, deployMode, @@ -23,6 +24,13 @@ describe( function () { let pluginName = "Google Sheets"; + before(() => { + // intercept features call gsheet all sheets disabled + featureFlagIntercept({ + release_gs_all_sheets_options_enabled: false, + }); + }); + it("1. Verify GSheets dropdown options", function () { dataSources.NavigateToDSCreateNew(); dataSources.CreatePlugIn("Google Sheets"); diff --git a/app/client/cypress/support/Pages/IDE/BottomTabs/Response.ts b/app/client/cypress/support/Pages/IDE/BottomTabs/Response.ts index 2e741833758f..c42006861c34 100644 --- a/app/client/cypress/support/Pages/IDE/BottomTabs/Response.ts +++ b/app/client/cypress/support/Pages/IDE/BottomTabs/Response.ts @@ -24,6 +24,7 @@ class Response { } public openResponseTypeMenu() { + this.switchToResponseTab(); cy.get(this.locators.responseDataContainer).realHover(); cy.get(this.locators.responseTypeMenuTrigger).click({ force: true }); } @@ -46,6 +47,7 @@ class Response { } public validateResponseStatus(status: string): void { + this.switchToResponseTab(); cy.get(this.locators.responseStatusInfo).trigger("mouseover"); cy.get(this.locators.responseStatusInfoTooltip).should( "include.text", diff --git a/app/client/src/ce/entities/FeatureFlag.ts b/app/client/src/ce/entities/FeatureFlag.ts index 0782223bca52..de41ab80134d 100644 --- a/app/client/src/ce/entities/FeatureFlag.ts +++ b/app/client/src/ce/entities/FeatureFlag.ts @@ -46,6 +46,8 @@ export const FEATURE_FLAG = { release_evaluation_scope_cache: "release_evaluation_scope_cache", release_table_html_column_type_enabled: "release_table_html_column_type_enabled", + release_gs_all_sheets_options_enabled: + "release_gs_all_sheets_options_enabled", } as const; export type FeatureFlag = keyof typeof FEATURE_FLAG; @@ -86,6 +88,7 @@ export const DEFAULT_FEATURE_FLAG_VALUE: FeatureFlags = { ab_request_new_integration_enabled: false, release_evaluation_scope_cache: false, release_table_html_column_type_enabled: false, + release_gs_all_sheets_options_enabled: false, }; export const AB_TESTING_EVENT_KEYS = { diff --git a/app/client/src/components/formControls/RadioButtonControl.tsx b/app/client/src/components/formControls/RadioButtonControl.tsx index ecee054f58c5..bba45c049a44 100644 --- a/app/client/src/components/formControls/RadioButtonControl.tsx +++ b/app/client/src/components/formControls/RadioButtonControl.tsx @@ -44,7 +44,10 @@ function renderComponent(props: renderComponentProps) { }; const options = props.options || []; - const defaultValue = props.initialValue as string; + const selectedValue = props.input?.value; + const defaultValue = !!selectedValue + ? selectedValue + : (props.initialValue as string); return ( @@ -148,7 +151,7 @@ export function renderDatasourceSection( isHidden( datasource.datasourceStorages[currentEnvironment], section.hidden, - undefined, + featureFlags, viewMode, ) ) @@ -168,6 +171,7 @@ export function renderDatasourceSection( currentEnvironment, datasource, viewMode, + featureFlags, ); } else { try { @@ -320,6 +324,7 @@ class RenderDatasourceInformation extends React.Component { ? false : isMultipleEnvEnabled(selectFeatureFlags(state)); const currentEnvironmentId = getCurrentEnvironmentId(state); + const featureFlags = selectFeatureFlags(state); return { currentEnv: isEnvEnabled ? currentEnvironmentId : getDefaultEnvId(), isEnvEnabled, + featureFlags, }; }; diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/enums/FeatureFlagEnum.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/enums/FeatureFlagEnum.java index 9849422cd531..0e47bb43b866 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/enums/FeatureFlagEnum.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/enums/FeatureFlagEnum.java @@ -15,6 +15,7 @@ public enum FeatureFlagEnum { release_embed_hide_share_settings_enabled, rollout_datasource_test_rate_limit_enabled, release_google_sheets_shared_drive_support_enabled, + release_gs_all_sheets_options_enabled, // Deprecated CE flags over here release_git_autocommit_feature_enabled, diff --git a/app/server/appsmith-plugins/googleSheetsPlugin/src/main/resources/form.json b/app/server/appsmith-plugins/googleSheetsPlugin/src/main/resources/form.json index 8e9fe67783ed..9d8c67c9ffc8 100644 --- a/app/server/appsmith-plugins/googleSheetsPlugin/src/main/resources/form.json +++ b/app/server/appsmith-plugins/googleSheetsPlugin/src/main/resources/form.json @@ -38,11 +38,48 @@ { "label": "Read / Write / Delete | Selected google sheets", "value": "https://www.googleapis.com/auth/drive.file" + }, + { + "label": "Read / Write / Delete | All google sheets", + "value": "https://www.googleapis.com/auth/spreadsheets,https://www.googleapis.com/auth/drive" + }, + { + "label": "Read / Write | All google sheets", + "value": "https://www.googleapis.com/auth/spreadsheets,https://www.googleapis.com/auth/drive.readonly" + }, + { + "label": "Read | All google sheets", + "value": "https://www.googleapis.com/auth/spreadsheets.readonly,https://www.googleapis.com/auth/drive.readonly" } ], "initialValue": "https://www.googleapis.com/auth/drive.file", "customStyles": { "width": "340px" + }, + "hidden": { + "flagValue": "release_gs_all_sheets_options_enabled", + "comparison": "FEATURE_FLAG", + "value": false + } + }, + { + "label": "Permissions | Scope", + "configProperty": "datasourceConfiguration.authentication.scopeString", + "controlType": "RADIO_BUTTON", + "options": [ + { + "label": "Read / Write / Delete | Selected google sheets", + "value": "https://www.googleapis.com/auth/drive.file" + } + ], + "initialValue": "https://www.googleapis.com/auth/drive.file", + "customStyles": { + "width": "340px" + }, + "hidden": { + "flagValue": "release_gs_all_sheets_options_enabled", + "comparison": "FEATURE_FLAG", + "value": true } } ] From 52353046878b1c12eb3fa21aba3ff1da4179bf3e Mon Sep 17 00:00:00 2001 From: Anagh Hegde Date: Fri, 13 Dec 2024 14:40:02 +0530 Subject: [PATCH 2/8] chore: refactor crud repository methods (#38144) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description As part of transaction support in PG, we are moving from using the jpa methods for database operations. This PR is refactoring the code to use custom repository class for DatasourceStorageStructureRepository from the default CrudRepository. ## Automation /ok-to-test tags="@tag.Datasource" ### :mag: Cypress test results > [!TIP] > 🟒 🟒 🟒 All cypress tests have passed! πŸŽ‰ πŸŽ‰ πŸŽ‰ > Workflow run: > Commit: f2268094b095c52557664da223936306d1bf5d48 > Cypress dashboard. > Tags: `@tag.Datasource` > Spec: >
Fri, 13 Dec 2024 07:34:38 UTC ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No ## Summary by CodeRabbit - **New Features** - Introduced a method to retrieve `DatasourceStorageStructure` based on datasource and environment identifiers. - **Bug Fixes** - Simplified the structure of the `DatasourceStorageStructureRepositoryCE` interface by removing an outdated method. - **Documentation** - Added `@Repository` annotation to the `DatasourceStorageStructureRepositoryCE` interface to indicate its role as a Spring Data repository. --- .../CustomDatasourceStorageStructureRepositoryCE.java | 3 +++ ...stomDatasourceStorageStructureRepositoryCEImpl.java | 10 ++++++++++ .../ce/DatasourceStorageStructureRepositoryCE.java | 8 +++----- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomDatasourceStorageStructureRepositoryCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomDatasourceStorageStructureRepositoryCE.java index d8ef2313e2d9..b5df4d8707bb 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomDatasourceStorageStructureRepositoryCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomDatasourceStorageStructureRepositoryCE.java @@ -1,9 +1,12 @@ package com.appsmith.server.repositories.ce; +import com.appsmith.external.models.DatasourceStorageStructure; import com.appsmith.external.models.DatasourceStructure; import reactor.core.publisher.Mono; public interface CustomDatasourceStorageStructureRepositoryCE { Mono updateStructure(String datasourceId, String environmentId, DatasourceStructure structure); + + Mono findByDatasourceIdAndEnvironmentId(String datasourceId, String environmentId); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomDatasourceStorageStructureRepositoryCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomDatasourceStorageStructureRepositoryCEImpl.java index bf59b2f36e68..288acb56d53d 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomDatasourceStorageStructureRepositoryCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomDatasourceStorageStructureRepositoryCEImpl.java @@ -3,6 +3,7 @@ import com.appsmith.external.models.DatasourceStorageStructure; import com.appsmith.external.models.DatasourceStructure; import com.appsmith.server.helpers.ce.bridge.Bridge; +import com.appsmith.server.helpers.ce.bridge.BridgeQuery; import com.appsmith.server.repositories.BaseAppsmithRepositoryImpl; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; @@ -19,4 +20,13 @@ public Mono updateStructure(String datasourceId, String environmentId, .equal(DatasourceStorageStructure.Fields.environmentId, environmentId)) .updateFirst(Bridge.update().set(DatasourceStorageStructure.Fields.structure, structure)); } + + @Override + public Mono findByDatasourceIdAndEnvironmentId( + String datasourceId, String environmentId) { + final BridgeQuery q = Bridge.equal( + DatasourceStorageStructure.Fields.datasourceId, datasourceId) + .equal(DatasourceStorageStructure.Fields.environmentId, environmentId); + return queryBuilder().criteria(q).one(); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/DatasourceStorageStructureRepositoryCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/DatasourceStorageStructureRepositoryCE.java index 23e69f5bf8de..0baf86c9903d 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/DatasourceStorageStructureRepositoryCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/DatasourceStorageStructureRepositoryCE.java @@ -3,10 +3,8 @@ import com.appsmith.external.models.DatasourceStorageStructure; import com.appsmith.server.repositories.BaseRepository; import com.appsmith.server.repositories.CustomDatasourceStorageStructureRepository; -import reactor.core.publisher.Mono; +import org.springframework.stereotype.Repository; +@Repository public interface DatasourceStorageStructureRepositoryCE - extends BaseRepository, CustomDatasourceStorageStructureRepository { - - Mono findByDatasourceIdAndEnvironmentId(String datasourceId, String environmentId); -} + extends BaseRepository, CustomDatasourceStorageStructureRepository {} From 1f2a4aae451dcdd0c28f73aeb33e6bc8f5bddf95 Mon Sep 17 00:00:00 2001 From: Anagh Hegde Date: Fri, 13 Dec 2024 14:40:14 +0530 Subject: [PATCH 3/8] chore: refactor the crud method to custom repo class (#38137) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description As part of transaction support in PG, we are moving from using the jpa methods for database operations. This PR is refactoring the code to use custom repository class for DatasourceRepository from the default CrudRepository. ## Automation /ok-to-test tags="@tag.ImportExport" ### :mag: Cypress test results > [!TIP] > 🟒 🟒 🟒 All cypress tests have passed! πŸŽ‰ πŸŽ‰ πŸŽ‰ > Workflow run: > Commit: d85d3ae9d9ca95a454b1f43f159e45b7643eea60 > Cypress dashboard. > Tags: `@tag.ImportExport` > Spec: >
Thu, 12 Dec 2024 21:15:06 UTC ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No ## Summary by CodeRabbit - **New Features** - Introduced a new method to retrieve action collections by application ID without requiring additional parameters. - **Bug Fixes** - Deprecated the previous method for fetching action collections that required more parameters, streamlining the querying process. - **Documentation** - Updated method signatures to reflect the new querying capabilities. --- .../repositories/ce/ActionCollectionRepositoryCE.java | 1 - .../repositories/ce/CustomActionCollectionRepositoryCE.java | 2 ++ .../ce/CustomActionCollectionRepositoryCEImpl.java | 6 ++++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/ActionCollectionRepositoryCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/ActionCollectionRepositoryCE.java index 0a2a4e554894..2b3137afa034 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/ActionCollectionRepositoryCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/ActionCollectionRepositoryCE.java @@ -10,7 +10,6 @@ public interface ActionCollectionRepositoryCE extends BaseRepository, CustomActionCollectionRepository { - Flux findByApplicationId(String applicationId); Flux findIdsAndPolicyMapByApplicationIdIn(List applicationIds); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomActionCollectionRepositoryCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomActionCollectionRepositoryCE.java index c7103a988fb8..4da3cdc325ab 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomActionCollectionRepositoryCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomActionCollectionRepositoryCE.java @@ -42,4 +42,6 @@ Flux findAllPublishedActionCollectionsByContextIdAndContextTyp Flux findAllNonComposedByPageIdAndViewMode( String pageId, boolean viewMode, AclPermission permission); + + Flux findByApplicationId(String applicationId); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomActionCollectionRepositoryCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomActionCollectionRepositoryCEImpl.java index b47d6751485a..2c845414323e 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomActionCollectionRepositoryCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomActionCollectionRepositoryCEImpl.java @@ -168,4 +168,10 @@ public Flux findAllNonComposedByPageIdAndViewMode( String pageId, boolean viewMode, AclPermission permission) { return this.findByPageIdAndViewMode(pageId, viewMode, permission); } + + @Override + public Flux findByApplicationId(String applicationId) { + final BridgeQuery q = Bridge.equal(ActionCollection.Fields.applicationId, applicationId); + return queryBuilder().criteria(q).all(); + } } From 8f8846619597ccc62be1db764f6d6c08b6de67de Mon Sep 17 00:00:00 2001 From: Sagar Khalasi Date: Fri, 13 Dec 2024 15:04:00 +0530 Subject: [PATCH 4/8] fix: Fix for the embedded case (#38101) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Fix the case for embedded case Fixes #`Issue Number` _or_ Fixes `Issue URL` > [!WARNING] > _If no issue exists, please create an issue first, and check with the maintainers if the issue is valid._ ## Automation /ok-to-test tags="@tag.Settings" ### :mag: Cypress test results > [!TIP] > 🟒 🟒 🟒 All cypress tests have passed! πŸŽ‰ πŸŽ‰ πŸŽ‰ > Workflow run: > Commit: b4e86b07be4154330ec77a7716d1ef9d29629cea > Cypress dashboard. > Tags: `@tag.Settings` > Spec: >
Fri, 13 Dec 2024 08:14:04 UTC ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No ## Summary by CodeRabbit - **New Features** - Introduced a new "copy application URL" button for easier URL management. - **Bug Fixes** - Enhanced timeout durations for server restart notifications to improve reliability. - **Improvements** - Modified test setup for better navigation and application creation process. - Updated navigation to wait for the page header before proceeding, improving test flow. --- .../EmbedSettings/EmbedSettings_spec.js | 47 ++++--------------- .../cypress/locators/AppNavigation.json | 3 +- .../cypress/support/AdminSettingsCommands.js | 4 +- .../cypress/support/Pages/DeployModeHelper.ts | 4 +- 4 files changed, 15 insertions(+), 43 deletions(-) diff --git a/app/client/cypress/e2e/Regression/ClientSide/EmbedSettings/EmbedSettings_spec.js b/app/client/cypress/e2e/Regression/ClientSide/EmbedSettings/EmbedSettings_spec.js index 7c3d54654b32..ea53ef2ba7c4 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/EmbedSettings/EmbedSettings_spec.js +++ b/app/client/cypress/e2e/Regression/ClientSide/EmbedSettings/EmbedSettings_spec.js @@ -31,32 +31,15 @@ describe("Embed settings options", { tags: ["@tag.Settings"] }, function () { } before(() => { - _.entityExplorer.DragDropWidgetNVerify(_.draggableWidgets.BUTTON); - _.deployMode.DeployApp(); - cy.get( - `${appNavigationLocators.header} ${appNavigationLocators.shareButton}`, - ) - .click() - .wait(1000); - cy.get("[data-testid='copy-application-url']").last().click(); - _.agHelper.GiveChromeCopyPermission(); - - cy.window() - .its("navigator.clipboard") - .invoke("readText") - .then((text) => { - cy.wrap(text).as("embeddedAppUrl"); - }); - - cy.enablePublicAccess(); - cy.get( - `${appNavigationLocators.header} ${appNavigationLocators.backToAppsButton}`, - ).click(); + _.homePage.NavigateToHome(); _.homePage.CreateNewApplication(); _.entityExplorer.DragDropWidgetNVerify(_.draggableWidgets.IFRAME); - cy.get("@embeddedAppUrl").then((url) => { - cy.testJsontext("url", url); - }); + // cy.get("@embeddedAppUrl").then((url) => { + cy.testJsontext( + "url", + "https://app.appsmith.com/applications/6752ba5904a5f464099437ec/pages/6752ba5904a5f464099437f3", + ); + //}); _.agHelper.Sleep(5000); //for Iframe to fully load with url data _.deployMode.DeployApp(); cy.get( @@ -73,6 +56,7 @@ describe("Embed settings options", { tags: ["@tag.Settings"] }, function () { }); cy.enablePublicAccess(); cy.wait(8000); //adding wait time for iframe to load fully! + _.agHelper.RefreshPage(); getIframeBody().contains("Submit").should("exist"); _.deployMode.NavigateToHomeDirectly(); }); @@ -102,13 +86,6 @@ describe("Embed settings options", { tags: ["@tag.Settings"] }, function () { }); cy.get(adminSettings.saveButton).click(); cy.waitForServerRestart(); - // TODO: Commented out as it is flaky - // cy.wait(["@getEnvVariables", "@getEnvVariables"]).then((interception) => { - // const { - // APPSMITH_ALLOWED_FRAME_ANCESTORS, - // } = interception[1].response.body.data; - // expect(APPSMITH_ALLOWED_FRAME_ANCESTORS).to.equal("*"); - // }); _.agHelper.Sleep(2000); cy.get("@deployUrl").then((depUrl) => { cy.log("deployUrl is " + depUrl); @@ -130,13 +107,7 @@ describe("Embed settings options", { tags: ["@tag.Settings"] }, function () { cy.get("@deployUrl").then((depUrl) => { cy.log("deployUrl is " + depUrl); cy.visit(depUrl, { timeout: 60000 }); - }); // TODO: Commented out as it is flaky - // cy.wait(["@getEnvVariables", "@getEnvVariables"]).then((interception) => { - // const { - // APPSMITH_ALLOWED_FRAME_ANCESTORS, - // } = interception[1].response.body.data; - // expect(APPSMITH_ALLOWED_FRAME_ANCESTORS).to.equal("'none'"); - // }); + }); getIframeBody().contains("Submit").should("not.exist"); ValidateEditModeSetting(_.embedSettings.locators._disabledText); diff --git a/app/client/cypress/locators/AppNavigation.json b/app/client/cypress/locators/AppNavigation.json index 7ba99ad6b84c..47140c63badb 100644 --- a/app/client/cypress/locators/AppNavigation.json +++ b/app/client/cypress/locators/AppNavigation.json @@ -43,5 +43,6 @@ "topInlineMoreButton": ".t--app-viewer-navigation-top-inline-more-button", "topInlineMoreDropdown": ".t--app-viewer-navigation-top-inline-more-dropdown", "topInlineMoreDropdownItem": ".t--app-viewer-navigation-top-inline-more-dropdown-item", - "sidebarCollapseButton": ".t--app-viewer-navigation-sidebar-collapse" + "sidebarCollapseButton": ".t--app-viewer-navigation-sidebar-collapse", + "copyAppUrl": "[data-testid='copy-application-url']" } diff --git a/app/client/cypress/support/AdminSettingsCommands.js b/app/client/cypress/support/AdminSettingsCommands.js index d91bddd67ab7..ccde6c90632a 100644 --- a/app/client/cypress/support/AdminSettingsCommands.js +++ b/app/client/cypress/support/AdminSettingsCommands.js @@ -79,8 +79,8 @@ Cypress.Commands.add("waitForServerRestart", () => { // cy.waitUntil(() => !Cypress.$(adminSettings.restartNotice).length, { // timeout: 180000, // }); - cy.get(adminSettings.restartNotice, { timeout: 300000 }).should("not.exist"); - cy.get(adminSettings.appsmithStarting, { timeout: 300000 }).should( + cy.get(adminSettings.restartNotice, { timeout: 600000 }).should("not.exist"); + cy.get(adminSettings.appsmithStarting, { timeout: 600000 }).should( "not.exist", ); diff --git a/app/client/cypress/support/Pages/DeployModeHelper.ts b/app/client/cypress/support/Pages/DeployModeHelper.ts index 3cba088ee8ac..a98b4cf81a5e 100644 --- a/app/client/cypress/support/Pages/DeployModeHelper.ts +++ b/app/client/cypress/support/Pages/DeployModeHelper.ts @@ -34,6 +34,7 @@ export class DeployMode { public _deployPageWidgets = ".bp3-heading, section.canvas [data-testid=t--app-viewer-page]:not(:empty)"; public _appViewPageName = `div.t--app-viewer-application-name`; + public homePagaHeader = `[data-testid="t--appsmith-page-header"]`; //refering PublishtheApp from command.js public DeployApp( @@ -154,8 +155,7 @@ export class DeployMode { public NavigateToHomeDirectly() { this.agHelper.GetNClick(this._backtoHome); - this.agHelper.Sleep(2000); - this.agHelper.AssertElementVisibility(this._homeAppsmithImage); + this.agHelper.WaitUntilEleAppear(this.homePagaHeader); } public EnterJSONInputValue( From 32ed6f0776a5ae9cbf63c43d98ebd6d812bf1fa6 Mon Sep 17 00:00:00 2001 From: Pawan Kumar Date: Fri, 13 Dec 2024 16:56:06 +0530 Subject: [PATCH 5/8] chore: fix sidebar mobile responsiveness. (#38159) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/user-attachments/assets/6eadc643-da55-4b9c-9aae-f07e09bd34cd /ok-to-test tags="@tag.Anvil" ## Summary by CodeRabbit - **New Features** - Enhanced mobile responsiveness for the sidebar component with the addition of a new data attribute. - **Bug Fixes** - Improved styling conditions for the sidebar in various states, ensuring better visual consistency across mobile and full-width displays. - **Style** - Updated CSS selectors for more precise styling of the sidebar, enhancing its responsive behavior. > [!TIP] > 🟒 🟒 🟒 All cypress tests have passed! πŸŽ‰ πŸŽ‰ πŸŽ‰ > Workflow run: > Commit: 3ee6c76ffd3e5af9849bf7db1cb1a242b5aabd2b > Cypress dashboard. > Tags: `@tag.Anvil` > Spec: >
Fri, 13 Dec 2024 11:17:26 UTC --- .../src/components/Sidebar/src/Sidebar.tsx | 3 ++- .../components/Sidebar/src/styles.module.css | 26 +++++++++---------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/app/client/packages/design-system/widgets/src/components/Sidebar/src/Sidebar.tsx b/app/client/packages/design-system/widgets/src/components/Sidebar/src/Sidebar.tsx index 820de64e50f3..f758522a8267 100644 --- a/app/client/packages/design-system/widgets/src/components/Sidebar/src/Sidebar.tsx +++ b/app/client/packages/design-system/widgets/src/components/Sidebar/src/Sidebar.tsx @@ -23,7 +23,7 @@ const _Sidebar = (props: SidebarProps, ref: Ref) => { ...rest } = props; const [isAnimating, setIsAnimating] = useState(false); - const { side, state } = useSidebar(); + const { isMobile, side, state } = useSidebar(); const sidebarRef = useRef(); const onEnter = () => { @@ -84,6 +84,7 @@ const _Sidebar = (props: SidebarProps, ref: Ref) => {
550px) { - .mainSidebar { - --sidebar-width: min(50cqw, 1024px); - } +.mainSidebar:not([data-is-mobile], [data-state="full-width"]) { + --sidebar-width: min(50cqw, 1024px); } /** @@ -102,21 +100,21 @@ width: calc(var(--sidebar-width-icon)); } -@container (width > 550px) { - .mainSidebar[data-side="start"] .sidebar { - border-inline-end: var(--border-width-1) solid var(--color-bd-elevation-1); - } +.mainSidebar[data-side="start"]:not([data-is-mobile]) .sidebar { + border-inline-end: var(--border-width-1) solid var(--color-bd-elevation-1); +} - .mainSidebar[data-side="end"] .sidebar { - border-inline-start: var(--border-width-1) solid var(--color-bd-elevation-1); - } +.mainSidebar[data-side="end"]:not([data-is-mobile]) .sidebar { + border-inline-start: var(--border-width-1) solid var(--color-bd-elevation-1); } -.mainSidebar[data-state="full-width"][data-side="start"] .sidebar { +.mainSidebar:is([data-state="full-width"][data-side="start"], [data-is-mobile]) + .sidebar { border-inline-end: none; } -.mainSidebar[data-state="full-width"][data-side="end"] .sidebar { +.mainSidebar:is([data-state="full-width"][data-side="end"], [data-is-mobile]) + .sidebar { border-inline-start: none; } From 813279bfc2d6fbfb64c0091bb423d173af300e5b Mon Sep 17 00:00:00 2001 From: Manish Kumar <107841575+sondermanish@users.noreply.github.com> Date: Fri, 13 Dec 2024 22:21:40 +0530 Subject: [PATCH 6/8] chore: Added changes for discard (#38152) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description - Added implementation for discard Fixes #37438 ## Automation /ok-to-test tags="@tag.Git" ### :mag: Cypress test results > [!TIP] > 🟒 🟒 🟒 All cypress tests have passed! πŸŽ‰ πŸŽ‰ πŸŽ‰ > Workflow run: > Commit: 0db248a0c8f5b83fc2a33f574680ab59de51c52d > Cypress dashboard. > Tags: `@tag.Git` > Spec: >
Fri, 13 Dec 2024 12:18:45 UTC ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No ## Summary by CodeRabbit - **New Features** - Introduced a method to discard changes associated with a specified branched artifact. - Added functionality to recreate artifact JSON from the last commit. - **Bug Fixes** - Enhanced handling of lock requirements in Git operations, allowing for null values. - **Documentation** - Updated method signatures and added descriptions for new functionalities. --- .../appsmith/server/git/GitRedisUtils.java | 2 +- .../git/central/CentralGitServiceCE.java | 2 + .../git/central/CentralGitServiceCEImpl.java | 80 +++++++++++++++++++ .../git/central/GitHandlingServiceCE.java | 3 + .../server/git/fs/GitFSServiceCEImpl.java | 19 +++++ 5 files changed, 105 insertions(+), 1 deletion(-) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/GitRedisUtils.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/GitRedisUtils.java index 6c27759f3ea3..e0d61cdc192b 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/GitRedisUtils.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/GitRedisUtils.java @@ -69,7 +69,7 @@ public Mono releaseFileLock(String defaultApplicationId) { * @return : Boolean for whether the lock is acquired */ // TODO @Manish add artifactType reference in incoming prs. - public Mono acquireGitLock(String baseArtifactId, String commandName, boolean isLockRequired) { + public Mono acquireGitLock(String baseArtifactId, String commandName, Boolean isLockRequired) { if (!Boolean.TRUE.equals(isLockRequired)) { return Mono.just(Boolean.TRUE); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCE.java index 576a7fb99cd3..4254b5d4dd2a 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCE.java @@ -31,4 +31,6 @@ Mono fetchRemoteChanges( ArtifactType artifactType, GitType gitType, RefType refType); + + Mono discardChanges(String branchedArtifactId, ArtifactType artifactType, GitType gitType); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCEImpl.java index 7a15b41f63c6..55e1692b816e 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCEImpl.java @@ -1112,4 +1112,84 @@ private Mono updateArtifactWithGitMetadataGivenPermission( .getArtifactHelper(artifact.getArtifactType()) .saveArtifact(artifact); } + + /** + * Resets the artifact to last commit, all uncommitted changes are lost in the process. + * @param branchedArtifactId : id of the branchedArtifact + * @param artifactType type of the artifact + * @param gitType what is the intended implementation type + * @return : a publisher of an artifact. + */ + @Override + public Mono discardChanges( + String branchedArtifactId, ArtifactType artifactType, GitType gitType) { + + if (!hasText(branchedArtifactId)) { + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.ARTIFACT_ID)); + } + + GitArtifactHelper gitArtifactHelper = gitArtifactHelperResolver.getArtifactHelper(artifactType); + AclPermission artifactEditPermission = gitArtifactHelper.getArtifactEditPermission(); + + Mono branchedArtifactMonoCached = + gitArtifactHelper.getArtifactById(branchedArtifactId, artifactEditPermission); + + Mono recreatedArtifactFromLastCommit; + + // Rehydrate the artifact from local file system + recreatedArtifactFromLastCommit = branchedArtifactMonoCached + .flatMap(branchedArtifact -> { + GitArtifactMetadata branchedGitData = branchedArtifact.getGitArtifactMetadata(); + if (branchedGitData == null || !hasText(branchedGitData.getDefaultArtifactId())) { + return Mono.error( + new AppsmithException(AppsmithError.INVALID_GIT_CONFIGURATION, GIT_CONFIG_ERROR)); + } + + return Mono.just(branchedArtifact) + .doFinally(signalType -> gitRedisUtils.acquireGitLock( + branchedGitData.getDefaultArtifactId(), + GitConstants.GitCommandConstants.DISCARD, + TRUE)); + }) + .flatMap(branchedArtifact -> { + GitArtifactMetadata branchedGitData = branchedArtifact.getGitArtifactMetadata(); + ArtifactJsonTransformationDTO jsonTransformationDTO = new ArtifactJsonTransformationDTO(); + // Because this operation is only valid for branches + jsonTransformationDTO.setArtifactType(artifactType); + jsonTransformationDTO.setRefType(RefType.BRANCH); + jsonTransformationDTO.setWorkspaceId(branchedArtifact.getWorkspaceId()); + jsonTransformationDTO.setBaseArtifactId(branchedGitData.getDefaultArtifactId()); + jsonTransformationDTO.setRefName(branchedGitData.getRefName()); + jsonTransformationDTO.setRepoName(branchedGitData.getRepoName()); + + GitHandlingService gitHandlingService = gitHandlingServiceResolver.getGitHandlingService(gitType); + + return gitHandlingService + .recreateArtifactJsonFromLastCommit(jsonTransformationDTO) + .onErrorResume(throwable -> { + log.error("Git recreate ArtifactJsonFailed : {}", throwable.getMessage()); + return Mono.error( + new AppsmithException( + AppsmithError.GIT_ACTION_FAILED, + "discard changes", + "Please create a new branch and resolve conflicts in the remote repository before proceeding.")); + }) + .flatMap(artifactExchangeJson -> importService.importArtifactInWorkspaceFromGit( + branchedArtifact.getWorkspaceId(), + branchedArtifact.getId(), + artifactExchangeJson, + branchedGitData.getBranchName())) + // Update the last deployed status after the rebase + .flatMap(importedArtifact -> gitArtifactHelper.publishArtifact(importedArtifact, true)); + }) + .flatMap(branchedArtifact -> gitAnalyticsUtils + .addAnalyticsForGitOperation(AnalyticsEvents.GIT_DISCARD_CHANGES, branchedArtifact, null) + .doFinally(signalType -> gitRedisUtils.releaseFileLock( + branchedArtifact.getGitArtifactMetadata().getDefaultArtifactId(), TRUE))) + .name(GitSpan.OPS_DISCARD_CHANGES) + .tap(Micrometer.observation(observationRegistry)); + + return Mono.create(sink -> + recreatedArtifactFromLastCommit.subscribe(sink::success, sink::error, null, sink.currentContext())); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/GitHandlingServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/GitHandlingServiceCE.java index 5b9551ffa675..de387a9e75ee 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/GitHandlingServiceCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/GitHandlingServiceCE.java @@ -58,4 +58,7 @@ Mono> commitArtifact( Artifact branchedArtifact, CommitDTO commitDTO, ArtifactJsonTransformationDTO jsonTransformationDTO); Mono fetchRemoteChanges(ArtifactJsonTransformationDTO jsonTransformationDTO, GitAuth gitAuth); + + Mono recreateArtifactJsonFromLastCommit( + ArtifactJsonTransformationDTO jsonTransformationDTO); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/fs/GitFSServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/fs/GitFSServiceCEImpl.java index e84ecd9f7a49..27dc09fde456 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/fs/GitFSServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/fs/GitFSServiceCEImpl.java @@ -598,4 +598,23 @@ public Mono fetchRemoteChanges(ArtifactJsonTransformationDTO jsonTransfo return checkoutBranchMono.then(Mono.defer(() -> fetchRemoteMono)); } + + @Override + public Mono recreateArtifactJsonFromLastCommit( + ArtifactJsonTransformationDTO jsonTransformationDTO) { + + String workspaceId = jsonTransformationDTO.getWorkspaceId(); + String baseArtifactId = jsonTransformationDTO.getBaseArtifactId(); + String repoName = jsonTransformationDTO.getRepoName(); + String refName = jsonTransformationDTO.getRefName(); + + ArtifactType artifactType = jsonTransformationDTO.getArtifactType(); + GitArtifactHelper gitArtifactHelper = gitArtifactHelperResolver.getArtifactHelper(artifactType); + Path repoSuffix = gitArtifactHelper.getRepoSuffixPath(workspaceId, baseArtifactId, repoName); + + return fsGitHandler.rebaseBranch(repoSuffix, refName).flatMap(rebaseStatus -> { + return commonGitFileUtils.reconstructArtifactExchangeJsonFromGitRepoWithAnalytics( + workspaceId, baseArtifactId, repoName, refName, artifactType); + }); + } } From e2916b205a7fd4955e1092e90eebdb74b04d6ef9 Mon Sep 17 00:00:00 2001 From: Rudraprasad Das Date: Sat, 14 Dec 2024 01:29:26 +0800 Subject: [PATCH 7/8] chore: git mod - adding git ops modal (#38136) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description - Adds git modal - Adds pull saga - Adds ability to commit - Adds ability to merge - Adds ability to see status - Adds ability to discard - Improves on saga error handling Fixes #37826 Fixes #37803 Fixes #37804 Fixes #37807 ## Automation /ok-to-test tags="@tag.Git" ### :mag: Cypress test results > [!TIP] > 🟒 🟒 🟒 All cypress tests have passed! πŸŽ‰ πŸŽ‰ πŸŽ‰ > Workflow run: > Commit: ec461fde26da90af2f365fb36f49e3758c14cb96 > Cypress dashboard. > Tags: `@tag.Git` > Spec: >
Fri, 13 Dec 2024 17:27:36 UTC ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No ## Summary by CodeRabbit - **New Features** - Introduced new components for managing Git operations, including `ConflictErrorView`, `OpsModal`, `TabDeploy`, `TabMerge`, and `StatusChanges`. - Added loading and error handling components such as `StatusLoader` and `DiscardFailedError`. - Enhanced the `StatusTree` component for displaying hierarchical status changes. - **Bug Fixes** - Improved error handling across various sagas and components. - **Refactor** - Renamed several modal toggle functions for clarity and consistency. - Updated prop types for improved type safety in components. - **Chores** - Removed unused component `GitTest`. - Updated imports and exports for better organization. --- .../application/statusTransformer.ts | 199 +++++++++ .../ConflictError/ConflictErrorView.tsx | 72 ++++ .../git/components/ConflictError/index.tsx | 15 + .../ConflictErrorModalView.tsx | 93 ++++ .../components/ConflictErrorModal/index.tsx | 14 + .../hooks/useGitBranches.ts | 26 +- .../GitContextProvider/hooks/useGitConnect.ts | 8 +- .../hooks/useGitContextValue.ts | 33 +- .../hooks/useGitMetadata.ts | 4 +- .../GitContextProvider/hooks/useGitOps.ts | 102 ++++- .../hooks/useGitSettings.ts | 12 +- .../components/GitContextProvider/index.tsx | 16 +- app/client/src/git/components/GitModals.tsx | 12 + .../git/components/OpsModal/OpsModalView.tsx | 105 +++++ .../OpsModal/TabDeploy/DeployPreview.tsx | 82 ++++ .../TabDeploy/DiscardChangesWarning.tsx | 43 ++ .../OpsModal/TabDeploy/DiscardFailedError.tsx | 30 ++ .../OpsModal/TabDeploy/PushFailedError.tsx | 31 ++ .../OpsModal/TabDeploy/SubmitWrapper.tsx | 29 ++ .../OpsModal/TabDeploy/TabDeployView.tsx | 407 ++++++++++++++++++ .../components/OpsModal/TabDeploy/index.tsx | 52 +++ .../OpsModal/TabMerge/MergeStatus.tsx | 77 ++++ .../TabMerge/MergeSuccessIndicator.tsx | 21 + .../OpsModal/TabMerge/TabMergeView.tsx | 325 ++++++++++++++ .../components/OpsModal/TabMerge/index.tsx | 43 ++ .../src/git/components/OpsModal/index.tsx | 27 ++ .../BranchButton/BranchList.tsx | 0 .../BranchButton/index.tsx | 0 .../ConnectButton.test.tsx | 0 .../ConnectButton.tsx | 0 .../QuickActionButton.test.tsx | 0 .../QuickActionButton.tsx | 0 .../QuickActionsView.test.tsx} | 45 +- .../QuickActionsView.tsx} | 40 +- .../helpers/getPullButtonStatus.test.ts | 0 .../helpers/getPullButtonStatus.ts | 0 .../hooks/useStatusChangeCount.ts | 0 .../index.tsx | 20 +- .../StatusChanges/StatusChangesView.tsx | 54 +++ .../components/StatusChanges/StatusLoader.tsx | 23 + .../components/StatusChanges/StatusTree.tsx | 88 ++++ .../git/components/StatusChanges/index.tsx | 17 + .../src/git/components/connect/GitTest.tsx | 7 - app/client/src/git/components/index.tsx | 2 + app/client/src/git/constants/enums.ts | 10 +- .../fetchAutocommitProgressRequest.types.ts | 4 +- .../requests/fetchBranchesRequest.types.ts | 2 +- .../requests/fetchMergeStatusRequest.types.ts | 1 + app/client/src/git/requests/pullRequest.ts | 4 +- .../src/git/requests/pullRequest.types.ts | 6 +- .../triggerAutocommitRequest.types.ts | 4 +- .../src/git/sagas/checkoutBranchSaga.ts | 25 +- app/client/src/git/sagas/commitSaga.ts | 39 +- app/client/src/git/sagas/connectSaga.ts | 33 +- app/client/src/git/sagas/createBranchSaga.ts | 30 +- app/client/src/git/sagas/deleteBranchSaga.ts | 23 +- app/client/src/git/sagas/fetchBranchesSaga.ts | 23 +- .../src/git/sagas/fetchGitMetadataSaga.ts | 23 +- .../src/git/sagas/fetchGlobalProfileSaga.ts | 21 +- .../src/git/sagas/fetchLocalProfileSaga.ts | 20 +- .../src/git/sagas/fetchMergeStatusSaga.ts | 53 +++ .../git/sagas/fetchProtectedBranchesSaga.ts | 23 +- app/client/src/git/sagas/fetchStatusSaga.ts | 25 +- app/client/src/git/sagas/index.ts | 4 + app/client/src/git/sagas/pullSaga.ts | 60 +++ .../src/git/sagas/triggerAutocommitSaga.ts | 62 ++- .../src/git/sagas/updateGlobalProfileSaga.ts | 15 +- .../src/git/sagas/updateLocalProfileSaga.ts | 20 +- .../src/git/store/actions/commitActions.ts | 6 + .../src/git/store/actions/discardActions.ts | 6 + .../store/actions/fetchMergeStatusActions.ts | 25 +- .../src/git/store/actions/pullActions.ts | 22 +- app/client/src/git/store/actions/uiActions.ts | 39 +- app/client/src/git/store/gitArtifactSlice.ts | 24 +- .../helpers/gitSingleArtifactInitialState.ts | 7 +- .../selectors/gitSingleArtifactSelectors.ts | 15 + app/client/src/git/store/types.ts | 71 +-- .../gitSync/components/DeployPreview.tsx | 1 + .../components/DiscardChangesWarning.tsx | 7 +- 79 files changed, 2604 insertions(+), 323 deletions(-) create mode 100644 app/client/src/git/artifactHelpers/application/statusTransformer.ts create mode 100644 app/client/src/git/components/ConflictError/ConflictErrorView.tsx create mode 100644 app/client/src/git/components/ConflictError/index.tsx create mode 100644 app/client/src/git/components/ConflictErrorModal/ConflictErrorModalView.tsx create mode 100644 app/client/src/git/components/ConflictErrorModal/index.tsx create mode 100644 app/client/src/git/components/GitModals.tsx create mode 100644 app/client/src/git/components/OpsModal/OpsModalView.tsx create mode 100644 app/client/src/git/components/OpsModal/TabDeploy/DeployPreview.tsx create mode 100644 app/client/src/git/components/OpsModal/TabDeploy/DiscardChangesWarning.tsx create mode 100644 app/client/src/git/components/OpsModal/TabDeploy/DiscardFailedError.tsx create mode 100644 app/client/src/git/components/OpsModal/TabDeploy/PushFailedError.tsx create mode 100644 app/client/src/git/components/OpsModal/TabDeploy/SubmitWrapper.tsx create mode 100644 app/client/src/git/components/OpsModal/TabDeploy/TabDeployView.tsx create mode 100644 app/client/src/git/components/OpsModal/TabDeploy/index.tsx create mode 100644 app/client/src/git/components/OpsModal/TabMerge/MergeStatus.tsx create mode 100644 app/client/src/git/components/OpsModal/TabMerge/MergeSuccessIndicator.tsx create mode 100644 app/client/src/git/components/OpsModal/TabMerge/TabMergeView.tsx create mode 100644 app/client/src/git/components/OpsModal/TabMerge/index.tsx create mode 100644 app/client/src/git/components/OpsModal/index.tsx rename app/client/src/git/components/{GitQuickActions => QuickActions}/BranchButton/BranchList.tsx (100%) rename app/client/src/git/components/{GitQuickActions => QuickActions}/BranchButton/index.tsx (100%) rename app/client/src/git/components/{GitQuickActions => QuickActions}/ConnectButton.test.tsx (100%) rename app/client/src/git/components/{GitQuickActions => QuickActions}/ConnectButton.tsx (100%) rename app/client/src/git/components/{GitQuickActions => QuickActions}/QuickActionButton.test.tsx (100%) rename app/client/src/git/components/{GitQuickActions => QuickActions}/QuickActionButton.tsx (100%) rename app/client/src/git/components/{GitQuickActions/index.test.tsx => QuickActions/QuickActionsView.test.tsx} (89%) rename app/client/src/git/components/{GitQuickActions/index.tsx => QuickActions/QuickActionsView.tsx} (85%) rename app/client/src/git/components/{GitQuickActions => QuickActions}/helpers/getPullButtonStatus.test.ts (100%) rename app/client/src/git/components/{GitQuickActions => QuickActions}/helpers/getPullButtonStatus.ts (100%) rename app/client/src/git/components/{CtxAwareGitQuickActions => QuickActions}/hooks/useStatusChangeCount.ts (100%) rename app/client/src/git/components/{CtxAwareGitQuickActions => QuickActions}/index.tsx (75%) create mode 100644 app/client/src/git/components/StatusChanges/StatusChangesView.tsx create mode 100644 app/client/src/git/components/StatusChanges/StatusLoader.tsx create mode 100644 app/client/src/git/components/StatusChanges/StatusTree.tsx create mode 100644 app/client/src/git/components/StatusChanges/index.tsx delete mode 100644 app/client/src/git/components/connect/GitTest.tsx create mode 100644 app/client/src/git/components/index.tsx create mode 100644 app/client/src/git/sagas/fetchMergeStatusSaga.ts create mode 100644 app/client/src/git/sagas/pullSaga.ts diff --git a/app/client/src/git/artifactHelpers/application/statusTransformer.ts b/app/client/src/git/artifactHelpers/application/statusTransformer.ts new file mode 100644 index 000000000000..9ac95d9c424e --- /dev/null +++ b/app/client/src/git/artifactHelpers/application/statusTransformer.ts @@ -0,0 +1,199 @@ +import type { FetchStatusResponseData } from "git/requests/fetchStatusRequest.types"; +import { objectKeys } from "@appsmith/utils"; +import type { StatusTreeStruct } from "git/components/StatusChanges/StatusTree"; + +const ICON_LOOKUP = { + query: "query", + jsObject: "js", + page: "page-line", + datasource: "database-2-line", + jsLib: "package", +}; + +interface TreeNodeDef { + subject: string; + verb: string; + type: keyof typeof ICON_LOOKUP; +} + +function createTreeNode(nodeDef: TreeNodeDef) { + return { + icon: ICON_LOOKUP[nodeDef.type], + message: `${nodeDef.subject} ${nodeDef.verb}`, + }; +} + +function determineVerbForDefs(defs: TreeNodeDef[]) { + const isRemoved = defs.some((def) => def.verb === "removed"); + const isAdded = defs.some((def) => def.verb === "added"); + const isModified = defs.some((def) => def.verb === "modified"); + + let action = ""; + + if (isRemoved && !isAdded && !isModified) { + action = "removed"; + } else if (isAdded && !isRemoved && !isModified) { + action = "added"; + } else { + action = "modified"; + } + + return action; +} + +function createTreeNodeGroup(nodeDefs: TreeNodeDef[], subject: string) { + return { + icon: ICON_LOOKUP[nodeDefs[0].type], + message: `${nodeDefs.length} ${subject} ${determineVerbForDefs(nodeDefs)}`, + children: nodeDefs.map(createTreeNode), + }; +} + +function statusPageTransformer(status: FetchStatusResponseData) { + const { + jsObjectsAdded, + jsObjectsModified, + jsObjectsRemoved, + pagesAdded, + pagesModified, + pagesRemoved, + queriesAdded, + queriesModified, + queriesRemoved, + } = status; + const pageEntityDefLookup: Record = {}; + const addToPageEntityDefLookup = ( + files: string[], + type: keyof typeof ICON_LOOKUP, + verb: string, + ) => { + files.forEach((file) => { + const [page, subject] = file.split("/"); + + pageEntityDefLookup[page] ??= []; + pageEntityDefLookup[page].push({ subject, verb, type }); + }); + }; + + addToPageEntityDefLookup(queriesModified, "query", "modified"); + addToPageEntityDefLookup(queriesAdded, "query", "added"); + addToPageEntityDefLookup(queriesRemoved, "query", "removed"); + addToPageEntityDefLookup(jsObjectsModified, "jsObject", "modified"); + addToPageEntityDefLookup(jsObjectsAdded, "jsObject", "added"); + addToPageEntityDefLookup(jsObjectsRemoved, "jsObject", "removed"); + + const pageDefLookup: Record = {}; + const addToPageDefLookup = (pages: string[], verb: string) => { + pages.forEach((page) => { + pageDefLookup[page] = { subject: page, verb, type: "page" }; + }); + }; + + addToPageDefLookup(pagesModified, "modified"); + addToPageDefLookup(pagesAdded, "added"); + addToPageDefLookup(pagesRemoved, "removed"); + + const tree = [] as StatusTreeStruct[]; + + objectKeys(pageEntityDefLookup).forEach((page) => { + const queryDefs = pageEntityDefLookup[page].filter( + (def) => def.type === "query", + ); + const jsObjectDefs = pageEntityDefLookup[page].filter( + (def) => def.type === "jsObject", + ); + const children = [] as StatusTreeStruct[]; + + if (queryDefs.length > 0) { + const subject = queryDefs.length === 1 ? "query" : "queries"; + + children.push(createTreeNodeGroup(queryDefs, subject)); + } + + if (jsObjectDefs.length > 0) { + const subject = jsObjectDefs.length === 1 ? "jsObject" : "jsObjects"; + + children.push(createTreeNodeGroup(jsObjectDefs, subject)); + } + + let pageDef = pageDefLookup[page]; + + if (!pageDef) { + pageDef = { subject: page, verb: "modified", type: "page" }; + } + + tree.push({ ...createTreeNode(pageDef), children }); + }); + + objectKeys(pageDefLookup).forEach((page) => { + if (!pageEntityDefLookup[page]) { + tree.push(createTreeNode(pageDefLookup[page])); + } + }); + + return tree; +} + +function statusDatasourceTransformer(status: FetchStatusResponseData) { + const { datasourcesAdded, datasourcesModified, datasourcesRemoved } = status; + const defs = [] as TreeNodeDef[]; + + datasourcesModified.forEach((datasource) => { + defs.push({ subject: datasource, verb: "modified", type: "datasource" }); + }); + + datasourcesAdded.forEach((datasource) => { + defs.push({ subject: datasource, verb: "added", type: "datasource" }); + }); + + datasourcesRemoved.forEach((datasource) => { + defs.push({ subject: datasource, verb: "removed", type: "datasource" }); + }); + + const tree = [] as StatusTreeStruct[]; + + if (defs.length > 0) { + tree.push(createTreeNodeGroup(defs, "datasource")); + } + + return tree; +} + +function statusJsLibTransformer(status: FetchStatusResponseData) { + const { jsLibsAdded, jsLibsModified, jsLibsRemoved } = status; + const defs = [] as TreeNodeDef[]; + + jsLibsModified.forEach((jsLib) => { + defs.push({ subject: jsLib, verb: "modified", type: "jsLib" }); + }); + + jsLibsAdded.forEach((jsLib) => { + defs.push({ subject: jsLib, verb: "added", type: "jsLib" }); + }); + + jsLibsRemoved.forEach((jsLib) => { + defs.push({ subject: jsLib, verb: "removed", type: "jsLib" }); + }); + + const tree = [] as StatusTreeStruct[]; + + if (defs.length > 0) { + const subject = defs.length === 1 ? "jsLib" : "jsLibs"; + + tree.push(createTreeNodeGroup(defs, subject)); + } + + return tree; +} + +export default function applicationStatusTransformer( + status: FetchStatusResponseData, +) { + const tree = [ + ...statusPageTransformer(status), + ...statusDatasourceTransformer(status), + ...statusJsLibTransformer(status), + ] as StatusTreeStruct[]; + + return tree; +} diff --git a/app/client/src/git/components/ConflictError/ConflictErrorView.tsx b/app/client/src/git/components/ConflictError/ConflictErrorView.tsx new file mode 100644 index 000000000000..5a62f6fa04ba --- /dev/null +++ b/app/client/src/git/components/ConflictError/ConflictErrorView.tsx @@ -0,0 +1,72 @@ +import React, { useCallback, useMemo } from "react"; +import styled from "styled-components"; +import { + createMessage, + GIT_CONFLICTING_INFO, + LEARN_MORE, + OPEN_REPO, +} from "ee/constants/messages"; +import { Button, Callout } from "@appsmith/ads"; + +const Row = styled.div` + display: flex; + align-items: center; +`; + +const StyledButton = styled(Button)` + margin-right: ${(props) => props.theme.spaces[3]}px; +`; + +const StyledCallout = styled(Callout)` + margin-bottom: 12px; +`; + +const ConflictInfoContainer = styled.div` + margin-top: ${(props) => props.theme.spaces[7]}px; + margin-bottom: ${(props) => props.theme.spaces[7]}px; +`; + +interface ConflictErrorViewProps { + learnMoreUrl: string; + repoUrl: string; +} + +export default function ConflictErrorView({ + learnMoreUrl, + repoUrl, +}: ConflictErrorViewProps) { + const handleClickOnOpenRepo = useCallback(() => { + window.open(repoUrl, "_blank", "noopener,noreferrer"); + }, [repoUrl]); + + const calloutLinks = useMemo( + () => [ + { + children: createMessage(LEARN_MORE), + to: learnMoreUrl, + }, + ], + [learnMoreUrl], + ); + + return ( + + + {createMessage(GIT_CONFLICTING_INFO)} + + + + {createMessage(OPEN_REPO)} + + + + ); +} diff --git a/app/client/src/git/components/ConflictError/index.tsx b/app/client/src/git/components/ConflictError/index.tsx new file mode 100644 index 000000000000..c5d49533f8ed --- /dev/null +++ b/app/client/src/git/components/ConflictError/index.tsx @@ -0,0 +1,15 @@ +import React from "react"; + +import { useGitContext } from "../GitContextProvider"; +import GitConflictErrorView from "./ConflictErrorView"; + +export default function ConflictError() { + const { gitMetadata } = useGitContext(); + + // ! case: learnMoreUrl comes from pullError + const learnMoreUrl = + "https://docs.appsmith.com/advanced-concepts/version-control-with-git"; + const repoUrl = gitMetadata?.browserSupportedRemoteUrl || ""; + + return ; +} diff --git a/app/client/src/git/components/ConflictErrorModal/ConflictErrorModalView.tsx b/app/client/src/git/components/ConflictErrorModal/ConflictErrorModalView.tsx new file mode 100644 index 000000000000..d59ff023af40 --- /dev/null +++ b/app/client/src/git/components/ConflictErrorModal/ConflictErrorModalView.tsx @@ -0,0 +1,93 @@ +import React, { useCallback } from "react"; +import styled from "styled-components"; +import { Overlay, Classes } from "@blueprintjs/core"; +import { + createMessage, + CONFLICTS_FOUND_WHILE_PULLING_CHANGES, +} from "ee/constants/messages"; + +import { Button } from "@appsmith/ads"; +import noop from "lodash/noop"; +import ConflictError from "../ConflictError"; + +const StyledGitErrorPopup = styled.div` + & { + .${Classes.OVERLAY} { + position: fixed; + bottom: 0; + left: 0; + right: 0; + display: flex; + justify-content: center; + + .${Classes.OVERLAY_CONTENT} { + overflow: hidden; + bottom: 52px; + left: 12px; + background-color: #ffffff; + } + } + + .git-error-popup { + width: 364px; + padding: ${(props) => props.theme.spaces[7]}px; + + display: flex; + flex-direction: column; + } + } +`; + +interface ConflictErrorModalViewProps { + isConflictErrorModalOpen?: boolean; + toggleConflictErrorModal?: (open: boolean) => void; +} + +function ConflictErrorModalView({ + isConflictErrorModalOpen = false, + toggleConflictErrorModal = noop, +}: ConflictErrorModalViewProps) { + const handleClose = useCallback(() => { + toggleConflictErrorModal(false); + }, [toggleConflictErrorModal]); + + return ( + + +
+
+
+
+ + {createMessage(CONFLICTS_FOUND_WHILE_PULLING_CHANGES)} + +
+
+ +
+
+
+
+ ); +} + +export default ConflictErrorModalView; diff --git a/app/client/src/git/components/ConflictErrorModal/index.tsx b/app/client/src/git/components/ConflictErrorModal/index.tsx new file mode 100644 index 000000000000..0de96380c6d1 --- /dev/null +++ b/app/client/src/git/components/ConflictErrorModal/index.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import ConflictErrorModalView from "./ConflictErrorModalView"; +import { useGitContext } from "../GitContextProvider"; + +export default function ConflictErrorModal() { + const { conflictErrorModalOpen, toggleConflictErrorModal } = useGitContext(); + + return ( + + ); +} diff --git a/app/client/src/git/components/GitContextProvider/hooks/useGitBranches.ts b/app/client/src/git/components/GitContextProvider/hooks/useGitBranches.ts index 4d23f042f795..551338860fad 100644 --- a/app/client/src/git/components/GitContextProvider/hooks/useGitBranches.ts +++ b/app/client/src/git/components/GitContextProvider/hooks/useGitBranches.ts @@ -5,9 +5,10 @@ import { selectBranches, selectCheckoutBranch, selectCreateBranch, + selectCurrentBranch, selectDeleteBranch, } from "git/store/selectors/gitSingleArtifactSelectors"; -import type { GitRootState } from "git/store/types"; +import type { GitApiError, GitRootState } from "git/store/types"; import { useCallback, useMemo } from "react"; import { useDispatch, useSelector } from "react-redux"; @@ -19,18 +20,19 @@ interface UseGitBranchesParams { export interface UseGitBranchesReturnValue { branches: FetchBranchesResponseData | null; fetchBranchesLoading: boolean; - fetchBranchesError: string | null; + fetchBranchesError: GitApiError | null; fetchBranches: () => void; createBranchLoading: boolean; - createBranchError: string | null; + createBranchError: GitApiError | null; createBranch: (branchName: string) => void; deleteBranchLoading: boolean; - deleteBranchError: string | null; + deleteBranchError: GitApiError | null; deleteBranch: (branchName: string) => void; checkoutBranchLoading: boolean; - checkoutBranchError: string | null; + checkoutBranchError: GitApiError | null; checkoutBranch: (branchName: string) => void; - toggleGitBranchListPopup: (open: boolean) => void; + currentBranch: string | null; + toggleBranchListPopup: (open: boolean) => void; } export default function useGitBranches({ @@ -102,10 +104,15 @@ export default function useGitBranches({ [basePayload, dispatch], ); + // derived + const currentBranch = useSelector((state: GitRootState) => + selectCurrentBranch(state, basePayload), + ); + // git branch list popup - const toggleGitBranchListPopup = (open: boolean) => { + const toggleBranchListPopup = (open: boolean) => { dispatch( - gitArtifactActions.toggleGitBranchListPopup({ + gitArtifactActions.toggleBranchListPopup({ ...basePayload, open, }), @@ -126,6 +133,7 @@ export default function useGitBranches({ checkoutBranchLoading: checkoutBranchState?.loading ?? false, checkoutBranchError: checkoutBranchState?.error, checkoutBranch, - toggleGitBranchListPopup, + currentBranch: currentBranch ?? null, + toggleBranchListPopup, }; } diff --git a/app/client/src/git/components/GitContextProvider/hooks/useGitConnect.ts b/app/client/src/git/components/GitContextProvider/hooks/useGitConnect.ts index 900f1716c8c5..9741a857e7a3 100644 --- a/app/client/src/git/components/GitContextProvider/hooks/useGitConnect.ts +++ b/app/client/src/git/components/GitContextProvider/hooks/useGitConnect.ts @@ -9,7 +9,7 @@ interface UseGitConnectParams { } export interface UseGitConnectReturnValue { - toggleGitConnectModal: (open: boolean) => void; + toggleConnectModal: (open: boolean) => void; } export default function useGitConnect({ @@ -22,9 +22,9 @@ export default function useGitConnect({ [artifactType, baseArtifactId], ); - const toggleGitConnectModal = (open: boolean) => { + const toggleConnectModal = (open: boolean) => { dispatch( - gitArtifactActions.toggleGitConnectModal({ + gitArtifactActions.toggleConnectModal({ ...basePayload, open, }), @@ -32,6 +32,6 @@ export default function useGitConnect({ }; return { - toggleGitConnectModal, + toggleConnectModal, }; } diff --git a/app/client/src/git/components/GitContextProvider/hooks/useGitContextValue.ts b/app/client/src/git/components/GitContextProvider/hooks/useGitContextValue.ts index 0945eea223f0..4a11ca17a73e 100644 --- a/app/client/src/git/components/GitContextProvider/hooks/useGitContextValue.ts +++ b/app/client/src/git/components/GitContextProvider/hooks/useGitContextValue.ts @@ -11,9 +11,19 @@ import { useMemo } from "react"; import type { UseGitMetadataReturnValue } from "./useGitMetadata"; import useGitMetadata from "./useGitMetadata"; -interface UseGitContextValueParams { +// internal dependencies +import type { ApplicationPayload } from "entities/Application"; +import type { FetchStatusResponseData } from "git/requests/fetchStatusRequest.types"; +import type { StatusTreeStruct } from "git/components/StatusChanges/StatusTree"; + +export interface UseGitContextValueParams { artifactType: keyof typeof GitArtifactType; baseArtifactId: string; + artifact: ApplicationPayload | null; + connectPermitted: boolean; + statusTransformer: ( + status: FetchStatusResponseData, + ) => StatusTreeStruct[] | null; } export interface GitContextValue @@ -21,11 +31,20 @@ export interface GitContextValue UseGitConnectReturnValue, UseGitOpsReturnValue, UseGitSettingsReturnValue, - UseGitBranchesReturnValue {} + UseGitBranchesReturnValue { + artifact: ApplicationPayload | null; + connectPermitted: boolean; + statusTransformer: ( + status: FetchStatusResponseData, + ) => StatusTreeStruct[] | null; +} export default function useGitContextValue({ + artifact, artifactType, - baseArtifactId, + baseArtifactId = "", + connectPermitted, + statusTransformer, }: UseGitContextValueParams): GitContextValue { const basePayload = useMemo( () => ({ artifactType, baseArtifactId }), @@ -33,11 +52,17 @@ export default function useGitContextValue({ ); const useGitMetadataReturnValue = useGitMetadata(basePayload); const useGitConnectReturnValue = useGitConnect(basePayload); - const useGitOpsReturnValue = useGitOps(basePayload); + const useGitOpsReturnValue = useGitOps({ + ...basePayload, + artifactId: artifact?.id ?? null, + }); const useGitBranchesReturnValue = useGitBranches(basePayload); const useGitSettingsReturnValue = useGitSettings(basePayload); return { + statusTransformer, + artifact, + connectPermitted, ...useGitMetadataReturnValue, ...useGitOpsReturnValue, ...useGitBranchesReturnValue, diff --git a/app/client/src/git/components/GitContextProvider/hooks/useGitMetadata.ts b/app/client/src/git/components/GitContextProvider/hooks/useGitMetadata.ts index b1a10de4a1a5..aabc125c8382 100644 --- a/app/client/src/git/components/GitContextProvider/hooks/useGitMetadata.ts +++ b/app/client/src/git/components/GitContextProvider/hooks/useGitMetadata.ts @@ -4,7 +4,7 @@ import { selectGitConnected, selectGitMetadata, } from "git/store/selectors/gitSingleArtifactSelectors"; -import type { GitRootState } from "git/store/types"; +import type { GitApiError, GitRootState } from "git/store/types"; import { useMemo } from "react"; import { useSelector } from "react-redux"; @@ -16,7 +16,7 @@ interface UseGitMetadataParams { export interface UseGitMetadataReturnValue { gitMetadata: FetchGitMetadataResponseData | null; fetchGitMetadataLoading: boolean; - fetchGitMetadataError: string | null; + fetchGitMetadataError: GitApiError | null; gitConnected: boolean; } diff --git a/app/client/src/git/components/GitContextProvider/hooks/useGitOps.ts b/app/client/src/git/components/GitContextProvider/hooks/useGitOps.ts index 1d0b59f71cd0..97b224f33717 100644 --- a/app/client/src/git/components/GitContextProvider/hooks/useGitOps.ts +++ b/app/client/src/git/components/GitContextProvider/hooks/useGitOps.ts @@ -4,46 +4,58 @@ import type { FetchStatusResponseData } from "git/requests/fetchStatusRequest.ty import { gitArtifactActions } from "git/store/gitArtifactSlice"; import { selectCommit, + selectConflictErrorModalOpen, selectDiscard, selectMerge, selectMergeStatus, + selectOpsModalOpen, + selectOpsModalTab, selectPull, selectStatus, } from "git/store/selectors/gitSingleArtifactSelectors"; -import type { GitRootState } from "git/store/types"; +import type { GitApiError, GitRootState } from "git/store/types"; import { useCallback, useMemo } from "react"; import { useDispatch, useSelector } from "react-redux"; interface UseGitOpsParams { artifactType: keyof typeof GitArtifactType; baseArtifactId: string; + artifactId: string | null; } export interface UseGitOpsReturnValue { commitLoading: boolean; - commitError: string | null; + commitError: GitApiError | null; commit: (commitMessage: string) => void; + clearCommitError: () => void; discardLoading: boolean; - discardError: string | null; + discardError: GitApiError | null; discard: () => void; + clearDiscardError: () => void; status: FetchStatusResponseData | null; fetchStatusLoading: boolean; - fetchStatusError: string | null; + fetchStatusError: GitApiError | null; fetchStatus: () => void; mergeLoading: boolean; - mergeError: string | null; + mergeError: GitApiError | null; merge: () => void; mergeStatus: FetchMergeStatusResponseData | null; fetchMergeStatusLoading: boolean; - fetchMergeStatusError: string | null; - fetchMergeStatus: () => void; + fetchMergeStatusError: GitApiError | null; + fetchMergeStatus: (sourceBranch: string, destinationBranch: string) => void; + clearMergeStatus: () => void; pullLoading: boolean; - pullError: string | null; + pullError: GitApiError | null; pull: () => void; - toggleGitOpsModal: (open: boolean, tab?: keyof typeof GitOpsTab) => void; + opsModalTab: keyof typeof GitOpsTab; + opsModalOpen: boolean; + toggleOpsModal: (open: boolean, tab?: keyof typeof GitOpsTab) => void; + conflictErrorModalOpen: boolean; + toggleConflictErrorModal: (open: boolean) => void; } export default function useGitOps({ + artifactId, artifactType, baseArtifactId, }: UseGitOpsParams): UseGitOpsReturnValue { @@ -71,6 +83,10 @@ export default function useGitOps({ [basePayload, dispatch], ); + const clearCommitError = useCallback(() => { + dispatch(gitArtifactActions.clearCommitError(basePayload)); + }, [basePayload, dispatch]); + // discard const discardState = useSelector((state: GitRootState) => selectDiscard(state, basePayload), @@ -80,6 +96,10 @@ export default function useGitOps({ dispatch(gitArtifactActions.discardInit(basePayload)); }, [basePayload, dispatch]); + const clearDiscardError = useCallback(() => { + dispatch(gitArtifactActions.clearDiscardError(basePayload)); + }, [basePayload, dispatch]); + // status const statusState = useSelector((state: GitRootState) => selectStatus(state, basePayload), @@ -108,8 +128,22 @@ export default function useGitOps({ selectMergeStatus(state, basePayload), ); - const fetchMergeStatus = useCallback(() => { - dispatch(gitArtifactActions.fetchMergeStatusInit(basePayload)); + const fetchMergeStatus = useCallback( + (sourceBranch: string, destinationBranch: string) => { + dispatch( + gitArtifactActions.fetchMergeStatusInit({ + ...basePayload, + artifactId: artifactId ?? "", + sourceBranch, + destinationBranch, + }), + ); + }, + [artifactId, basePayload, dispatch], + ); + + const clearMergeStatus = useCallback(() => { + dispatch(gitArtifactActions.clearMergeStatus(basePayload)); }, [basePayload, dispatch]); // pull @@ -118,14 +152,41 @@ export default function useGitOps({ ); const pull = useCallback(() => { - dispatch(gitArtifactActions.pullInit(basePayload)); - }, [basePayload, dispatch]); + dispatch( + gitArtifactActions.pullInit({ + ...basePayload, + artifactId: artifactId ?? "", + }), + ); + }, [basePayload, artifactId, dispatch]); + + // ops modal + const opsModalOpen = useSelector((state: GitRootState) => + selectOpsModalOpen(state, basePayload), + ); - // git ops modal - const toggleGitOpsModal = useCallback( + const opsModalTab = useSelector((state: GitRootState) => + selectOpsModalTab(state, basePayload), + ); + + const toggleOpsModal = useCallback( (open: boolean, tab: keyof typeof GitOpsTab = GitOpsTab.Deploy) => { dispatch( - gitArtifactActions.toggleGitOpsModal({ ...basePayload, open, tab }), + gitArtifactActions.toggleOpsModal({ ...basePayload, open, tab }), + ); + }, + [basePayload, dispatch], + ); + + // conflict error modal + const conflictErrorModalOpen = useSelector((state: GitRootState) => + selectConflictErrorModalOpen(state, basePayload), + ); + + const toggleConflictErrorModal = useCallback( + (open: boolean) => { + dispatch( + gitArtifactActions.toggleConflictErrorModal({ ...basePayload, open }), ); }, [basePayload, dispatch], @@ -135,9 +196,11 @@ export default function useGitOps({ commitLoading: commitState?.loading ?? false, commitError: commitState?.error, commit, + clearCommitError, discardLoading: discardState?.loading ?? false, discardError: discardState?.error, discard, + clearDiscardError, status: statusState?.value, fetchStatusLoading: statusState?.loading ?? false, fetchStatusError: statusState?.error, @@ -149,9 +212,14 @@ export default function useGitOps({ fetchMergeStatusLoading: mergeStatusState?.loading ?? false, fetchMergeStatusError: mergeStatusState?.error, fetchMergeStatus, + clearMergeStatus, pullLoading: pullState?.loading ?? false, pullError: pullState?.error, pull, - toggleGitOpsModal, + opsModalTab, + opsModalOpen, + toggleOpsModal, + conflictErrorModalOpen, + toggleConflictErrorModal, }; } diff --git a/app/client/src/git/components/GitContextProvider/hooks/useGitSettings.ts b/app/client/src/git/components/GitContextProvider/hooks/useGitSettings.ts index 2e63f8e60325..521c2dac72c6 100644 --- a/app/client/src/git/components/GitContextProvider/hooks/useGitSettings.ts +++ b/app/client/src/git/components/GitContextProvider/hooks/useGitSettings.ts @@ -7,7 +7,7 @@ import { selectProtectedBranches, selectProtectedMode, } from "git/store/selectors/gitSingleArtifactSelectors"; -import type { GitRootState } from "git/store/types"; +import type { GitApiError, GitRootState } from "git/store/types"; import { useMemo } from "react"; import { useDispatch, useSelector } from "react-redux"; @@ -21,10 +21,10 @@ export interface UseGitSettingsReturnValue { autocommitPolling: boolean; protectedBranches: FetchProtectedBranchesResponseData | null; fetchProtectedBranchesLoading: boolean; - fetchProtectedBranchesError: string | null; + fetchProtectedBranchesError: GitApiError | null; fetchProtectedBranches: () => void; protectedMode: boolean; - toggleGitSettingsModal: ( + toggleSettingsModal: ( open: boolean, tab: keyof typeof GitSettingsTab, ) => void; @@ -67,12 +67,12 @@ export default function useGitSettings({ ); // ui - const toggleGitSettingsModal = ( + const toggleSettingsModal = ( open: boolean, tab: keyof typeof GitSettingsTab, ) => { dispatch( - gitArtifactActions.toggleGitSettingsModal({ + gitArtifactActions.toggleSettingsModal({ ...basePayload, open, tab, @@ -88,6 +88,6 @@ export default function useGitSettings({ fetchProtectedBranchesError: protectedBranchesState.error, fetchProtectedBranches, protectedMode: protectedMode ?? false, - toggleGitSettingsModal, + toggleSettingsModal, }; } diff --git a/app/client/src/git/components/GitContextProvider/index.tsx b/app/client/src/git/components/GitContextProvider/index.tsx index afb9f010a50c..59d3094476c5 100644 --- a/app/client/src/git/components/GitContextProvider/index.tsx +++ b/app/client/src/git/components/GitContextProvider/index.tsx @@ -1,6 +1,8 @@ import React, { createContext, useContext } from "react"; -import type { GitArtifactType } from "git/constants/enums"; -import type { GitContextValue } from "./hooks/useGitContextValue"; +import type { + GitContextValue, + UseGitContextValueParams, +} from "./hooks/useGitContextValue"; import useGitContextValue from "./hooks/useGitContextValue"; const gitContextInitialValue = {} as GitContextValue; @@ -11,21 +13,17 @@ export const useGitContext = () => { return useContext(GitContext); }; -interface GitContextProviderProps { - artifactType: keyof typeof GitArtifactType; - baseArtifactId: string; +interface GitContextProviderProps extends UseGitContextValueParams { children: React.ReactNode; // extra // connectPermitted?: boolean; } export default function GitContextProvider({ - artifactType, - baseArtifactId, children, - // connectPermitted = true, + ...useContextValueParams }: GitContextProviderProps) { - const contextValue = useGitContextValue({ artifactType, baseArtifactId }); + const contextValue = useGitContextValue(useContextValueParams); return ( {children} diff --git a/app/client/src/git/components/GitModals.tsx b/app/client/src/git/components/GitModals.tsx new file mode 100644 index 000000000000..443943ba736d --- /dev/null +++ b/app/client/src/git/components/GitModals.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import ConflictErrorModal from "./ConflictErrorModal"; +import OpsModal from "./OpsModal"; + +export default function GitModals() { + return ( + <> + + + + ); +} diff --git a/app/client/src/git/components/OpsModal/OpsModalView.tsx b/app/client/src/git/components/OpsModal/OpsModalView.tsx new file mode 100644 index 000000000000..54acf3f32b38 --- /dev/null +++ b/app/client/src/git/components/OpsModal/OpsModalView.tsx @@ -0,0 +1,105 @@ +import React, { useCallback, useEffect } from "react"; +import TabDeploy from "./TabDeploy"; +import TabMerge from "./TabMerge"; +import { createMessage, DEPLOY, MERGE } from "ee/constants/messages"; +import AnalyticsUtil from "ee/utils/AnalyticsUtil"; +import { + Modal, + ModalContent, + ModalHeader, + Tab, + Tabs, + TabsList, +} from "@appsmith/ads"; +import styled from "styled-components"; +// import ReconnectSSHError from "../components/ReconnectSSHError"; +import { GitOpsTab } from "git/constants/enums"; +import noop from "lodash/noop"; + +const StyledModalContent = styled(ModalContent)` + &&& { + width: 640px; + transform: none !important; + top: 100px; + left: calc(50% - 320px); + max-height: calc(100vh - 200px); + } +`; + +interface OpsModalViewProps { + fetchStatus: () => void; + isOpsModalOpen: boolean; + isProtectedMode: boolean; + opsModalTab: keyof typeof GitOpsTab; + repoName: string | null; + toggleOpsModal: (open: boolean, tab?: keyof typeof GitOpsTab) => void; +} + +function OpsModalView({ + fetchStatus = noop, + isOpsModalOpen = false, + isProtectedMode = false, + opsModalTab = GitOpsTab.Deploy, + repoName = null, + toggleOpsModal = noop, +}: OpsModalViewProps) { + useEffect( + function fetchStatusOnMountEffect() { + if (isOpsModalOpen) { + fetchStatus(); + } + }, + [isOpsModalOpen, fetchStatus], + ); + + const handleTabKeyChange = useCallback( + (tabKey: string) => { + if (tabKey === GitOpsTab.Deploy) { + AnalyticsUtil.logEvent("GS_DEPLOY_GIT_MODAL_TRIGGERED", { + source: `${tabKey.toUpperCase()}_TAB`, + }); + } else if (tabKey === GitOpsTab.Merge) { + AnalyticsUtil.logEvent("GS_MERGE_GIT_MODAL_TRIGGERED", { + source: `${tabKey.toUpperCase()}_TAB`, + }); + } + + toggleOpsModal(true, tabKey as GitOpsTab); + }, + [toggleOpsModal], + ); + + return ( + <> + + + {repoName} + {/* {isGitConnected && } */} + + + + {createMessage(DEPLOY)} + + + {createMessage(MERGE)} + + + + {opsModalTab === GitOpsTab.Deploy && } + {opsModalTab === GitOpsTab.Merge && } + + + {/* */} + + ); +} + +export default OpsModalView; diff --git a/app/client/src/git/components/OpsModal/TabDeploy/DeployPreview.tsx b/app/client/src/git/components/OpsModal/TabDeploy/DeployPreview.tsx new file mode 100644 index 000000000000..79abbe1439dc --- /dev/null +++ b/app/client/src/git/components/OpsModal/TabDeploy/DeployPreview.tsx @@ -0,0 +1,82 @@ +import React from "react"; + +import styled from "styled-components"; +import { useSelector } from "react-redux"; +import { + getApplicationLastDeployedAt, + getCurrentBasePageId, +} from "selectors/editorSelectors"; +import { + createMessage, + LATEST_DP_SUBTITLE, + LATEST_DP_TITLE, +} from "ee/constants/messages"; +import SuccessTick from "pages/common/SuccessTick"; +import { howMuchTimeBeforeText } from "utils/helpers"; +import AnalyticsUtil from "ee/utils/AnalyticsUtil"; +import { viewerURL } from "ee/RouteBuilder"; +import { Link, Text } from "@appsmith/ads"; +import { importSvg } from "@appsmith/ads-old"; + +const CloudyIcon = importSvg( + async () => import("assets/icons/ads/cloudy-line.svg"), +); + +const Container = styled.div` + display: flex; + flex: 1; + flex-direction: row; + gap: ${(props) => props.theme.spaces[6]}px; + + .cloud-icon { + stroke: var(--ads-v2-color-fg); + } +`; + +export default function DeployPreview() { + // ! case: should reset after timer + const showSuccess = false; + + const basePageId = useSelector(getCurrentBasePageId); + const lastDeployedAt = useSelector(getApplicationLastDeployedAt); + + const showDeployPreview = () => { + AnalyticsUtil.logEvent("GS_LAST_DEPLOYED_PREVIEW_LINK_CLICK", { + source: "GIT_DEPLOY_MODAL", + }); + const path = viewerURL({ + basePageId, + }); + + window.open(path, "_blank"); + }; + + const lastDeployedAtMsg = lastDeployedAt + ? `${createMessage(LATEST_DP_SUBTITLE)} ${howMuchTimeBeforeText( + lastDeployedAt, + { + lessThanAMinute: true, + }, + )} ago` + : ""; + + return lastDeployedAt ? ( + +
+ {showSuccess ? ( + + ) : ( + + )} +
+
+ + {createMessage(LATEST_DP_TITLE)} + + + {lastDeployedAtMsg} + +
+
+ ) : null; +} diff --git a/app/client/src/git/components/OpsModal/TabDeploy/DiscardChangesWarning.tsx b/app/client/src/git/components/OpsModal/TabDeploy/DiscardChangesWarning.tsx new file mode 100644 index 000000000000..730a419655db --- /dev/null +++ b/app/client/src/git/components/OpsModal/TabDeploy/DiscardChangesWarning.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import { + createMessage, + DISCARD_CHANGES_WARNING, + DISCARD_MESSAGE, +} from "ee/constants/messages"; +import { Callout, Text } from "@appsmith/ads"; +import styled from "styled-components"; + +const Container = styled.div` + margin: 8px 0 16px; +`; + +export default function DiscardChangesWarning({ + onCloseDiscardChangesWarning, // TODO: Fix this the next time the file is edited + // eslint-disable-next-line @typescript-eslint/no-explicit-any +}: any) { + const discardDocUrl = + "https://docs.appsmith.com/advanced-concepts/version-control-with-git/commit-and-push"; + + return ( + + + {createMessage(DISCARD_CHANGES_WARNING)} +
+ {createMessage(DISCARD_MESSAGE)} +
+
+ ); +} diff --git a/app/client/src/git/components/OpsModal/TabDeploy/DiscardFailedError.tsx b/app/client/src/git/components/OpsModal/TabDeploy/DiscardFailedError.tsx new file mode 100644 index 000000000000..740193af9038 --- /dev/null +++ b/app/client/src/git/components/OpsModal/TabDeploy/DiscardFailedError.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import styled from "styled-components"; +import { Callout } from "@appsmith/ads"; +import type { CalloutProps } from "@appsmith/ads"; +import type { GitApiError } from "git/store/types"; + +const Container = styled.div` + margin: 8px 0 16px; +`; + +export default function DiscardFailedError({ + closeHandler, + error, +}: { + closeHandler: () => void; + error: GitApiError; +}) { + const calloutOptions: CalloutProps = { + isClosable: true, + kind: "error", + onClose: () => closeHandler(), + children: error?.message ?? "", + }; + + return ( + + + + ); +} diff --git a/app/client/src/git/components/OpsModal/TabDeploy/PushFailedError.tsx b/app/client/src/git/components/OpsModal/TabDeploy/PushFailedError.tsx new file mode 100644 index 000000000000..25fb748928cd --- /dev/null +++ b/app/client/src/git/components/OpsModal/TabDeploy/PushFailedError.tsx @@ -0,0 +1,31 @@ +import { Callout } from "@appsmith/ads"; +import { Text, TextType } from "@appsmith/ads-old"; +import type { GitApiError } from "git/store/types"; +import React from "react"; +import styled from "styled-components"; + +const Container = styled.div` + margin: 8px 0 16px; +`; + +export interface PushFailedErrorProps { + closeHandler: () => void; + error: GitApiError; +} + +export default function PushFailedError({ + closeHandler, + error, +}: PushFailedErrorProps) { + return ( + + + <> + {error.errorType} +
+ {error.message} + +
+
+ ); +} diff --git a/app/client/src/git/components/OpsModal/TabDeploy/SubmitWrapper.tsx b/app/client/src/git/components/OpsModal/TabDeploy/SubmitWrapper.tsx new file mode 100644 index 000000000000..3e11cf40e0e4 --- /dev/null +++ b/app/client/src/git/components/OpsModal/TabDeploy/SubmitWrapper.tsx @@ -0,0 +1,29 @@ +import noop from "lodash/noop"; +import React, { useCallback } from "react"; +import { isMacOrIOS } from "utils/helpers"; + +interface SubmitWrapperProps { + children: React.ReactNode; + onSubmit: () => void; +} + +export default function SubmitWrapper({ + children = null, + onSubmit = noop, +}: SubmitWrapperProps) { + const onKeyDown = useCallback( + (e: React.KeyboardEvent) => { + const triggerSubmit = isMacOrIOS() + ? e.metaKey && e.key === "Enter" + : e.ctrlKey && e.key === "Enter"; + + if (triggerSubmit) { + e.preventDefault(); + onSubmit(); + } + }, + [onSubmit], + ); + + return
{children}
; +} diff --git a/app/client/src/git/components/OpsModal/TabDeploy/TabDeployView.tsx b/app/client/src/git/components/OpsModal/TabDeploy/TabDeployView.tsx new file mode 100644 index 000000000000..d22c93f79b97 --- /dev/null +++ b/app/client/src/git/components/OpsModal/TabDeploy/TabDeployView.tsx @@ -0,0 +1,407 @@ +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { + ARE_YOU_SURE, + COMMIT_AND_PUSH, + COMMIT_TO, + COMMITTING_AND_PUSHING_CHANGES, + createMessage, + DISCARD_CHANGES, + DISCARDING_AND_PULLING_CHANGES, + GIT_NO_UPDATED_TOOLTIP, + PULL_CHANGES, +} from "ee/constants/messages"; +import styled from "styled-components"; +import { + Button, + Input, + ModalBody, + ModalFooter, + Text, + Tooltip, +} from "@appsmith/ads"; +import DeployPreview from "./DeployPreview"; +import Statusbar, { + StatusbarWrapper, +} from "pages/Editor/gitSync/components/Statusbar"; + +import { isEllipsisActive } from "utils/helpers"; +import AnalyticsUtil from "ee/utils/AnalyticsUtil"; +import GIT_ERROR_CODES from "constants/GitErrorCodes"; +import DiscardChangesWarning from "./DiscardChangesWarning"; +import PushFailedError from "./PushFailedError"; +import DiscardFailedError from "./DiscardFailedError"; +import StatusChanges from "git/components/StatusChanges"; +import ConflictError from "git/components/ConflictError"; +import SubmitWrapper from "./SubmitWrapper"; +import noop from "lodash/noop"; +import type { GitApiError } from "git/store/types"; + +const Section = styled.div` + margin-top: 0; + margin-bottom: ${(props) => props.theme.spaces[7]}px; +`; + +const Row = styled.div` + display: flex; + align-items: center; +`; + +const StyledModalFooter = styled(ModalFooter)` + min-height: 52px; +`; + +const CommitLabelText = styled(Text)` + min-width: fit-content; +`; + +const CommitLabelBranchText = styled(Text)` + overflow: hidden; + text-overflow: ellipsis; + whitespace: nowrap; +`; + +const FIRST_COMMIT = "First Commit"; +const NO_CHANGES_TO_COMMIT = "No changes to commit"; + +interface TabDeployViewProps { + clearCommitError: () => void; + clearDiscardError: () => void; + commit: (commitMessage: string) => void; + commitError: GitApiError | null; + currentBranch: string | null; + discard: () => void; + discardError: GitApiError | null; + isCommitLoading: boolean; + isDiscardLoading: boolean; + isFetchStatusLoading: boolean; + isPullFailing: boolean; + isPullLoading: boolean; + lastDeployedAt: string | null; + pull: () => void; + remoteUrl: string | null; + statusBehindCount: number; + statusIsClean: boolean; +} + +function TabDeployView({ + clearCommitError = noop, + clearDiscardError = noop, + commit = noop, + commitError = null, + currentBranch = null, + discard = noop, + discardError = null, + isCommitLoading = false, + isDiscardLoading = false, + isFetchStatusLoading = false, + isPullFailing = false, + isPullLoading = false, + lastDeployedAt = null, + pull = noop, + remoteUrl = null, + statusBehindCount = 0, + statusIsClean = false, +}: TabDeployViewProps) { + const hasChangesToCommit = !statusIsClean; + const commitInputRef = useRef(null); + const [commitMessage, setCommitMessage] = useState( + remoteUrl && lastDeployedAt ? "" : FIRST_COMMIT, + ); + const [shouldDiscard, setShouldDiscard] = useState(false); + const [isDiscarding, setIsDiscarding] = useState(isDiscardLoading); + const [showDiscardWarning, setShowDiscardWarning] = useState(false); + + const commitButtonDisabled = + !hasChangesToCommit || !commitMessage || commitMessage.trim().length < 1; + const commitButtonLoading = isCommitLoading; + + const commitRequired = !statusIsClean; + const isConflicting = !isFetchStatusLoading && !!isPullFailing; + const commitInputDisabled = + isConflicting || !hasChangesToCommit || isCommitLoading || isDiscarding; + const pullRequired = + commitError?.code === + GIT_ERROR_CODES.PUSH_FAILED_REMOTE_COUNTERPART_IS_AHEAD; + + const showCommitButton = + !isConflicting && + !pullRequired && + !isFetchStatusLoading && + !isCommitLoading && + !isDiscarding; + + const isCommitting = + !!commitButtonLoading && + (commitRequired || showCommitButton) && + !isDiscarding; + + const showDiscardChangesButton = + !isFetchStatusLoading && + !isCommitLoading && + hasChangesToCommit && + !isDiscarding && + !isCommitting; + + const commitMessageDisplay = hasChangesToCommit + ? commitMessage + : NO_CHANGES_TO_COMMIT; + + const showPullButton = + !isFetchStatusLoading && + ((pullRequired && !isConflicting) || + (statusBehindCount > 0 && statusIsClean)); + + useEffect( + function focusCommitInputEffect() { + if (!commitInputDisabled && commitInputRef.current) { + commitInputRef.current.focus(); + } + }, + [commitInputDisabled], + ); + + useEffect( + function discardErrorChangeEffect() { + if (discardError) { + setIsDiscarding(false); + setShouldDiscard(false); + } + }, + [discardError], + ); + + const scrollWrapperRef = React.createRef(); + + useEffect( + function scrollContainerToTopEffect() { + if (scrollWrapperRef.current) { + setTimeout(() => { + const top = scrollWrapperRef.current?.scrollHeight || 0; + + scrollWrapperRef.current?.scrollTo({ + top: top, + }); + }, 100); + } + }, + [scrollWrapperRef], + ); + + const triggerCommit = useCallback(() => { + setShowDiscardWarning(false); + AnalyticsUtil.logEvent("GS_COMMIT_AND_PUSH_BUTTON_CLICK", { + source: "GIT_DEPLOY_MODAL", + }); + + if (currentBranch) { + commit(commitMessage.trim()); + } + }, [commit, commitMessage, currentBranch]); + + const handleCommitViaKeyPress = useCallback(() => { + if (!commitButtonDisabled) { + triggerCommit(); + } + }, [commitButtonDisabled, triggerCommit]); + + const triggerPull = useCallback(() => { + AnalyticsUtil.logEvent("GS_PULL_GIT_CLICK", { + source: "GIT_DEPLOY_MODAL", + }); + + if (currentBranch) { + pull(); + } + }, [currentBranch, pull]); + + const triggerDiscardInit = useCallback(() => { + AnalyticsUtil.logEvent("GIT_DISCARD_WARNING", { + source: "GIT_DISCARD_BUTTON_PRESS_1", + }); + setShowDiscardWarning(true); + setShouldDiscard(true); + clearDiscardError(); + }, [clearDiscardError]); + + const triggerDiscardChanges = useCallback(() => { + AnalyticsUtil.logEvent("GIT_DISCARD", { + source: "GIT_DISCARD_BUTTON_PRESS_2", + }); + discard(); + setShowDiscardWarning(false); + setShouldDiscard(true); + setIsDiscarding(true); + }, [discard]); + + const handleDiscardBtnClick = useCallback(() => { + if (shouldDiscard) { + triggerDiscardChanges(); + } else { + triggerDiscardInit(); + } + }, [shouldDiscard, triggerDiscardChanges, triggerDiscardInit]); + + const onCloseDiscardWarning = useCallback(() => { + AnalyticsUtil.logEvent("GIT_DISCARD_CANCEL", { + source: "GIT_DISCARD_WARNING_BANNER_CLOSE_CLICK", + }); + setShowDiscardWarning(false); + setShouldDiscard(false); + }, []); + + function handleCommitAndPushErrorClose() { + clearCommitError(); + } + + function handleDiscardErrorClose() { + clearDiscardError(); + } + + const inputLabel = useMemo( + () => ( + + {createMessage(COMMIT_TO)} + + +  {currentBranch} + + + + ), + [currentBranch], + ); + + return ( + <> + +
+
+ + + + + {isConflicting && } + {commitError && ( + + )} + {isCommitting && !isDiscarding && ( + + + + )} + {isDiscarding && !isCommitting && ( + + + + )} +
+ + {discardError && ( + + )} + + {showDiscardWarning && ( + + )} + + {!pullRequired && !isConflicting && } +
+
+ + {showPullButton && ( + + )} + + {showDiscardChangesButton && ( + + )} + {showCommitButton && ( + + + + )} + + + ); +} + +export default TabDeployView; diff --git a/app/client/src/git/components/OpsModal/TabDeploy/index.tsx b/app/client/src/git/components/OpsModal/TabDeploy/index.tsx new file mode 100644 index 000000000000..e363f571f4fe --- /dev/null +++ b/app/client/src/git/components/OpsModal/TabDeploy/index.tsx @@ -0,0 +1,52 @@ +import React from "react"; +import TabDeployView from "./TabDeployView"; +import { useGitContext } from "git/components/GitContextProvider"; + +export default function TabDeploy() { + const { + artifact, + clearCommitError, + clearDiscardError, + commit, + commitError, + commitLoading, + currentBranch, + discard, + discardError, + discardLoading, + fetchStatusLoading, + gitMetadata, + pull, + pullError, + pullLoading, + status, + } = useGitContext(); + + const lastDeployedAt = artifact?.lastDeployedAt ?? null; + const isPullFailing = !!pullError; + const statusIsClean = status?.isClean ?? false; + const statusBehindCount = status?.behindCount ?? 0; + const remoteUrl = gitMetadata?.remoteUrl ?? ""; + + return ( + + ); +} diff --git a/app/client/src/git/components/OpsModal/TabMerge/MergeStatus.tsx b/app/client/src/git/components/OpsModal/TabMerge/MergeStatus.tsx new file mode 100644 index 000000000000..1970332600ed --- /dev/null +++ b/app/client/src/git/components/OpsModal/TabMerge/MergeStatus.tsx @@ -0,0 +1,77 @@ +import React from "react"; +import styled from "styled-components"; +import { Icon, Spinner, Text } from "@appsmith/ads"; +import { MergeStatusState } from "git/constants/enums"; + +const LoaderWrapper = styled.div` + display: flex; + flex-direction: row; + align-items: center; + margin-top: ${(props) => `${props.theme.spaces[3]}px`}; +`; + +const Flex = styled.div` + display: flex; +`; + +const Wrapper = styled.div` + display: flex; + flex-direction: row; + margin-top: ${(props) => `${props.theme.spaces[3]}px`}; + width: 45%; + align-items: flex-start; + gap: 5px; + .ads-v2-icon { + margin-top: 3px; + } +`; + +function MergeStatus({ + message = "", + status, +}: { + status: string; + message: string | null; +}) { + switch (status) { + case MergeStatusState.FETCHING: + return ( + + + + {message} + + + ); + case MergeStatusState.MERGEABLE: + return ( + + + + {message} + + + + ); + case MergeStatusState.NOT_MERGEABLE: + case MergeStatusState.ERROR: + return ( + + + + + {message} + + + + ); + default: + return null; + } +} + +export default MergeStatus; diff --git a/app/client/src/git/components/OpsModal/TabMerge/MergeSuccessIndicator.tsx b/app/client/src/git/components/OpsModal/TabMerge/MergeSuccessIndicator.tsx new file mode 100644 index 000000000000..2515513c0fa9 --- /dev/null +++ b/app/client/src/git/components/OpsModal/TabMerge/MergeSuccessIndicator.tsx @@ -0,0 +1,21 @@ +import { Text } from "@appsmith/ads"; +import { createMessage, MERGED_SUCCESSFULLY } from "ee/constants/messages"; +import React from "react"; + +// internal dependencies +import SuccessTick from "pages/common/SuccessTick"; + +export default function MergeSuccessIndicator() { + return ( +
+ + + {createMessage(MERGED_SUCCESSFULLY)} + +
+ ); +} diff --git a/app/client/src/git/components/OpsModal/TabMerge/TabMergeView.tsx b/app/client/src/git/components/OpsModal/TabMerge/TabMergeView.tsx new file mode 100644 index 000000000000..b1be40e900dc --- /dev/null +++ b/app/client/src/git/components/OpsModal/TabMerge/TabMergeView.tsx @@ -0,0 +1,325 @@ +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import styled from "styled-components"; + +import { + BRANCH_PROTECTION_PROTECTED, + CANNOT_MERGE_DUE_TO_UNCOMMITTED_CHANGES, + createMessage, + FETCH_GIT_STATUS, + FETCH_MERGE_STATUS, + IS_MERGING, + MERGE_CHANGES, + SELECT_BRANCH_TO_MERGE, +} from "ee/constants/messages"; + +import Statusbar, { + StatusbarWrapper, +} from "pages/Editor/gitSync/components/Statusbar"; +import { getIsStartingWithRemoteBranches } from "pages/Editor/gitSync/utils"; +import { + Button, + Option, + Select, + Text, + Icon, + ModalFooter, + ModalBody, +} from "@appsmith/ads"; +import AnalyticsUtil from "ee/utils/AnalyticsUtil"; +import { MergeStatusState } from "git/constants/enums"; +import MergeStatus from "./MergeStatus"; +import ConflictError from "git/components/ConflictError"; +import MergeSuccessIndicator from "./MergeSuccessIndicator"; +import { noop } from "lodash"; +import type { FetchBranchesResponseData } from "git/requests/fetchBranchesRequest.types"; +import type { FetchProtectedBranchesResponseData } from "git/requests/fetchProtectedBranchesRequest.types"; +import type { FetchMergeStatusResponseData } from "git/requests/fetchMergeStatusRequest.types"; +import type { GitApiError } from "git/store/types"; + +const Container = styled.div` + min-height: 360px; + overflow: unset; + padding-bottom: 4px; +`; + +const MergeSelectLabel = styled(Text)` + margin-bottom: 12px; + color: var(--ads-v2-color-fg-emphasis); +`; + +const SelectContainer = styled.div` + display: flex; + align-items: center; + overflow: unset; + padding-bottom: 4px; +`; + +const StyledModalFooter = styled(ModalFooter)` + min-height: 52px; +`; + +interface BranchOption { + label: string; + value: string; +} + +interface TabMergeViewProps { + branches: FetchBranchesResponseData | null; + clearMergeStatus: () => void; + currentBranch: string | null; + fetchBranches: () => void; + fetchMergeStatus: (sourceBranch: string, destinationBranch: string) => void; + isFetchBranchesLoading: boolean; + isFetchMergeStatusLoading: boolean; + isFetchStatusLoading: boolean; + isMergeLoading: boolean; + isStatusClean: boolean; + merge: (sourceBranch: string, destinationBranch: string) => void; + mergeError: GitApiError | null; + mergeStatus: FetchMergeStatusResponseData | null; + protectedBranches: FetchProtectedBranchesResponseData | null; +} + +export default function TabMergeView({ + branches = null, + clearMergeStatus = noop, + currentBranch = null, + fetchBranches = noop, + fetchMergeStatus = noop, + isFetchBranchesLoading = false, + isFetchMergeStatusLoading = false, + isFetchStatusLoading = false, + isMergeLoading = false, + isStatusClean = false, + merge = noop, + mergeError = null, + mergeStatus = null, + protectedBranches = null, +}: TabMergeViewProps) { + const [showMergeSuccessIndicator, setShowMergeSuccessIndicator] = + useState(false); + const [selectedBranchOption, setSelectedBranchOption] = + useState(); + + const isMergeable = mergeStatus?.isMergeAble && isStatusClean; + let message = !isStatusClean + ? createMessage(CANNOT_MERGE_DUE_TO_UNCOMMITTED_CHANGES) + : mergeStatus?.message ?? null; + + const mergeBtnDisabled = isFetchMergeStatusLoading || !isMergeable; + + let status = MergeStatusState.NONE; + + if (isFetchStatusLoading) { + status = MergeStatusState.FETCHING; + message = createMessage(FETCH_GIT_STATUS); + } else if (!isStatusClean) { + status = MergeStatusState.NOT_MERGEABLE; + } else if (isFetchMergeStatusLoading) { + status = MergeStatusState.FETCHING; + message = createMessage(FETCH_MERGE_STATUS); + } else if (mergeStatus && mergeStatus?.isMergeAble) { + status = MergeStatusState.MERGEABLE; + } else if (mergeStatus && !mergeStatus?.isMergeAble) { + status = MergeStatusState.NOT_MERGEABLE; + } else if (mergeError) { + status = MergeStatusState.ERROR; + message = mergeError.message; + } + + // should check after added error code for conflicting + const isConflicting = (mergeStatus?.conflictingFiles?.length || 0) > 0; + const showMergeButton = + !isConflicting && !mergeError && !isFetchStatusLoading && !isMergeLoading; + + const branchList = useMemo(() => { + const branchOptions = [] as BranchOption[]; + + if (!branches) return branchOptions; + + let index = 0; + + while (true) { + if (index === branches.length) break; + + const branchObj = branches[index]; + + if (currentBranch !== branchObj.branchName) { + if (!branchObj.default) { + branchOptions.push({ + label: branchObj.branchName, + value: branchObj.branchName, + }); + } else { + branchOptions.unshift({ + label: branchObj.branchName, + value: branchObj.branchName, + }); + } + } + + const nextBranchObj = branches[index + 1]; + + if ( + getIsStartingWithRemoteBranches( + branchObj.branchName, + nextBranchObj?.branchName, + ) + ) { + break; + } + + index++; + } + + return branchOptions; + }, [branches, currentBranch]); + + const currentBranchDropdownOptions = useMemo( + () => [ + { + label: currentBranch || "", + value: currentBranch || "", + }, + ], + [currentBranch], + ); + + // ! case how to do this + // const handleMergeSuccess = () => { + // setShowMergeSuccessIndicator(true); + // }; + + useEffect( + function fetchBranchesOnMountffect() { + fetchBranches(); + }, + [fetchBranches], + ); + + useEffect( + function clearMergeStatusOnUnmountEffect() { + return () => { + clearMergeStatus(); + }; + }, + [clearMergeStatus], + ); + + useEffect( + function fetchMergeStatusOnChangeEffect() { + // when user selects a branch to merge + if (currentBranch && selectedBranchOption?.value) { + fetchMergeStatus(currentBranch, selectedBranchOption?.value); + setShowMergeSuccessIndicator(false); + } + }, + [currentBranch, selectedBranchOption?.value, fetchMergeStatus], + ); + + const handleMergeBtnClick = useCallback(() => { + AnalyticsUtil.logEvent("GS_MERGE_CHANGES_BUTTON_CLICK", { + source: "GIT_MERGE_MODAL", + }); + + if (currentBranch && selectedBranchOption?.value) { + merge(currentBranch, selectedBranchOption?.value); + } + }, [currentBranch, merge, selectedBranchOption?.value]); + + const handleSelectBranchOption = useCallback((value?: string) => { + if (value) setSelectedBranchOption({ label: value, value: value }); + }, []); + + const handleGetPopupContainer = useCallback((triggerNode) => { + return triggerNode.parentNode; + }, []); + + return ( + <> + + + + {createMessage(SELECT_BRANCH_TO_MERGE)} + + + + + + +
+ +
+ {isConflicting ? : null} + {showMergeSuccessIndicator ? : null} + {isMergeLoading ? ( + + + + ) : null} +
+
+ + {!showMergeSuccessIndicator && showMergeButton ? ( + + ) : null} + + + ); +} diff --git a/app/client/src/git/components/OpsModal/TabMerge/index.tsx b/app/client/src/git/components/OpsModal/TabMerge/index.tsx new file mode 100644 index 000000000000..204cf25093f3 --- /dev/null +++ b/app/client/src/git/components/OpsModal/TabMerge/index.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import TabMergeView from "./TabMergeView"; +import { useGitContext } from "git/components/GitContextProvider"; + +export default function TabMerge() { + const { + branches, + clearMergeStatus, + currentBranch, + fetchBranches, + fetchBranchesLoading, + fetchMergeStatus, + fetchMergeStatusLoading, + fetchStatusLoading, + merge, + mergeError, + mergeLoading, + mergeStatus, + protectedBranches, + status, + } = useGitContext(); + + const isStatusClean = status?.isClean ?? false; + + return ( + + ); +} diff --git a/app/client/src/git/components/OpsModal/index.tsx b/app/client/src/git/components/OpsModal/index.tsx new file mode 100644 index 000000000000..a44e20e5f1ef --- /dev/null +++ b/app/client/src/git/components/OpsModal/index.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import OpsModalView from "./OpsModalView"; +import { useGitContext } from "../GitContextProvider"; + +export default function OpsModal() { + const { + fetchStatus, + gitMetadata, + opsModalOpen, + opsModalTab, + protectedMode, + toggleOpsModal, + } = useGitContext(); + + const repoName = gitMetadata?.repoName ?? null; + + return ( + + ); +} diff --git a/app/client/src/git/components/GitQuickActions/BranchButton/BranchList.tsx b/app/client/src/git/components/QuickActions/BranchButton/BranchList.tsx similarity index 100% rename from app/client/src/git/components/GitQuickActions/BranchButton/BranchList.tsx rename to app/client/src/git/components/QuickActions/BranchButton/BranchList.tsx diff --git a/app/client/src/git/components/GitQuickActions/BranchButton/index.tsx b/app/client/src/git/components/QuickActions/BranchButton/index.tsx similarity index 100% rename from app/client/src/git/components/GitQuickActions/BranchButton/index.tsx rename to app/client/src/git/components/QuickActions/BranchButton/index.tsx diff --git a/app/client/src/git/components/GitQuickActions/ConnectButton.test.tsx b/app/client/src/git/components/QuickActions/ConnectButton.test.tsx similarity index 100% rename from app/client/src/git/components/GitQuickActions/ConnectButton.test.tsx rename to app/client/src/git/components/QuickActions/ConnectButton.test.tsx diff --git a/app/client/src/git/components/GitQuickActions/ConnectButton.tsx b/app/client/src/git/components/QuickActions/ConnectButton.tsx similarity index 100% rename from app/client/src/git/components/GitQuickActions/ConnectButton.tsx rename to app/client/src/git/components/QuickActions/ConnectButton.tsx diff --git a/app/client/src/git/components/GitQuickActions/QuickActionButton.test.tsx b/app/client/src/git/components/QuickActions/QuickActionButton.test.tsx similarity index 100% rename from app/client/src/git/components/GitQuickActions/QuickActionButton.test.tsx rename to app/client/src/git/components/QuickActions/QuickActionButton.test.tsx diff --git a/app/client/src/git/components/GitQuickActions/QuickActionButton.tsx b/app/client/src/git/components/QuickActions/QuickActionButton.tsx similarity index 100% rename from app/client/src/git/components/GitQuickActions/QuickActionButton.tsx rename to app/client/src/git/components/QuickActions/QuickActionButton.tsx diff --git a/app/client/src/git/components/GitQuickActions/index.test.tsx b/app/client/src/git/components/QuickActions/QuickActionsView.test.tsx similarity index 89% rename from app/client/src/git/components/GitQuickActions/index.test.tsx rename to app/client/src/git/components/QuickActions/QuickActionsView.test.tsx index 9beebcc7593f..a1d595b923c8 100644 --- a/app/client/src/git/components/GitQuickActions/index.test.tsx +++ b/app/client/src/git/components/QuickActions/QuickActionsView.test.tsx @@ -1,7 +1,7 @@ import React from "react"; import { render, screen, fireEvent } from "@testing-library/react"; import AnalyticsUtil from "ee/utils/AnalyticsUtil"; -import QuickActions from "."; +import QuickActionsView from "./QuickActionsView"; import { theme } from "constants/DefaultTheme"; import { ThemeProvider } from "styled-components"; import "@testing-library/jest-dom/extend-expect"; @@ -19,7 +19,7 @@ jest.mock("./../Statusbar", () => () => (
Statusbar
)); -describe("QuickActions Component", () => { +describe("QuickActionsView Component", () => { const defaultProps = { discard: jest.fn(), isAutocommitEnabled: false, @@ -35,9 +35,9 @@ describe("QuickActions Component", () => { pull: jest.fn(), statusBehindCount: 0, statusChangeCount: 0, - toggleGitConnectModal: jest.fn(), - toggleGitOpsModal: jest.fn(), - toggleGitSettingsModal: jest.fn(), + toggleConnectModal: jest.fn(), + toggleOpsModal: jest.fn(), + toggleSettingsModal: jest.fn(), }; afterEach(() => { @@ -47,7 +47,7 @@ describe("QuickActions Component", () => { it("should render ConnectButton when isGitConnected is false", () => { render( - + , ); expect(screen.getByTestId("connect-button")).toBeInTheDocument(); @@ -61,7 +61,7 @@ describe("QuickActions Component", () => { const { container } = render( - + , ); @@ -89,7 +89,7 @@ describe("QuickActions Component", () => { const { container } = render( - + , ); @@ -107,7 +107,7 @@ describe("QuickActions Component", () => { const { container } = render( - + , ); const commitButton = container.querySelectorAll( @@ -115,10 +115,7 @@ describe("QuickActions Component", () => { )[0]; fireEvent.click(commitButton); - expect(props.toggleGitOpsModal).toHaveBeenCalledWith( - true, - GitOpsTab.Deploy, - ); + expect(props.toggleOpsModal).toHaveBeenCalledWith(true, GitOpsTab.Deploy); expect(AnalyticsUtil.logEvent).toHaveBeenCalledWith( "GS_DEPLOY_GIT_MODAL_TRIGGERED", { @@ -142,7 +139,7 @@ describe("QuickActions Component", () => { const { container } = render( - + , ); const pullButton = container.querySelectorAll( @@ -163,7 +160,7 @@ describe("QuickActions Component", () => { const { container } = render( - + , ); const mergeButton = container.querySelectorAll( @@ -177,7 +174,7 @@ describe("QuickActions Component", () => { source: "BOTTOM_BAR_GIT_MERGE_BUTTON", }, ); - expect(props.toggleGitOpsModal).toHaveBeenCalledWith(true, GitOpsTab.Merge); + expect(props.toggleOpsModal).toHaveBeenCalledWith(true, GitOpsTab.Merge); }); it("should call onSettingsClick when settings button is clicked", () => { @@ -188,7 +185,7 @@ describe("QuickActions Component", () => { const { container } = render( - + , ); const settingsButton = container.querySelectorAll( @@ -199,7 +196,7 @@ describe("QuickActions Component", () => { expect(AnalyticsUtil.logEvent).toHaveBeenCalledWith("GS_SETTING_CLICK", { source: "BOTTOM_BAR_GIT_SETTING_BUTTON", }); - expect(props.toggleGitSettingsModal).toHaveBeenCalledWith( + expect(props.toggleSettingsModal).toHaveBeenCalledWith( true, GitSettingsTab.General, ); @@ -214,7 +211,7 @@ describe("QuickActions Component", () => { const { container } = render( - + , ); const commitButton = container.querySelectorAll( @@ -233,7 +230,7 @@ describe("QuickActions Component", () => { const { container } = render( - + , ); @@ -255,7 +252,7 @@ describe("QuickActions Component", () => { render( - + , ); const countElement = screen.getByTestId("t--bottom-bar-count"); @@ -273,7 +270,7 @@ describe("QuickActions Component", () => { render( - + , ); expect(screen.queryByTestId("t--bottom-bar-count")).not.toBeInTheDocument(); @@ -296,7 +293,7 @@ describe("QuickActions Component", () => { const { container } = render( - + , ); const pullButton = container.querySelectorAll( @@ -316,7 +313,7 @@ describe("QuickActions Component", () => { render( - + , ); const countElement = screen.getByTestId("t--bottom-bar-count"); diff --git a/app/client/src/git/components/GitQuickActions/index.tsx b/app/client/src/git/components/QuickActions/QuickActionsView.tsx similarity index 85% rename from app/client/src/git/components/GitQuickActions/index.tsx rename to app/client/src/git/components/QuickActions/QuickActionsView.tsx index 42d172839f11..eedb67bf3787 100644 --- a/app/client/src/git/components/GitQuickActions/index.tsx +++ b/app/client/src/git/components/QuickActions/QuickActionsView.tsx @@ -13,7 +13,7 @@ import { GitOpsTab } from "../../constants/enums"; import { GitSettingsTab } from "../../constants/enums"; import ConnectButton from "./ConnectButton"; import QuickActionButton from "./QuickActionButton"; -import Statusbar from "../Statusbar"; +import AutocommitStatusbar from "../Statusbar"; import getPullBtnStatus from "./helpers/getPullButtonStatus"; import noop from "lodash/noop"; @@ -23,7 +23,7 @@ const Container = styled.div` align-items: center; `; -interface GitQuickActionsProps { +interface QuickActionsViewProps { discard: () => void; isAutocommitEnabled: boolean; isAutocommitPolling: boolean; @@ -38,15 +38,15 @@ interface GitQuickActionsProps { pull: () => void; statusBehindCount: number; statusChangeCount: number; - toggleGitConnectModal: (open: boolean) => void; - toggleGitOpsModal: (open: boolean, tab: keyof typeof GitOpsTab) => void; - toggleGitSettingsModal: ( + toggleConnectModal: (open: boolean) => void; + toggleOpsModal: (open: boolean, tab: keyof typeof GitOpsTab) => void; + toggleSettingsModal: ( open: boolean, tab: keyof typeof GitSettingsTab, ) => void; } -function GitQuickActions({ +function QuickActionsView({ discard = noop, isAutocommitEnabled = false, isAutocommitPolling = false, @@ -61,10 +61,10 @@ function GitQuickActions({ pull = noop, statusBehindCount = 0, statusChangeCount = 0, - toggleGitConnectModal = noop, - toggleGitOpsModal = noop, - toggleGitSettingsModal = noop, -}: GitQuickActionsProps) { + toggleConnectModal = noop, + toggleOpsModal = noop, + toggleSettingsModal = noop, +}: QuickActionsViewProps) { const { isDisabled: isPullDisabled, message: pullTooltipMessage } = getPullBtnStatus({ isStatusClean, @@ -78,13 +78,13 @@ function GitQuickActions({ const onCommitBtnClick = useCallback(() => { if (!isFetchStatusLoading && !isProtectedMode) { - toggleGitOpsModal(true, GitOpsTab.Deploy); + toggleOpsModal(true, GitOpsTab.Deploy); AnalyticsUtil.logEvent("GS_DEPLOY_GIT_MODAL_TRIGGERED", { source: "BOTTOM_BAR_GIT_COMMIT_BUTTON", }); } - }, [isFetchStatusLoading, isProtectedMode, toggleGitOpsModal]); + }, [isFetchStatusLoading, isProtectedMode, toggleOpsModal]); const onPullBtnClick = useCallback(() => { if (!isPullButtonLoading && !isPullDisabled) { @@ -106,29 +106,29 @@ function GitQuickActions({ AnalyticsUtil.logEvent("GS_MERGE_GIT_MODAL_TRIGGERED", { source: "BOTTOM_BAR_GIT_MERGE_BUTTON", }); - toggleGitOpsModal(true, GitOpsTab.Merge); - }, [toggleGitOpsModal]); + toggleOpsModal(true, GitOpsTab.Merge); + }, [toggleOpsModal]); const onSettingsClick = useCallback(() => { - toggleGitSettingsModal(true, GitSettingsTab.General); + toggleSettingsModal(true, GitSettingsTab.General); AnalyticsUtil.logEvent("GS_SETTING_CLICK", { source: "BOTTOM_BAR_GIT_SETTING_BUTTON", }); - }, [toggleGitSettingsModal]); + }, [toggleSettingsModal]); const onConnectBtnClick = useCallback(() => { AnalyticsUtil.logEvent("GS_CONNECT_GIT_CLICK", { source: "BOTTOM_BAR_GIT_CONNECT_BUTTON", }); - toggleGitConnectModal(true); - }, [toggleGitConnectModal]); + toggleConnectModal(true); + }, [toggleConnectModal]); return isGitConnected ? ( {/* */} {isAutocommitEnabled && isAutocommitPolling ? ( - + ) : ( <> ); } -export default CtxAwareGitQuickActions; +export default QuickActions; diff --git a/app/client/src/git/components/StatusChanges/StatusChangesView.tsx b/app/client/src/git/components/StatusChanges/StatusChangesView.tsx new file mode 100644 index 000000000000..c3768c0d866c --- /dev/null +++ b/app/client/src/git/components/StatusChanges/StatusChangesView.tsx @@ -0,0 +1,54 @@ +import type { FetchStatusResponseData } from "git/requests/fetchStatusRequest.types"; +import React, { useMemo } from "react"; +import type { StatusTreeStruct } from "./StatusTree"; +import StatusTree from "./StatusTree"; +import { Text } from "@appsmith/ads"; +import { createMessage } from "@appsmith/ads-old"; +import { + CHANGES_SINCE_LAST_DEPLOYMENT, + FETCH_GIT_STATUS, +} from "ee/constants/messages"; +import StatusLoader from "pages/Editor/gitSync/components/StatusLoader"; + +const noopStatusTransformer = () => null; + +interface StatusChangesViewProps { + status: FetchStatusResponseData | null; + statusTransformer: ( + status: FetchStatusResponseData, + ) => StatusTreeStruct[] | null; + isFetchStatusLoading: boolean; +} + +export default function StatusChangesView({ + isFetchStatusLoading = false, + status = null, + statusTransformer = noopStatusTransformer, +}: StatusChangesViewProps) { + const statusTree = useMemo(() => { + if (!status || isFetchStatusLoading) return null; + + return statusTransformer(status); + }, [isFetchStatusLoading, status, statusTransformer]); + + if (isFetchStatusLoading) { + return ; + } + + if (!status || status.isClean || !statusTree) { + return null; + } + + return ( +
+ + {createMessage(CHANGES_SINCE_LAST_DEPLOYMENT)} + + +
+ ); +} diff --git a/app/client/src/git/components/StatusChanges/StatusLoader.tsx b/app/client/src/git/components/StatusChanges/StatusLoader.tsx new file mode 100644 index 000000000000..bc0427bd6521 --- /dev/null +++ b/app/client/src/git/components/StatusChanges/StatusLoader.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import styled from "styled-components"; +import { Spinner, Text } from "@appsmith/ads"; + +const LoaderWrapper = styled.div` + display: flex; + flex-direction: row; + align-items: center; + margin-top: ${(props) => `${props.theme.spaces[3]}px`}; +`; + +function StatusLoader({ loaderMsg }: { loaderMsg: string }) { + return ( + + + + {loaderMsg} + + + ); +} + +export default StatusLoader; diff --git a/app/client/src/git/components/StatusChanges/StatusTree.tsx b/app/client/src/git/components/StatusChanges/StatusTree.tsx new file mode 100644 index 000000000000..289d2d28b796 --- /dev/null +++ b/app/client/src/git/components/StatusChanges/StatusTree.tsx @@ -0,0 +1,88 @@ +import React from "react"; +import { + Collapsible, + CollapsibleContent, + CollapsibleHeader, + Icon, + Text, +} from "@appsmith/ads"; +import clsx from "clsx"; + +export interface StatusTreeStruct { + icon: string; + message: string; + children?: StatusTreeStruct[]; +} + +interface StatusTreeNodeProps { + icon: string; + message: string; + noEmphasis?: boolean; +} + +function StatusTreeNode({ + icon, + message, + noEmphasis = false, +}: StatusTreeNodeProps) { + return ( +
+ + + {message} + +
+ ); +} + +interface SingleStatusTreeProps { + tree: StatusTreeStruct | null; + depth?: number; +} + +function SingleStatusTree({ depth = 1, tree }: SingleStatusTreeProps) { + if (!tree) return null; + + if (!tree.children) { + return ( + 2} + /> + ); + } + + return ( + + + + + + {tree.children.map((child, index) => ( + + ))} + + + ); +} + +interface StatusTreeProps { + tree: StatusTreeStruct[] | null; +} + +function StatusTree({ tree }: StatusTreeProps) { + if (!tree) return null; + + return ( +
+ {tree.map((tree, index) => ( + + ))} +
+ ); +} + +export default StatusTree; diff --git a/app/client/src/git/components/StatusChanges/index.tsx b/app/client/src/git/components/StatusChanges/index.tsx new file mode 100644 index 000000000000..05b8e280189b --- /dev/null +++ b/app/client/src/git/components/StatusChanges/index.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import { useGitContext } from "../GitContextProvider"; +import StatusChangesView from "./StatusChangesView"; + +function StatusChanges() { + const { fetchStatusLoading, status, statusTransformer } = useGitContext(); + + return ( + + ); +} + +export default StatusChanges; diff --git a/app/client/src/git/components/connect/GitTest.tsx b/app/client/src/git/components/connect/GitTest.tsx deleted file mode 100644 index 88e24b56e3e1..000000000000 --- a/app/client/src/git/components/connect/GitTest.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from "react"; - -function GitTest() { - return
GitTest
; -} - -export default GitTest; diff --git a/app/client/src/git/components/index.tsx b/app/client/src/git/components/index.tsx new file mode 100644 index 000000000000..ec1d504d030a --- /dev/null +++ b/app/client/src/git/components/index.tsx @@ -0,0 +1,2 @@ +export { default as GitModals } from "./GitModals"; +export { default as GitQuickActions } from "./QuickActions"; diff --git a/app/client/src/git/constants/enums.ts b/app/client/src/git/constants/enums.ts index edec972ecfd7..848521ee58ad 100644 --- a/app/client/src/git/constants/enums.ts +++ b/app/client/src/git/constants/enums.ts @@ -26,7 +26,7 @@ export enum GitSettingsTab { Branch = "Branch", } -export enum AutocommitStatus { +export enum AutocommitStatusState { IN_PROGRESS = "IN_PROGRESS", LOCKED = "LOCKED", PUBLISHED = "PUBLISHED", @@ -35,6 +35,14 @@ export enum AutocommitStatus { NON_GIT_APP = "NON_GIT_APP", } +export enum MergeStatusState { + FETCHING = "FETCHING", + MERGEABLE = "MERGEABLE", + NOT_MERGEABLE = "NOT_MERGEABLE", + NONE = "NONE", + ERROR = "ERROR", +} + export enum GitErrorCodes { REPO_LIMIT_REACHED = "AE-GIT-4043", PUSH_FAILED_REMOTE_COUNTERPART_IS_AHEAD = "AE-GIT-4048", diff --git a/app/client/src/git/requests/fetchAutocommitProgressRequest.types.ts b/app/client/src/git/requests/fetchAutocommitProgressRequest.types.ts index e59d7a64d547..2c8d44a65f06 100644 --- a/app/client/src/git/requests/fetchAutocommitProgressRequest.types.ts +++ b/app/client/src/git/requests/fetchAutocommitProgressRequest.types.ts @@ -1,8 +1,8 @@ import type { ApiResponse } from "api/types"; -import type { AutocommitStatus } from "../constants/enums"; +import type { AutocommitStatusState } from "../constants/enums"; export interface FetchAutocommitProgressResponseData { - autoCommitResponse: AutocommitStatus; + autoCommitResponse: AutocommitStatusState; progress: number; branchName: string; } diff --git a/app/client/src/git/requests/fetchBranchesRequest.types.ts b/app/client/src/git/requests/fetchBranchesRequest.types.ts index 0ea1aeed64cf..f23bcc8ee757 100644 --- a/app/client/src/git/requests/fetchBranchesRequest.types.ts +++ b/app/client/src/git/requests/fetchBranchesRequest.types.ts @@ -1,4 +1,4 @@ -import type { ApiResponse } from "api/ApiResponses"; +import type { ApiResponse } from "api/types"; export interface FetchBranchesRequestParams { pruneBranches?: boolean; diff --git a/app/client/src/git/requests/fetchMergeStatusRequest.types.ts b/app/client/src/git/requests/fetchMergeStatusRequest.types.ts index b8c928af2630..8612919a4aca 100644 --- a/app/client/src/git/requests/fetchMergeStatusRequest.types.ts +++ b/app/client/src/git/requests/fetchMergeStatusRequest.types.ts @@ -9,6 +9,7 @@ export interface FetchMergeStatusResponseData { isMergeAble: boolean; status: string; // merge status message: string; + conflictingFiles?: string[]; } export type FetchMergeStatusResponse = diff --git a/app/client/src/git/requests/pullRequest.ts b/app/client/src/git/requests/pullRequest.ts index 18870918c0cd..cd533f6d7679 100644 --- a/app/client/src/git/requests/pullRequest.ts +++ b/app/client/src/git/requests/pullRequest.ts @@ -1,10 +1,10 @@ import Api from "api/Api"; import { GIT_BASE_URL } from "./constants"; import type { AxiosPromise } from "axios"; -import type { PullRequestResponse } from "./pullRequest.types"; +import type { PullResponse } from "./pullRequest.types"; export default async function pullRequest( branchedApplicationId: string, -): AxiosPromise { +): AxiosPromise { return Api.get(`${GIT_BASE_URL}/pull/app/${branchedApplicationId}`); } diff --git a/app/client/src/git/requests/pullRequest.types.ts b/app/client/src/git/requests/pullRequest.types.ts index abfb2586ca8e..57f936391cc8 100644 --- a/app/client/src/git/requests/pullRequest.types.ts +++ b/app/client/src/git/requests/pullRequest.types.ts @@ -1,6 +1,10 @@ -export interface PullRequestResponse { +import type { ApiResponse } from "api/types"; + +export interface PullResponseData { mergeStatus: { isMergeAble: boolean; status: string; // pull merge status }; } + +export type PullResponse = ApiResponse; diff --git a/app/client/src/git/requests/triggerAutocommitRequest.types.ts b/app/client/src/git/requests/triggerAutocommitRequest.types.ts index 7a3959478280..c342d28ab5b7 100644 --- a/app/client/src/git/requests/triggerAutocommitRequest.types.ts +++ b/app/client/src/git/requests/triggerAutocommitRequest.types.ts @@ -1,8 +1,8 @@ import type { ApiResponse } from "api/types"; -import type { AutocommitStatus } from "../constants/enums"; +import type { AutocommitStatusState } from "../constants/enums"; export interface TriggerAutocommitResponseData { - autoCommitResponse: AutocommitStatus; + autoCommitResponse: AutocommitStatusState; progress: number; branchName: string; } diff --git a/app/client/src/git/sagas/checkoutBranchSaga.ts b/app/client/src/git/sagas/checkoutBranchSaga.ts index 506ecae653d3..18eaea3532ab 100644 --- a/app/client/src/git/sagas/checkoutBranchSaga.ts +++ b/app/client/src/git/sagas/checkoutBranchSaga.ts @@ -19,6 +19,8 @@ import { FocusEntity, identifyEntityFromPath } from "navigation/FocusEntity"; import { validateResponse } from "sagas/ErrorSagas"; import history from "utils/history"; import type { JSCollectionDataState } from "ee/reducers/entityReducers/jsActionsReducer"; +import log from "loglevel"; +import { captureException } from "@sentry/react"; export default function* checkoutBranchSaga( action: GitArtifactPayloadAction, @@ -46,7 +48,7 @@ export default function* checkoutBranchSaga( ); yield put( - gitArtifactActions.toggleGitBranchListPopup({ + gitArtifactActions.toggleBranchListPopup({ ...basePayload, open: false, }), @@ -110,12 +112,19 @@ export default function* checkoutBranchSaga( } } } - } catch (error) { - yield put( - gitArtifactActions.checkoutBranchError({ - ...basePayload, - error: error as string, - }), - ); + } catch (e) { + if (response && response.responseMeta.error) { + const { error } = response.responseMeta; + + yield put( + gitArtifactActions.checkoutBranchError({ + ...basePayload, + error, + }), + ); + } else { + log.error(e); + captureException(e); + } } } diff --git a/app/client/src/git/sagas/commitSaga.ts b/app/client/src/git/sagas/commitSaga.ts index e609a8f5145a..159021acc145 100644 --- a/app/client/src/git/sagas/commitSaga.ts +++ b/app/client/src/git/sagas/commitSaga.ts @@ -1,3 +1,6 @@ +import { call, put } from "redux-saga/effects"; +import { captureException } from "@sentry/react"; +import log from "loglevel"; import type { CommitInitPayload } from "../store/actions/commitActions"; import { GitArtifactType, GitErrorCodes } from "../constants/enums"; import commitRequest from "../requests/commitRequest"; @@ -7,7 +10,6 @@ import type { } from "../requests/commitRequest.types"; import { gitArtifactActions } from "../store/gitArtifactSlice"; import type { GitArtifactPayloadAction } from "../store/types"; -import { call, put } from "redux-saga/effects"; // internal dependencies import { validateResponse } from "sagas/ErrorSagas"; @@ -17,6 +19,7 @@ export default function* commitSaga( ) { const { artifactType, baseArtifactId } = action.payload; const basePayload = { artifactType, baseArtifactId }; + let response: CommitResponse | undefined; try { @@ -42,23 +45,23 @@ export default function* commitSaga( // ! case for updating lastDeployedAt in application manually? } } - } catch (error) { - if ( - GitErrorCodes.REPO_LIMIT_REACHED === response?.responseMeta?.error?.code - ) { - yield put( - gitArtifactActions.toggleRepoLimitErrorModal({ - ...basePayload, - open: true, - }), - ); - } + } catch (e) { + if (response && response.responseMeta.error) { + const { error } = response.responseMeta; - yield put( - gitArtifactActions.commitError({ - ...basePayload, - error: error as string, - }), - ); + if (error.code === GitErrorCodes.REPO_LIMIT_REACHED) { + yield put( + gitArtifactActions.toggleRepoLimitErrorModal({ + ...basePayload, + open: true, + }), + ); + } + + yield put(gitArtifactActions.commitError({ ...basePayload, error })); + } else { + log.error(e); + captureException(e); + } } } diff --git a/app/client/src/git/sagas/connectSaga.ts b/app/client/src/git/sagas/connectSaga.ts index 16d4ba1cbe0e..b1bfa286e778 100644 --- a/app/client/src/git/sagas/connectSaga.ts +++ b/app/client/src/git/sagas/connectSaga.ts @@ -15,6 +15,8 @@ import { validateResponse } from "sagas/ErrorSagas"; import { fetchPageAction } from "actions/pageActions"; import history from "utils/history"; import { addBranchParam } from "constants/routes"; +import log from "loglevel"; +import { captureException } from "@sentry/react"; export default function* connectSaga( action: GitArtifactPayloadAction, @@ -52,23 +54,28 @@ export default function* connectSaga( // ! case for updating lastDeployedAt in application manually? } } - } catch (error) { - if ( - GitErrorCodes.REPO_LIMIT_REACHED === response?.responseMeta?.error?.code - ) { + } catch (e) { + if (response && response.responseMeta.error) { + const { error } = response.responseMeta; + + if (GitErrorCodes.REPO_LIMIT_REACHED === error.code) { + yield put( + gitArtifactActions.toggleRepoLimitErrorModal({ + ...basePayload, + open: true, + }), + ); + } + yield put( - gitArtifactActions.toggleRepoLimitErrorModal({ + gitArtifactActions.connectError({ ...basePayload, - open: true, + error, }), ); + } else { + log.error(e); + captureException(e); } - - yield put( - gitArtifactActions.connectError({ - ...basePayload, - error: error as string, - }), - ); } } diff --git a/app/client/src/git/sagas/createBranchSaga.ts b/app/client/src/git/sagas/createBranchSaga.ts index 37ce2de1afaf..99604bd86602 100644 --- a/app/client/src/git/sagas/createBranchSaga.ts +++ b/app/client/src/git/sagas/createBranchSaga.ts @@ -10,6 +10,8 @@ import type { GitArtifactPayloadAction } from "../store/types"; // internal dependencies import { validateResponse } from "sagas/ErrorSagas"; +import { captureException } from "@sentry/react"; +import log from "loglevel"; export default function* createBranchSaga( action: GitArtifactPayloadAction, @@ -35,14 +37,26 @@ export default function* createBranchSaga( }), ); - // ! case to switch to the new branch + yield put( + gitArtifactActions.checkoutBranchInit({ + ...basePayload, + branchName: action.payload.branchName, + }), + ); + } + } catch (e) { + if (response && response.responseMeta.error) { + const { error } = response.responseMeta; + + yield put( + gitArtifactActions.createBranchError({ + ...basePayload, + error, + }), + ); + } else { + log.error(e); + captureException(e); } - } catch (error) { - yield put( - gitArtifactActions.createBranchError({ - ...basePayload, - error: error as string, - }), - ); } } diff --git a/app/client/src/git/sagas/deleteBranchSaga.ts b/app/client/src/git/sagas/deleteBranchSaga.ts index 7b138685e6a9..4f6bdde2f7fc 100644 --- a/app/client/src/git/sagas/deleteBranchSaga.ts +++ b/app/client/src/git/sagas/deleteBranchSaga.ts @@ -10,6 +10,8 @@ import { call, put } from "redux-saga/effects"; // internal dependencies import { validateResponse } from "sagas/ErrorSagas"; +import log from "loglevel"; +import { captureException } from "@sentry/react"; export default function* deleteBranchSaga( action: GitArtifactPayloadAction, @@ -35,12 +37,19 @@ export default function* deleteBranchSaga( }), ); } - } catch (error) { - yield put( - gitArtifactActions.deleteBranchError({ - ...basePayload, - error: error as string, - }), - ); + } catch (e) { + if (response && response.responseMeta.error) { + const { error } = response.responseMeta; + + yield put( + gitArtifactActions.deleteBranchError({ + ...basePayload, + error, + }), + ); + } else { + log.error(e); + captureException(e); + } } } diff --git a/app/client/src/git/sagas/fetchBranchesSaga.ts b/app/client/src/git/sagas/fetchBranchesSaga.ts index 5909b62e0c45..465310e01a4f 100644 --- a/app/client/src/git/sagas/fetchBranchesSaga.ts +++ b/app/client/src/git/sagas/fetchBranchesSaga.ts @@ -8,6 +8,8 @@ import { gitArtifactActions } from "git/store/gitArtifactSlice"; import type { GitArtifactPayloadAction } from "../store/types"; import { call, put } from "redux-saga/effects"; import { validateResponse } from "sagas/ErrorSagas"; +import log from "loglevel"; +import { captureException } from "@sentry/react"; export default function* fetchBranchesSaga( action: GitArtifactPayloadAction, @@ -32,12 +34,19 @@ export default function* fetchBranchesSaga( }), ); } - } catch (error) { - yield put( - gitArtifactActions.fetchBranchesError({ - ...basePayload, - error: error as string, - }), - ); + } catch (e) { + if (response && response.responseMeta.error) { + const { error } = response.responseMeta; + + yield put( + gitArtifactActions.fetchBranchesError({ + ...basePayload, + error, + }), + ); + } else { + log.error(e); + captureException(e); + } } } diff --git a/app/client/src/git/sagas/fetchGitMetadataSaga.ts b/app/client/src/git/sagas/fetchGitMetadataSaga.ts index 9f694ff10f36..a91122b83682 100644 --- a/app/client/src/git/sagas/fetchGitMetadataSaga.ts +++ b/app/client/src/git/sagas/fetchGitMetadataSaga.ts @@ -1,7 +1,9 @@ +import { captureException } from "@sentry/react"; import fetchGitMetadataRequest from "git/requests/fetchGitMetadataRequest"; import type { FetchGitMetadataResponse } from "git/requests/fetchGitMetadataRequest.types"; import { gitArtifactActions } from "git/store/gitArtifactSlice"; import type { GitArtifactPayloadAction } from "git/store/types"; +import log from "loglevel"; import { call, put } from "redux-saga/effects"; import { validateResponse } from "sagas/ErrorSagas"; @@ -24,12 +26,19 @@ export default function* fetchGitMetadataSaga( }), ); } - } catch (error) { - yield put( - gitArtifactActions.fetchGitMetadataError({ - ...basePayload, - error: error as string, - }), - ); + } catch (e) { + if (response && response.responseMeta.error) { + const { error } = response.responseMeta; + + yield put( + gitArtifactActions.fetchGitMetadataError({ + ...basePayload, + error, + }), + ); + } else { + log.error(e); + captureException(e); + } } } diff --git a/app/client/src/git/sagas/fetchGlobalProfileSaga.ts b/app/client/src/git/sagas/fetchGlobalProfileSaga.ts index 0443703ad0d5..71e7ca1a9c29 100644 --- a/app/client/src/git/sagas/fetchGlobalProfileSaga.ts +++ b/app/client/src/git/sagas/fetchGlobalProfileSaga.ts @@ -5,6 +5,8 @@ import { gitConfigActions } from "../store/gitConfigSlice"; // internal dependencies import { validateResponse } from "sagas/ErrorSagas"; +import log from "loglevel"; +import { captureException } from "@sentry/react"; export default function* fetchGlobalProfileSaga() { let response: FetchGlobalProfileResponse | undefined; @@ -21,11 +23,18 @@ export default function* fetchGlobalProfileSaga() { }), ); } - } catch (error) { - yield put( - gitConfigActions.fetchGlobalProfileError({ - error: error as string, - }), - ); + } catch (e) { + if (response && response.responseMeta.error) { + const { error } = response.responseMeta; + + yield put( + gitConfigActions.fetchGlobalProfileError({ + error, + }), + ); + } else { + log.error(e); + captureException(e); + } } } diff --git a/app/client/src/git/sagas/fetchLocalProfileSaga.ts b/app/client/src/git/sagas/fetchLocalProfileSaga.ts index a284b7e21d57..c568129beab9 100644 --- a/app/client/src/git/sagas/fetchLocalProfileSaga.ts +++ b/app/client/src/git/sagas/fetchLocalProfileSaga.ts @@ -4,6 +4,8 @@ import { gitArtifactActions } from "git/store/gitArtifactSlice"; import type { GitArtifactPayloadAction } from "../store/types"; import { call, put } from "redux-saga/effects"; import { validateResponse } from "sagas/ErrorSagas"; +import log from "loglevel"; +import { captureException } from "@sentry/react"; export default function* fetchLocalProfileSaga( action: GitArtifactPayloadAction, @@ -24,12 +26,16 @@ export default function* fetchLocalProfileSaga( }), ); } - } catch (error) { - yield put( - gitArtifactActions.fetchLocalProfileError({ - ...basePayload, - error: error as string, - }), - ); + } catch (e) { + if (response && response.responseMeta.error) { + const { error } = response.responseMeta; + + yield put( + gitArtifactActions.fetchLocalProfileError({ ...basePayload, error }), + ); + } else { + log.error(e); + captureException(e); + } } } diff --git a/app/client/src/git/sagas/fetchMergeStatusSaga.ts b/app/client/src/git/sagas/fetchMergeStatusSaga.ts new file mode 100644 index 000000000000..684233de206d --- /dev/null +++ b/app/client/src/git/sagas/fetchMergeStatusSaga.ts @@ -0,0 +1,53 @@ +import { captureException } from "@sentry/react"; +import fetchMergeStatusRequest from "git/requests/fetchMergeStatusRequest"; +import type { + FetchMergeStatusRequestParams, + FetchMergeStatusResponse, +} from "git/requests/fetchMergeStatusRequest.types"; +import type { FetchMergeStatusInitPayload } from "git/store/actions/fetchMergeStatusActions"; +import { gitArtifactActions } from "git/store/gitArtifactSlice"; +import type { GitArtifactPayloadAction } from "git/store/types"; +import log from "loglevel"; +import { call, put } from "redux-saga/effects"; +import { validateResponse } from "sagas/ErrorSagas"; + +export default function* fetchMergeStatusSaga( + action: GitArtifactPayloadAction, +) { + const { artifactId, artifactType, baseArtifactId } = action.payload; + const basePayload = { artifactType, baseArtifactId }; + let response: FetchMergeStatusResponse | undefined; + + try { + const params: FetchMergeStatusRequestParams = { + destinationBranch: action.payload.destinationBranch, + sourceBranch: action.payload.sourceBranch, + }; + + response = yield call(fetchMergeStatusRequest, artifactId, params); + const isValidResponse: boolean = yield validateResponse(response); + + if (response && isValidResponse) { + yield put( + gitArtifactActions.fetchMergeStatusSuccess({ + ...basePayload, + responseData: response.data, + }), + ); + } + } catch (e) { + if (response && response.responseMeta.error) { + const { error } = response.responseMeta; + + yield put( + gitArtifactActions.fetchMergeStatusError({ + ...basePayload, + error, + }), + ); + } else { + log.error(e); + captureException(e); + } + } +} diff --git a/app/client/src/git/sagas/fetchProtectedBranchesSaga.ts b/app/client/src/git/sagas/fetchProtectedBranchesSaga.ts index c0c5d70e7314..9c81123ea26f 100644 --- a/app/client/src/git/sagas/fetchProtectedBranchesSaga.ts +++ b/app/client/src/git/sagas/fetchProtectedBranchesSaga.ts @@ -1,7 +1,9 @@ +import { captureException } from "@sentry/react"; import fetchProtectedBranchesRequest from "git/requests/fetchProtectedBranchesRequest"; import type { FetchProtectedBranchesResponse } from "git/requests/fetchProtectedBranchesRequest.types"; import { gitArtifactActions } from "git/store/gitArtifactSlice"; import type { GitArtifactPayloadAction } from "git/store/types"; +import log from "loglevel"; import { call, put } from "redux-saga/effects"; import { validateResponse } from "sagas/ErrorSagas"; @@ -25,12 +27,19 @@ export default function* fetchProtectedBranchesSaga( }), ); } - } catch (error) { - yield put( - gitArtifactActions.fetchProtectedBranchesError({ - ...basePayload, - error: error as string, - }), - ); + } catch (e) { + if (response && response.responseMeta.error) { + const { error } = response.responseMeta; + + yield put( + gitArtifactActions.fetchProtectedBranchesError({ + ...basePayload, + error, + }), + ); + } else { + log.error(e); + captureException(e); + } } } diff --git a/app/client/src/git/sagas/fetchStatusSaga.ts b/app/client/src/git/sagas/fetchStatusSaga.ts index b0bf5d97e4da..4c714a22c0da 100644 --- a/app/client/src/git/sagas/fetchStatusSaga.ts +++ b/app/client/src/git/sagas/fetchStatusSaga.ts @@ -1,8 +1,10 @@ +import { captureException } from "@sentry/react"; import fetchStatusRequest from "git/requests/fetchStatusRequest"; import type { FetchStatusResponse } from "git/requests/fetchStatusRequest.types"; import type { FetchStatusInitPayload } from "git/store/actions/fetchStatusActions"; import { gitArtifactActions } from "git/store/gitArtifactSlice"; import type { GitArtifactPayloadAction } from "git/store/types"; +import log from "loglevel"; import { call, put } from "redux-saga/effects"; import { validateResponse } from "sagas/ErrorSagas"; @@ -25,15 +27,22 @@ export default function* fetchStatusSaga( }), ); } - } catch (error) { - yield put( - gitArtifactActions.fetchStatusError({ - ...basePayload, - error: error as string, - }), - ); + } catch (e) { + if (response && response.responseMeta.error) { + const { error } = response.responseMeta; - // ! case: BETTER ERROR HANDLING + yield put( + gitArtifactActions.fetchStatusError({ + ...basePayload, + error, + }), + ); + } else { + log.error(e); + captureException(e); + } + + // ! case: better error handling than passing strings // if ((error as Error)?.message?.includes("Auth fail")) { // payload.error = new Error(createMessage(ERROR_GIT_AUTH_FAIL)); // } else if ((error as Error)?.message?.includes("Invalid remote: origin")) { diff --git a/app/client/src/git/sagas/index.ts b/app/client/src/git/sagas/index.ts index 60108c4d068b..acb2111a7520 100644 --- a/app/client/src/git/sagas/index.ts +++ b/app/client/src/git/sagas/index.ts @@ -22,6 +22,8 @@ import fetchGitMetadataSaga from "./fetchGitMetadataSaga"; import triggerAutocommitSaga from "./triggerAutocommitSaga"; import fetchStatusSaga from "./fetchStatusSaga"; import fetchProtectedBranchesSaga from "./fetchProtectedBranchesSaga"; +import pullSaga from "./pullSaga"; +import fetchMergeStatusSaga from "./fetchMergeStatusSaga"; const gitRequestBlockingActions: Record< string, @@ -37,6 +39,8 @@ const gitRequestBlockingActions: Record< // ops [gitArtifactActions.commitInit.type]: commitSaga, [gitArtifactActions.fetchStatusInit.type]: fetchStatusSaga, + [gitArtifactActions.pullInit.type]: pullSaga, + [gitArtifactActions.fetchMergeStatusInit.type]: fetchMergeStatusSaga, // branches [gitArtifactActions.fetchBranchesInit.type]: fetchBranchesSaga, diff --git a/app/client/src/git/sagas/pullSaga.ts b/app/client/src/git/sagas/pullSaga.ts new file mode 100644 index 000000000000..eafb4a5c098b --- /dev/null +++ b/app/client/src/git/sagas/pullSaga.ts @@ -0,0 +1,60 @@ +import { call, put, select } from "redux-saga/effects"; +import pullRequest from "git/requests/pullRequest"; +import type { PullResponse } from "git/requests/pullRequest.types"; +import type { PullInitPayload } from "git/store/actions/pullActions"; +import { gitArtifactActions } from "git/store/gitArtifactSlice"; +import type { GitArtifactPayloadAction } from "git/store/types"; +import { selectCurrentBranch } from "git/store/selectors/gitSingleArtifactSelectors"; + +// internal dependencies +import { validateResponse } from "sagas/ErrorSagas"; +import { getCurrentBasePageId } from "selectors/editorSelectors"; +import { initEditorAction } from "actions/initActions"; +import { APP_MODE } from "entities/App"; +import log from "loglevel"; +import { captureException } from "@sentry/react"; + +export default function* pullSaga( + action: GitArtifactPayloadAction, +) { + const { artifactId, artifactType, baseArtifactId } = action.payload; + const basePayload = { artifactType, baseArtifactId }; + let response: PullResponse | undefined; + + try { + response = yield call(pullRequest, artifactId); + const isValidResponse: boolean = yield validateResponse(response); + + if (response && isValidResponse) { + yield put(gitArtifactActions.pullSuccess(basePayload)); + + const currentBasePageId: string = yield select(getCurrentBasePageId); + const currentBranch: string = yield select( + selectCurrentBranch, + basePayload, + ); + + yield put( + initEditorAction({ + basePageId: currentBasePageId, + branch: currentBranch, + mode: APP_MODE.EDIT, + }), + ); + } + } catch (e) { + if (response && response.responseMeta.error) { + const { error } = response.responseMeta; + + // !case: handle this with error + // if (triggeredFromBottomBar) { + // yield put(setIsGitErrorPopupVisible({ isVisible: true })); + // } + + yield put(gitArtifactActions.pullError({ ...basePayload, error })); + } else { + log.error(e); + captureException(e); + } + } +} diff --git a/app/client/src/git/sagas/triggerAutocommitSaga.ts b/app/client/src/git/sagas/triggerAutocommitSaga.ts index ac50a6b03c08..c69452acf94b 100644 --- a/app/client/src/git/sagas/triggerAutocommitSaga.ts +++ b/app/client/src/git/sagas/triggerAutocommitSaga.ts @@ -1,5 +1,8 @@ import { triggerAutocommitSuccessAction } from "actions/gitSyncActions"; -import { AutocommitStatus, type GitArtifactType } from "git/constants/enums"; +import { + AutocommitStatusState, + type GitArtifactType, +} from "git/constants/enums"; import fetchAutocommitProgressRequest from "git/requests/fetchAutocommitProgressRequest"; import type { FetchAutocommitProgressResponse, @@ -25,12 +28,14 @@ import { } from "redux-saga/effects"; import type { Task } from "redux-saga"; import { validateResponse } from "sagas/ErrorSagas"; +import log from "loglevel"; +import { captureException } from "@sentry/react"; const AUTOCOMMIT_POLL_DELAY = 1000; const AUTOCOMMIT_WHITELISTED_STATES = [ - AutocommitStatus.PUBLISHED, - AutocommitStatus.IN_PROGRESS, - AutocommitStatus.LOCKED, + AutocommitStatusState.PUBLISHED, + AutocommitStatusState.IN_PROGRESS, + AutocommitStatusState.LOCKED, ]; interface PollAutocommitProgressParams { @@ -63,25 +68,30 @@ function* pollAutocommitProgressSaga(params: PollAutocommitProgressParams) { if (triggerResponse && isValidResponse) { yield put(gitArtifactActions.triggerAutocommitSuccess(basePayload)); } - } catch (error) { - yield put( - gitArtifactActions.triggerAutocommitError({ - ...basePayload, - error: error as string, - }), - ); + } catch (e) { + if (triggerResponse && triggerResponse.responseMeta.error) { + const { error } = triggerResponse.responseMeta; + + yield put( + gitArtifactActions.triggerAutocommitError({ ...basePayload, error }), + ); + } else { + log.error(e); + captureException(e); + } } + let progressResponse: FetchAutocommitProgressResponse | null = null; + try { if (isAutocommitHappening(triggerResponse?.data)) { yield put(gitArtifactActions.pollAutocommitProgressStart(basePayload)); while (true) { - yield put(gitArtifactActions.fetchAutocommitProgressInit(basePayload)); - const progressResponse: FetchAutocommitProgressResponse = yield call( - fetchAutocommitProgressRequest, - baseArtifactId, + progressResponse = yield put( + gitArtifactActions.fetchAutocommitProgressInit(basePayload), ); + yield call(fetchAutocommitProgressRequest, baseArtifactId); const isValidResponse: boolean = yield validateResponse(progressResponse); @@ -98,14 +108,22 @@ function* pollAutocommitProgressSaga(params: PollAutocommitProgressParams) { } else { yield put(gitArtifactActions.pollAutocommitProgressStop(basePayload)); } - } catch (error) { + } catch (e) { yield put(gitArtifactActions.pollAutocommitProgressStop(basePayload)); - yield put( - gitArtifactActions.fetchAutocommitProgressError({ - ...basePayload, - error: error as string, - }), - ); + + if (progressResponse && progressResponse.responseMeta.error) { + const { error } = progressResponse.responseMeta; + + yield put( + gitArtifactActions.fetchAutocommitProgressError({ + ...basePayload, + error, + }), + ); + } else { + log.error(e); + captureException(e); + } } } diff --git a/app/client/src/git/sagas/updateGlobalProfileSaga.ts b/app/client/src/git/sagas/updateGlobalProfileSaga.ts index 967d6228a0a0..2ce98fe47fb4 100644 --- a/app/client/src/git/sagas/updateGlobalProfileSaga.ts +++ b/app/client/src/git/sagas/updateGlobalProfileSaga.ts @@ -10,6 +10,8 @@ import { gitConfigActions } from "../store/gitConfigSlice"; // internal dependencies import { validateResponse } from "sagas/ErrorSagas"; +import log from "loglevel"; +import { captureException } from "@sentry/react"; export default function* updateGlobalProfileSaga( action: PayloadAction, @@ -30,9 +32,14 @@ export default function* updateGlobalProfileSaga( yield put(gitConfigActions.updateGlobalProfileSuccess()); yield put(gitConfigActions.fetchGlobalProfileInit()); } - } catch (error) { - yield put( - gitConfigActions.updateGlobalProfileError({ error: error as string }), - ); + } catch (e) { + if (response && response.responseMeta.error) { + const { error } = response.responseMeta; + + yield put(gitConfigActions.updateGlobalProfileError({ error })); + } else { + log.error(e); + captureException(e); + } } } diff --git a/app/client/src/git/sagas/updateLocalProfileSaga.ts b/app/client/src/git/sagas/updateLocalProfileSaga.ts index d2a9c0243a15..74579562b058 100644 --- a/app/client/src/git/sagas/updateLocalProfileSaga.ts +++ b/app/client/src/git/sagas/updateLocalProfileSaga.ts @@ -8,6 +8,8 @@ import { gitArtifactActions } from "../store/gitArtifactSlice"; import type { GitArtifactPayloadAction } from "../store/types"; import { call, put } from "redux-saga/effects"; import { validateResponse } from "sagas/ErrorSagas"; +import log from "loglevel"; +import { captureException } from "@sentry/react"; export default function* updateLocalProfileSaga( action: GitArtifactPayloadAction, @@ -31,12 +33,16 @@ export default function* updateLocalProfileSaga( yield put(gitArtifactActions.updateLocalProfileSuccess(basePayload)); yield put(gitArtifactActions.fetchLocalProfileInit(basePayload)); } - } catch (error) { - yield put( - gitArtifactActions.updateLocalProfileError({ - ...basePayload, - error: error as string, - }), - ); + } catch (e) { + if (response && response.responseMeta.error) { + const { error } = response.responseMeta; + + yield put( + gitArtifactActions.updateLocalProfileError({ ...basePayload, error }), + ); + } else { + log.error(e); + captureException(e); + } } } diff --git a/app/client/src/git/store/actions/commitActions.ts b/app/client/src/git/store/actions/commitActions.ts index 24c5b17267f4..9ebb8d37db60 100644 --- a/app/client/src/git/store/actions/commitActions.ts +++ b/app/client/src/git/store/actions/commitActions.ts @@ -28,3 +28,9 @@ export const commitErrorAction = return state; }); + +export const clearCommitErrorAction = createSingleArtifactAction((state) => { + state.apiResponses.commit.error = null; + + return state; +}); diff --git a/app/client/src/git/store/actions/discardActions.ts b/app/client/src/git/store/actions/discardActions.ts index c12b236f06ef..35d37c0a6d0e 100644 --- a/app/client/src/git/store/actions/discardActions.ts +++ b/app/client/src/git/store/actions/discardActions.ts @@ -24,3 +24,9 @@ export const discardErrorAction = createSingleArtifactAction( return state; }, ); + +export const clearDiscardErrorAction = createSingleArtifactAction((state) => { + state.apiResponses.discard.error = null; + + return state; +}); diff --git a/app/client/src/git/store/actions/fetchMergeStatusActions.ts b/app/client/src/git/store/actions/fetchMergeStatusActions.ts index 71ae8fccf107..65ed313114cb 100644 --- a/app/client/src/git/store/actions/fetchMergeStatusActions.ts +++ b/app/client/src/git/store/actions/fetchMergeStatusActions.ts @@ -1,15 +1,22 @@ import type { GitAsyncSuccessPayload, GitAsyncErrorPayload } from "../types"; import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction"; -import type { FetchMergeStatusResponseData } from "git/requests/fetchMergeStatusRequest.types"; +import type { + FetchMergeStatusRequestParams, + FetchMergeStatusResponseData, +} from "git/requests/fetchMergeStatusRequest.types"; -export const fetchMergeStatusInitAction = createSingleArtifactAction( - (state) => { +export interface FetchMergeStatusInitPayload + extends FetchMergeStatusRequestParams { + artifactId: string; +} + +export const fetchMergeStatusInitAction = + createSingleArtifactAction((state) => { state.apiResponses.mergeStatus.loading = true; state.apiResponses.mergeStatus.error = null; return state; - }, -); + }); export const fetchMergeStatusSuccessAction = createSingleArtifactAction< GitAsyncSuccessPayload @@ -29,3 +36,11 @@ export const fetchMergeStatusErrorAction = return state; }); + +export const clearMergeStatusAction = createSingleArtifactAction((state) => { + state.apiResponses.mergeStatus.loading = false; + state.apiResponses.mergeStatus.error = null; + state.apiResponses.mergeStatus.value = null; + + return state; +}); diff --git a/app/client/src/git/store/actions/pullActions.ts b/app/client/src/git/store/actions/pullActions.ts index 5acb7dc30f98..00a0d00f2033 100644 --- a/app/client/src/git/store/actions/pullActions.ts +++ b/app/client/src/git/store/actions/pullActions.ts @@ -1,12 +1,18 @@ import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction"; -import type { GitArtifactErrorPayloadAction } from "../types"; +import type { GitAsyncErrorPayload } from "../types"; -export const pullInitAction = createSingleArtifactAction((state) => { - state.apiResponses.pull.loading = true; - state.apiResponses.pull.error = null; +export interface PullInitPayload { + artifactId: string; +} - return state; -}); +export const pullInitAction = createSingleArtifactAction( + (state) => { + state.apiResponses.pull.loading = true; + state.apiResponses.pull.error = null; + + return state; + }, +); export const pullSuccessAction = createSingleArtifactAction((state) => { state.apiResponses.pull.loading = false; @@ -14,8 +20,8 @@ export const pullSuccessAction = createSingleArtifactAction((state) => { return state; }); -export const pullErrorAction = createSingleArtifactAction( - (state, action: GitArtifactErrorPayloadAction) => { +export const pullErrorAction = createSingleArtifactAction( + (state, action) => { const { error } = action.payload; state.apiResponses.pull.loading = false; diff --git a/app/client/src/git/store/actions/uiActions.ts b/app/client/src/git/store/actions/uiActions.ts index fe77a5684e51..c4e47fe2fe33 100644 --- a/app/client/src/git/store/actions/uiActions.ts +++ b/app/client/src/git/store/actions/uiActions.ts @@ -14,11 +14,26 @@ export const toggleRepoLimitErrorModalAction = return state; }); +interface ToggleConflictErrorModalPayload { + open: boolean; +} + +export const toggleConflictErrorModalAction = + createSingleArtifactAction( + (state, action) => { + const { open } = action.payload; + + state.ui.conflictErrorModalOpen = open; + + return state; + }, + ); + interface BranchListPopupPayload { open: boolean; } -export const toggleGitBranchListPopupAction = +export const toggleBranchListPopupAction = createSingleArtifactAction((state, action) => { const { open } = action.payload; @@ -27,28 +42,28 @@ export const toggleGitBranchListPopupAction = return state; }); -export interface ToggleGitOpsModalPayload { +export interface ToggleOpsModalPayload { open: boolean; tab: keyof typeof GitOpsTab; } -export const toggleGitOpsModalAction = - createSingleArtifactAction((state, action) => { +export const toggleOpsModalAction = + createSingleArtifactAction((state, action) => { const { open, tab } = action.payload; - state.ui.opsModal.open = open; - state.ui.opsModal.tab = tab; + state.ui.opsModalOpen = open; + state.ui.opsModalTab = tab; return state; }); -export interface ToggleGitSettingsModalPayload { +export interface ToggleSettingsModalPayload { open: boolean; tab: keyof typeof GitSettingsTab; } -export const toggleGitSettingsModalAction = - createSingleArtifactAction((state, action) => { +export const toggleSettingsModalAction = + createSingleArtifactAction((state, action) => { const { open, tab } = action.payload; state.ui.settingsModal.open = open; @@ -57,12 +72,12 @@ export const toggleGitSettingsModalAction = return state; }); -export interface ToggleGitConnectModalPayload { +export interface ToggleConnectModalPayload { open: boolean; } -export const toggleGitConnectModalAction = - createSingleArtifactAction((state, action) => { +export const toggleConnectModalAction = + createSingleArtifactAction((state, action) => { const { open } = action.payload; state.ui.connectModal.open = open; diff --git a/app/client/src/git/store/gitArtifactSlice.ts b/app/client/src/git/store/gitArtifactSlice.ts index 425b6421d403..16aa5d6d4162 100644 --- a/app/client/src/git/store/gitArtifactSlice.ts +++ b/app/client/src/git/store/gitArtifactSlice.ts @@ -23,6 +23,7 @@ import { fetchStatusSuccessAction, } from "./actions/fetchStatusActions"; import { + clearCommitErrorAction, commitErrorAction, commitInitAction, commitSuccessAction, @@ -53,11 +54,12 @@ import { deleteBranchSuccessAction, } from "./actions/deleteBranchActions"; import { - toggleGitBranchListPopupAction, - toggleGitConnectModalAction, - toggleGitOpsModalAction, - toggleGitSettingsModalAction, + toggleBranchListPopupAction, + toggleConnectModalAction, + toggleOpsModalAction, + toggleSettingsModalAction, toggleRepoLimitErrorModalAction, + toggleConflictErrorModalAction, } from "./actions/uiActions"; import { checkoutBranchErrorAction, @@ -65,11 +67,13 @@ import { checkoutBranchSuccessAction, } from "./actions/checkoutBranchActions"; import { + clearDiscardErrorAction, discardErrorAction, discardInitAction, discardSuccessAction, } from "./actions/discardActions"; import { + clearMergeStatusAction, fetchMergeStatusErrorAction, fetchMergeStatusInitAction, fetchMergeStatusSuccessAction, @@ -127,29 +131,33 @@ export const gitArtifactSlice = createSlice({ connectInit: connectInitAction, connectSuccess: connectSuccessAction, connectError: connectErrorAction, - toggleGitConnectModal: toggleGitConnectModalAction, + toggleConnectModal: toggleConnectModalAction, toggleRepoLimitErrorModal: toggleRepoLimitErrorModalAction, // git ops commitInit: commitInitAction, commitSuccess: commitSuccessAction, commitError: commitErrorAction, + clearCommitError: clearCommitErrorAction, discardInit: discardInitAction, discardSuccess: discardSuccessAction, discardError: discardErrorAction, + clearDiscardError: clearDiscardErrorAction, fetchStatusInit: fetchStatusInitAction, fetchStatusSuccess: fetchStatusSuccessAction, fetchStatusError: fetchStatusErrorAction, fetchMergeStatusInit: fetchMergeStatusInitAction, fetchMergeStatusSuccess: fetchMergeStatusSuccessAction, fetchMergeStatusError: fetchMergeStatusErrorAction, + clearMergeStatus: clearMergeStatusAction, mergeInit: mergeInitAction, mergeSuccess: mergeSuccessAction, mergeError: mergeErrorAction, pullInit: pullInitAction, pullSuccess: pullSuccessAction, pullError: pullErrorAction, - toggleGitOpsModal: toggleGitOpsModalAction, + toggleOpsModal: toggleOpsModalAction, + toggleConflictErrorModal: toggleConflictErrorModalAction, // branches fetchBranchesInit: fetchBranchesInitAction, @@ -164,10 +172,10 @@ export const gitArtifactSlice = createSlice({ checkoutBranchInit: checkoutBranchInitAction, checkoutBranchSuccess: checkoutBranchSuccessAction, checkoutBranchError: checkoutBranchErrorAction, - toggleGitBranchListPopup: toggleGitBranchListPopupAction, + toggleBranchListPopup: toggleBranchListPopupAction, // settings - toggleGitSettingsModal: toggleGitSettingsModalAction, + toggleSettingsModal: toggleSettingsModalAction, fetchLocalProfileInit: fetchLocalProfileInitAction, fetchLocalProfileSuccess: fetchLocalProfileSuccessAction, fetchLocalProfileError: fetchLocalProfileErrorAction, diff --git a/app/client/src/git/store/helpers/gitSingleArtifactInitialState.ts b/app/client/src/git/store/helpers/gitSingleArtifactInitialState.ts index 589eb44be8de..9266e852ff7a 100644 --- a/app/client/src/git/store/helpers/gitSingleArtifactInitialState.ts +++ b/app/client/src/git/store/helpers/gitSingleArtifactInitialState.ts @@ -22,10 +22,9 @@ const gitSingleArtifactInitialUIState: GitSingleArtifactUIReduxState = { branchListPopup: { open: false, }, - opsModal: { - open: false, - tab: GitOpsTab.Deploy, - }, + opsModalOpen: false, + opsModalTab: GitOpsTab.Deploy, + conflictErrorModalOpen: false, settingsModal: { open: false, tab: GitSettingsTab.General, diff --git a/app/client/src/git/store/selectors/gitSingleArtifactSelectors.ts b/app/client/src/git/store/selectors/gitSingleArtifactSelectors.ts index 6e36c250d148..4ff29711e662 100644 --- a/app/client/src/git/store/selectors/gitSingleArtifactSelectors.ts +++ b/app/client/src/git/store/selectors/gitSingleArtifactSelectors.ts @@ -53,6 +53,21 @@ export const selectMergeStatus = ( export const selectPull = (state: GitRootState, artifactDef: GitArtifactDef) => selectSingleArtifact(state, artifactDef)?.apiResponses?.pull; +export const selectOpsModalOpen = ( + state: GitRootState, + artifactDef: GitArtifactDef, +) => selectSingleArtifact(state, artifactDef)?.ui.opsModalOpen; + +export const selectOpsModalTab = ( + state: GitRootState, + artifactDef: GitArtifactDef, +) => selectSingleArtifact(state, artifactDef)?.ui.opsModalTab; + +export const selectConflictErrorModalOpen = ( + state: GitRootState, + artifactDef: GitArtifactDef, +) => selectSingleArtifact(state, artifactDef)?.ui.conflictErrorModalOpen; + // git branches export const selectCurrentBranch = ( diff --git a/app/client/src/git/store/types.ts b/app/client/src/git/store/types.ts index b3a8c18fc02a..f023e916ea2e 100644 --- a/app/client/src/git/store/types.ts +++ b/app/client/src/git/store/types.ts @@ -13,42 +13,48 @@ import type { FetchStatusResponseData } from "git/requests/fetchStatusRequest.ty import type { FetchMergeStatusResponseData } from "git/requests/fetchMergeStatusRequest.types"; import type { FetchGitMetadataResponseData } from "git/requests/fetchGitMetadataRequest.types"; import type { FetchProtectedBranchesResponseData } from "git/requests/fetchProtectedBranchesRequest.types"; +import type { ApiResponseError } from "api/types"; export type GitSSHKey = Record; -export interface AsyncState { +export interface GitApiError extends ApiResponseError { + errorType?: string; + referenceDoc?: string; + title?: string; +} +interface GitAsyncState { value: T | null; loading: boolean; - error: string | null; + error: GitApiError | null; } -interface AsyncStateWithoutValue { +interface GitAsyncStateWithoutValue { loading: boolean; - error: string | null; + error: GitApiError | null; } export interface GitSingleArtifactAPIResponsesReduxState { - metadata: AsyncState; - connect: AsyncStateWithoutValue; - status: AsyncState; - commit: AsyncStateWithoutValue; - pull: AsyncStateWithoutValue; - discard: AsyncStateWithoutValue; - mergeStatus: AsyncState; - merge: AsyncStateWithoutValue; - branches: AsyncState; - checkoutBranch: AsyncStateWithoutValue; - createBranch: AsyncStateWithoutValue; - deleteBranch: AsyncStateWithoutValue; - localProfile: AsyncState; - updateLocalProfile: AsyncStateWithoutValue; - disconnect: AsyncStateWithoutValue; - protectedBranches: AsyncState; - updateProtectedBranches: AsyncStateWithoutValue; - autocommitProgress: AsyncStateWithoutValue; - toggleAutocommit: AsyncStateWithoutValue; - triggerAutocommit: AsyncStateWithoutValue; - sshKey: AsyncState; - generateSSHKey: AsyncStateWithoutValue; + metadata: GitAsyncState; + connect: GitAsyncStateWithoutValue; + status: GitAsyncState; + commit: GitAsyncStateWithoutValue; + pull: GitAsyncStateWithoutValue; + discard: GitAsyncStateWithoutValue; + mergeStatus: GitAsyncState; + merge: GitAsyncStateWithoutValue; + branches: GitAsyncState; + checkoutBranch: GitAsyncStateWithoutValue; + createBranch: GitAsyncStateWithoutValue; + deleteBranch: GitAsyncStateWithoutValue; + localProfile: GitAsyncState; + updateLocalProfile: GitAsyncStateWithoutValue; + disconnect: GitAsyncStateWithoutValue; + protectedBranches: GitAsyncState; + updateProtectedBranches: GitAsyncStateWithoutValue; + autocommitProgress: GitAsyncStateWithoutValue; + toggleAutocommit: GitAsyncStateWithoutValue; + triggerAutocommit: GitAsyncStateWithoutValue; + sshKey: GitAsyncState; + generateSSHKey: GitAsyncStateWithoutValue; } export interface GitSingleArtifactUIReduxState { @@ -63,10 +69,9 @@ export interface GitSingleArtifactUIReduxState { branchListPopup: { open: boolean; }; - opsModal: { - open: boolean; - tab: keyof typeof GitOpsTab; - }; + opsModalOpen: boolean; + opsModalTab: keyof typeof GitOpsTab; + conflictErrorModalOpen: boolean; settingsModal: { open: boolean; tab: keyof typeof GitSettingsTab; @@ -87,8 +92,8 @@ export interface GitArtifactReduxState { } export interface GitConfigReduxState { - globalProfile: AsyncState; - updateGlobalProfile: AsyncStateWithoutValue; + globalProfile: GitAsyncState; + updateGlobalProfile: GitAsyncStateWithoutValue; } export interface GitRootState { @@ -104,7 +109,7 @@ export interface GitArtifactBasePayload { } export interface GitAsyncErrorPayload { - error: string; + error: GitApiError; } export interface GitAsyncSuccessPayload { diff --git a/app/client/src/pages/Editor/gitSync/components/DeployPreview.tsx b/app/client/src/pages/Editor/gitSync/components/DeployPreview.tsx index 9ead5d9d65c9..68c5d609429b 100644 --- a/app/client/src/pages/Editor/gitSync/components/DeployPreview.tsx +++ b/app/client/src/pages/Editor/gitSync/components/DeployPreview.tsx @@ -58,6 +58,7 @@ export default function DeployPreview(props: { showSuccess: boolean }) { : ""; return lastDeployedAt ? ( + // ! case: can use flex?
{props.showSuccess ? ( diff --git a/app/client/src/pages/Editor/gitSync/components/DiscardChangesWarning.tsx b/app/client/src/pages/Editor/gitSync/components/DiscardChangesWarning.tsx index 730a419655db..c940996def18 100644 --- a/app/client/src/pages/Editor/gitSync/components/DiscardChangesWarning.tsx +++ b/app/client/src/pages/Editor/gitSync/components/DiscardChangesWarning.tsx @@ -12,9 +12,10 @@ const Container = styled.div` `; export default function DiscardChangesWarning({ - onCloseDiscardChangesWarning, // TODO: Fix this the next time the file is edited - // eslint-disable-next-line @typescript-eslint/no-explicit-any -}: any) { + onCloseDiscardChangesWarning, +}: { + onCloseDiscardChangesWarning: () => void; +}) { const discardDocUrl = "https://docs.appsmith.com/advanced-concepts/version-control-with-git/commit-and-push"; From f08dd293002e3a352a430db8ab9ef0897cf1fa43 Mon Sep 17 00:00:00 2001 From: Anagh Hegde Date: Mon, 16 Dec 2024 15:47:59 +0530 Subject: [PATCH 8/8] chore: refactor new page crud repo methods (#38169) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description As part of transaction support in PG, we are moving from using the jpa methods for database operations. This PR is refactoring the code to use custom repository class for NewPageRepository from the default CrudRepository. ## Automation /ok-to-test tags="@tag.ImportExport" ### :mag: Cypress test results > [!IMPORTANT] > 🟣 🟣 🟣 Your tests are running. > Tests running at: > Commit: d8ff386a0c54ae00c4abc51ec6012e4c220bcba6 > Workflow: `PR Automation test suite` > Tags: `@tag.ImportExport` > Spec: `` >
Sat, 14 Dec 2024 11:17:37 UTC ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No ## Summary by CodeRabbit - **New Features** - Introduced methods to retrieve pages by application ID and count non-deleted pages. - **Bug Fixes** - Removed outdated methods from the `NewPageRepositoryCE` interface to streamline functionality. --- .../repositories/ce/CustomNewPageRepositoryCE.java | 4 ++++ .../ce/CustomNewPageRepositoryCEImpl.java | 12 ++++++++++++ .../server/repositories/ce/NewPageRepositoryCE.java | 5 ----- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomNewPageRepositoryCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomNewPageRepositoryCE.java index ca0beb98bec7..e2fa493346aa 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomNewPageRepositoryCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomNewPageRepositoryCE.java @@ -42,4 +42,8 @@ Mono findPageByBranchNameAndBasePageId( Flux findAllByApplicationIdsWithoutPermission(List applicationIds, List includeFields); Mono updateDependencyMap(String pageId, Map> dependencyMap); + + Flux findByApplicationId(String applicationId); + + Mono countByDeletedAtNull(); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomNewPageRepositoryCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomNewPageRepositoryCEImpl.java index 03b06fc065f2..0fd9514d73c9 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomNewPageRepositoryCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomNewPageRepositoryCEImpl.java @@ -261,4 +261,16 @@ public Mono updateDependencyMap(String pageId, Map update.set(NewPage.Fields.unpublishedPage_dependencyMap, dependencyMap); return queryBuilder().criteria(q).updateFirst(update); } + + @Override + public Flux findByApplicationId(String applicationId) { + final BridgeQuery q = Bridge.equal(NewPage.Fields.applicationId, applicationId); + return queryBuilder().criteria(q).all(); + } + + @Override + public Mono countByDeletedAtNull() { + final BridgeQuery q = Bridge.notExists(NewPage.Fields.deletedAt); + return queryBuilder().criteria(q).count(); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/NewPageRepositoryCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/NewPageRepositoryCE.java index 3f582daea3ac..e7182ef16af3 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/NewPageRepositoryCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/NewPageRepositoryCE.java @@ -5,15 +5,10 @@ import com.appsmith.server.repositories.BaseRepository; import com.appsmith.server.repositories.CustomNewPageRepository; import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; import java.util.List; public interface NewPageRepositoryCE extends BaseRepository, CustomNewPageRepository { - Flux findByApplicationId(String applicationId); - - Mono countByDeletedAtNull(); - Flux findIdsAndPolicyMapByApplicationIdIn(List applicationIds); }