chore: git modularisation disconnect modal (#37938)
## Description Decouple and move disconnect modal Fixes https://github.com/appsmithorg/appsmith/issues/37811 ## Automation /ok-to-test tags="@tag.Git" ### 🔍 Cypress test results <!-- This is an auto-generated comment: Cypress test results --> > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: <https://github.com/appsmithorg/appsmith/actions/runs/12155733900> > Commit: f30ad803c1d26d6880fe549965d9dd364b3a1a4f > <a href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=12155733900&attempt=1" target="_blank">Cypress dashboard</a>. > Tags: `@tag.Git` > Spec: > <hr>Wed, 04 Dec 2024 09:17:09 UTC <!-- end of auto-generated comment: Cypress test results --> ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced the `DisconnectModal` component for disconnecting Git repositories from applications. - Added input validation for the application name with a "Revoke" button that is conditionally enabled. - Included a warning message about the irreversible action and a link to documentation. - **Tests** - Implemented a comprehensive test suite for the `DisconnectModal` to ensure correct functionality and user interactions. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
6d41e77b1e
commit
a9471f06d4
165
app/client/src/git/components/DisconnectModal/index.test.tsx
Normal file
165
app/client/src/git/components/DisconnectModal/index.test.tsx
Normal file
|
|
@ -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(<DisconnectModal {...defaultProps} />);
|
||||||
|
expect(screen.getByTestId("t--disconnect-git-modal")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not render the modal when isModalOpen is false", () => {
|
||||||
|
render(<DisconnectModal {...defaultProps} isModalOpen={false} />);
|
||||||
|
expect(
|
||||||
|
screen.queryByTestId("t--disconnect-git-modal"),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should display the correct modal header", () => {
|
||||||
|
render(<DisconnectModal {...defaultProps} />);
|
||||||
|
expect(screen.getByText("Revoke access to TestApp")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should display the correct instruction text", () => {
|
||||||
|
render(<DisconnectModal {...defaultProps} />);
|
||||||
|
expect(
|
||||||
|
screen.getByText("Type “TestApp” in the input box to revoke access."),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update appName state when input changes", () => {
|
||||||
|
render(<DisconnectModal {...defaultProps} />);
|
||||||
|
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(<DisconnectModal {...defaultProps} />);
|
||||||
|
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(<DisconnectModal {...defaultProps} />);
|
||||||
|
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(<DisconnectModal {...defaultProps} />);
|
||||||
|
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(<DisconnectModal {...defaultProps} />);
|
||||||
|
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(<DisconnectModal {...defaultProps} />);
|
||||||
|
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(<DisconnectModal {...defaultProps} />);
|
||||||
|
|
||||||
|
expect(defaultProps.onDisconnect).toHaveBeenCalledTimes(1);
|
||||||
|
expect(revokeButton).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should log analytics event on input blur", () => {
|
||||||
|
render(<DisconnectModal {...defaultProps} />);
|
||||||
|
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(<DisconnectModal {...defaultProps} />);
|
||||||
|
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(<DisconnectModal {...defaultProps} />);
|
||||||
|
const revokeButton = document.getElementsByClassName(
|
||||||
|
"t--git-revoke-button",
|
||||||
|
)[0];
|
||||||
|
|
||||||
|
fireEvent.click(revokeButton);
|
||||||
|
expect(defaultProps.onDisconnect).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
146
app/client/src/git/components/DisconnectModal/index.tsx
Normal file
146
app/client/src/git/components/DisconnectModal/index.tsx
Normal file
|
|
@ -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<Element, Element>) => {
|
||||||
|
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 (
|
||||||
|
<Modal onOpenChange={onModalOpenValueChange} open={isModalOpen}>
|
||||||
|
<StyledModalContent data-testid="t--disconnect-git-modal">
|
||||||
|
<ModalHeader>
|
||||||
|
{createMessage(GIT_REVOKE_ACCESS, disconnectingApp.name)}
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<Flex flexDirection="column" gap="spaces-1">
|
||||||
|
<Text color={"var(--ads-v2-color-fg-emphasis)"} kind="heading-s">
|
||||||
|
{createMessage(
|
||||||
|
GIT_TYPE_REPO_NAME_FOR_REVOKING_ACCESS,
|
||||||
|
disconnectingApp.name,
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
<Input
|
||||||
|
className="t--git-app-name-input"
|
||||||
|
label={createMessage(APPLICATION_NAME)}
|
||||||
|
onBlur={inputOnBlur}
|
||||||
|
onChange={inputOnChange}
|
||||||
|
size="md"
|
||||||
|
value={appName}
|
||||||
|
/>
|
||||||
|
<Callout kind="error" links={DOCS_LINK_PROPS}>
|
||||||
|
{createMessage(NONE_REVERSIBLE_MESSAGE)}
|
||||||
|
</Callout>
|
||||||
|
</Flex>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
className="t--git-revoke-back-button"
|
||||||
|
kind="secondary"
|
||||||
|
onClick={onBackClick}
|
||||||
|
size="md"
|
||||||
|
>
|
||||||
|
{createMessage(GO_BACK)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="t--git-revoke-button"
|
||||||
|
isDisabled={shouldDisableRevokeButton}
|
||||||
|
kind="primary"
|
||||||
|
onClick={onDisconnectGit}
|
||||||
|
size="md"
|
||||||
|
>
|
||||||
|
{createMessage(REVOKE)}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</StyledModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DisconnectModal;
|
||||||
Loading…
Reference in New Issue
Block a user