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/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/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( 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/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; } 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 ( 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/DataSourceEditor/DatasourceSection.tsx b/app/client/src/pages/Editor/DataSourceEditor/DatasourceSection.tsx index f98787ab40b0..677849f7a85f 100644 --- a/app/client/src/pages/Editor/DataSourceEditor/DatasourceSection.tsx +++ b/app/client/src/pages/Editor/DataSourceEditor/DatasourceSection.tsx @@ -21,6 +21,7 @@ import { isMultipleEnvEnabled } from "ee/utils/planHelpers"; import { selectFeatureFlags } from "ee/selectors/featureFlagsSelectors"; import { Text } from "@appsmith/ads"; import { Table } from "@appsmith/ads-old"; +import type { FeatureFlags } from "ee/entities/FeatureFlag"; const Key = styled.div` color: var(--ads-v2-color-fg-muted); @@ -69,6 +70,7 @@ interface RenderDatasourceSectionProps { showOnlyCurrentEnv?: boolean; currentEnv: string; isEnvEnabled: boolean; + featureFlags?: FeatureFlags; } const renderKVArray = ( // TODO: Fix this the next time the file is edited @@ -140,6 +142,7 @@ export function renderDatasourceSection( currentEnvironment: string, datasource: Datasource, viewMode: boolean | undefined, + featureFlags?: FeatureFlags, ) { 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/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"; 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 } } ] 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); + }); + } } 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(); + } } 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/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/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 {} 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); }