diff --git a/app/client/src/git/components/DisconnectModal/index.test.tsx b/app/client/src/git/components/DisconnectModal/index.test.tsx new file mode 100644 index 000000000000..70f6fb14ea26 --- /dev/null +++ b/app/client/src/git/components/DisconnectModal/index.test.tsx @@ -0,0 +1,165 @@ +import React from "react"; +import { render, screen, fireEvent } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import AnalyticsUtil from "ee/utils/AnalyticsUtil"; +import DisconnectModal from "."; + +jest.mock("ee/utils/AnalyticsUtil", () => ({ + logEvent: jest.fn(), +})); + +describe("DisconnectModal", () => { + const defaultProps = { + isModalOpen: true, + disconnectingApp: { + id: "app123", + name: "TestApp", + }, + closeModal: jest.fn(), + onBackClick: jest.fn(), + onDisconnect: jest.fn(), + }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should render the modal when isModalOpen is true", () => { + render(); + expect(screen.getByTestId("t--disconnect-git-modal")).toBeInTheDocument(); + }); + + it("should not render the modal when isModalOpen is false", () => { + render(); + expect( + screen.queryByTestId("t--disconnect-git-modal"), + ).not.toBeInTheDocument(); + }); + + it("should display the correct modal header", () => { + render(); + expect(screen.getByText("Revoke access to TestApp")).toBeInTheDocument(); + }); + + it("should display the correct instruction text", () => { + render(); + expect( + screen.getByText("Type “TestApp” in the input box to revoke access."), + ).toBeInTheDocument(); + }); + + it("should update appName state when input changes", () => { + render(); + const input = screen.getByLabelText("Application name"); + + fireEvent.change(input, { target: { value: "TestApp" } }); + expect(input).toHaveValue("TestApp"); + }); + + it("should enable Revoke button when appName matches disconnectingApp.name", () => { + render(); + const input = screen.getByLabelText("Application name"); + const revokeButton = document.getElementsByClassName( + "t--git-revoke-button", + )[0]; + + expect(revokeButton).toBeDisabled(); + + fireEvent.change(input, { target: { value: "TestApp" } }); + expect(revokeButton).toBeEnabled(); + }); + + it("should disable Revoke button when appName does not match disconnectingApp.name", () => { + render(); + const input = screen.getByLabelText("Application name"); + const revokeButton = document.getElementsByClassName( + "t--git-revoke-button", + )[0]; + + fireEvent.change(input, { target: { value: "WrongAppName" } }); + expect(revokeButton).toBeDisabled(); + }); + + it("should call onBackClick when Go Back button is clicked", () => { + render(); + const goBackButton = document.getElementsByClassName( + "t--git-revoke-back-button", + )[0]; + + fireEvent.click(goBackButton); + expect(defaultProps.onBackClick).toHaveBeenCalledTimes(1); + }); + + it("should call onDisconnect when Revoke button is clicked", () => { + render(); + const input = screen.getByLabelText("Application name"); + const revokeButton = document.getElementsByClassName( + "t--git-revoke-button", + )[0]; + + fireEvent.change(input, { target: { value: "TestApp" } }); + fireEvent.click(revokeButton); + + expect(defaultProps.onDisconnect).toHaveBeenCalledTimes(1); + }); + + it("should disable Revoke button when isRevoking is true", () => { + const { rerender } = render(); + const input = screen.getByLabelText("Application name"); + const revokeButton = document.getElementsByClassName( + "t--git-revoke-button", + )[0]; + + fireEvent.change(input, { target: { value: "TestApp" } }); + expect(revokeButton).toBeEnabled(); + + fireEvent.click(revokeButton); + // Rerender to reflect state change + rerender(); + + expect(defaultProps.onDisconnect).toHaveBeenCalledTimes(1); + expect(revokeButton).toBeDisabled(); + }); + + it("should log analytics event on input blur", () => { + render(); + const input = screen.getByLabelText("Application name"); + + fireEvent.change(input, { target: { value: "SomeValue" } }); + fireEvent.blur(input); + + expect(AnalyticsUtil.logEvent).toHaveBeenCalledWith( + "GS_MATCHING_REPO_NAME_ON_GIT_DISCONNECT_MODAL", + { + value: "SomeValue", + expecting: "TestApp", + }, + ); + }); + + it("should display callout with non-reversible message and learn more link", () => { + render(); + expect( + screen.getByText( + "This action is non-reversible. Please proceed with caution.", + ), + ).toBeInTheDocument(); + const learnMoreLink = screen.getByText("Learn more").parentElement; + + expect(learnMoreLink).toBeInTheDocument(); + expect(learnMoreLink).toHaveAttribute( + "href", + "https://docs.appsmith.com/advanced-concepts/version-control-with-git/disconnect-the-git-repository", + ); + }); + + it("should not call onDisconnect when Revoke button is clicked and appName does not match", () => { + render(); + const revokeButton = document.getElementsByClassName( + "t--git-revoke-button", + )[0]; + + fireEvent.click(revokeButton); + expect(defaultProps.onDisconnect).not.toHaveBeenCalled(); + }); +}); diff --git a/app/client/src/git/components/DisconnectModal/index.tsx b/app/client/src/git/components/DisconnectModal/index.tsx new file mode 100644 index 000000000000..b18750a38422 --- /dev/null +++ b/app/client/src/git/components/DisconnectModal/index.tsx @@ -0,0 +1,146 @@ +import React, { useCallback, useState } from "react"; +import { + Button, + Callout, + Flex, + Input, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + Text, +} from "@appsmith/ads"; +import { + APPLICATION_NAME, + createMessage, + GIT_REVOKE_ACCESS, + GIT_TYPE_REPO_NAME_FOR_REVOKING_ACCESS, + GO_BACK, + NONE_REVERSIBLE_MESSAGE, + REVOKE, +} from "ee/constants/messages"; +import AnalyticsUtil from "ee/utils/AnalyticsUtil"; +import styled from "styled-components"; + +const DOCS_URL = + "https://docs.appsmith.com/advanced-concepts/version-control-with-git/disconnect-the-git-repository"; +const DOCS_LINK_PROPS = [ + { + children: "Learn more", + to: DOCS_URL, + className: "t--disconnect-learn-more", + }, +]; +const MODAL_WIDTH = 640; + +interface DisconnectModalProps { + isModalOpen: boolean; + disconnectingApp: { + id: string; + name: string; + }; + closeModal: () => void; + onBackClick: () => void; + onDisconnect: () => void; +} + +const StyledModalContent = styled(ModalContent)` + width: ${MODAL_WIDTH}px; +`; + +function DisconnectModal({ + closeModal, + disconnectingApp, + isModalOpen, + onBackClick, + onDisconnect, +}: DisconnectModalProps) { + const [appName, setAppName] = useState(""); + const [isRevoking, setIsRevoking] = useState(false); + + const onDisconnectGit = useCallback(() => { + setIsRevoking(true); + onDisconnect(); + }, [onDisconnect]); + + const shouldDisableRevokeButton = + disconnectingApp.id === "" || + appName !== disconnectingApp.name || + isRevoking; + + const onModalOpenValueChange = useCallback( + (open: boolean) => { + if (!open) { + closeModal(); + } + }, + [closeModal], + ); + + const inputOnBlur = useCallback( + (event: React.FocusEvent) => { + AnalyticsUtil.logEvent("GS_MATCHING_REPO_NAME_ON_GIT_DISCONNECT_MODAL", { + value: "value" in event.target ? event.target.value : "", + expecting: disconnectingApp.name, + }); + }, + [disconnectingApp.name], + ); + + const inputOnChange = useCallback((value: string) => { + setAppName(value); + }, []); + + return ( + + + + {createMessage(GIT_REVOKE_ACCESS, disconnectingApp.name)} + + + + + {createMessage( + GIT_TYPE_REPO_NAME_FOR_REVOKING_ACCESS, + disconnectingApp.name, + )} + + + + {createMessage(NONE_REVERSIBLE_MESSAGE)} + + + + + + + + + + ); +} + +export default DisconnectModal;