diff --git a/app/client/src/git/ce/components/ContinuousDelivery/index.tsx b/app/client/src/git/ce/components/ContinuousDelivery/index.tsx index 90e2fd68c1a1..0246dbc180ce 100644 --- a/app/client/src/git/ce/components/ContinuousDelivery/index.tsx +++ b/app/client/src/git/ce/components/ContinuousDelivery/index.tsx @@ -1,8 +1,8 @@ import React from "react"; -import CDUnLicnesed from "./CDUnLicensed"; +import CDUnLicensed from "./CDUnLicensed"; function ContinuousDelivery() { - return ; + return ; } export default ContinuousDelivery; diff --git a/app/client/src/git/ce/components/DefaultBranch/index.tsx b/app/client/src/git/ce/components/DefaultBranch/index.tsx index 1c18109e64fb..60235b9c0e0c 100644 --- a/app/client/src/git/ce/components/DefaultBranch/index.tsx +++ b/app/client/src/git/ce/components/DefaultBranch/index.tsx @@ -1,9 +1,9 @@ import React from "react"; -import { useGitContext } from "git/components/GitContextProvider"; import DefaultBranchView from "./DefaultBranchView"; +import useBranches from "git/hooks/useBranches"; export default function DefaultBranch() { - const { branches } = useGitContext(); + const { branches } = useBranches(); return ( + diff --git a/app/client/src/git/components/BranchList/BranchListHotKeys.tsx b/app/client/src/git/components/BranchList/BranchListHotKeys.tsx new file mode 100644 index 000000000000..65055ebf3265 --- /dev/null +++ b/app/client/src/git/components/BranchList/BranchListHotKeys.tsx @@ -0,0 +1,86 @@ +import React from "react"; +import { Hotkey, Hotkeys, HotkeysTarget } from "@blueprintjs/core"; + +interface Props { + handleUpKey: () => void; + handleDownKey: () => void; + handleSubmitKey: () => void; + handleEscKey: () => void; + children: React.ReactNode; +} + +@HotkeysTarget +class GlobalSearchHotKeys extends React.Component { + get hotKeysConfig() { + return [ + { + combo: "up", + onKeyDown: () => { + this.props.handleUpKey(); + }, + allowInInput: true, + group: "Branches", + label: "Move up the list", + }, + { + combo: "down", + onKeyDown: this.props.handleDownKey, + allowInInput: true, + group: "Branches", + label: "Move down the list", + }, + { + combo: "return", + onKeyDown: this.props.handleSubmitKey, + allowInInput: true, + group: "Branches", + label: "Submit", + }, + { + combo: "ESC", + onKeyDown: this.props.handleEscKey, + allowInInput: true, + group: "Branches", + label: "ESC", + }, + ]; + } + + renderHotkeys() { + return ( + + {this.hotKeysConfig.map( + ({ allowInInput, combo, group, label, onKeyDown }, index) => ( + + ), + )} + + ); + } + + render() { + return ( +
+ {this.props.children} +
+ ); + } +} + +export default GlobalSearchHotKeys; diff --git a/app/client/src/git/components/BranchList/BranchListItemContainer.tsx b/app/client/src/git/components/BranchList/BranchListItemContainer.tsx new file mode 100644 index 000000000000..510e834e7bb7 --- /dev/null +++ b/app/client/src/git/components/BranchList/BranchListItemContainer.tsx @@ -0,0 +1,37 @@ +import styled from "styled-components"; + +const BranchListItemContainer = styled.div<{ + isSelected?: boolean; + isActive?: boolean; + isDefault?: boolean; +}>` + padding: ${(props) => `${props.theme.spaces[4]}px`}; + margin: ${(props) => `${props.theme.spaces[1]}px 0`}; + color: var(--ads-v2-color-fg-emphasis); + cursor: pointer; + width: 100%; + height: 36px; + border-radius: var(--ads-v2-border-radius); + background-color: ${(props) => + props.isSelected || props.isActive ? "var(--ads-v2-color-bg-muted)" : ""}; + ${(props) => + !props.isActive && + `&:hover { + background-color: var(--ads-v2-color-bg-subtle); + }`} + + display: flex; + align-items: center; + + .branch-list-item-text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: flex; + align-items: center; + gap: 4px; + flex: 1; + } +`; + +export default BranchListItemContainer; diff --git a/app/client/src/git/components/BranchList/BranchListView.tsx b/app/client/src/git/components/BranchList/BranchListView.tsx new file mode 100644 index 000000000000..53064528888d --- /dev/null +++ b/app/client/src/git/components/BranchList/BranchListView.tsx @@ -0,0 +1,427 @@ +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { getTypographyByKey } from "@appsmith/ads-old"; +import styled, { useTheme } from "styled-components"; +import Skeleton from "components/utils/Skeleton"; +import scrollIntoView from "scroll-into-view-if-needed"; +import BranchListHotkeys from "./BranchListHotKeys"; +import { + createMessage, + FIND_OR_CREATE_A_BRANCH, + SWITCH_BRANCHES, + SYNC_BRANCHES, +} from "ee/constants/messages"; +import { + Icon, + Spinner, + Tooltip, + Button, + SearchInput, + Text, +} from "@appsmith/ads"; +import get from "lodash/get"; +import noop from "lodash/noop"; +import { + isLocalBranch, + isRemoteBranch, + removeSpecialChars, +} from "pages/Editor/gitSync/utils"; +import AnalyticsUtil from "ee/utils/AnalyticsUtil"; +import RemoteBranchList from "./RemoteBranchList"; +import type { Theme } from "constants/DefaultTheme"; +import BranchListItemContainer from "./BranchListItemContainer"; +import LocalBranchList from "./LocalBranchList"; +import { useFilteredBranches } from "./hooks/useFilteredBranches"; +import useActiveHoverIndex from "./hooks/useActiveHoverIndex"; +import { Space } from "pages/Editor/gitSync/components/StyledComponents"; +import type { FetchBranchesResponseData } from "git/requests/fetchBranchesRequest.types"; +import type { FetchProtectedBranchesResponseData } from "git/requests/fetchProtectedBranchesRequest.types"; + +const ListContainer = styled.div` + flex: 1; + overflow: auto; + width: calc(300px + 5px); + margin-right: -5px; + position: relative; +`; + +const BranchDropdownContainer = styled.div` + height: 45vh; + display: flex; + flex-direction: column; + + padding: ${(props) => props.theme.spaces[5]}px; + min-height: 0; +`; + +// used for skeletons +const textInputHeight = 38; +const textHeight = 18; + +const CreateNewBranchContainer = styled.div` + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + margin-right: 4px; + + & div { + margin-left: ${(props) => props.theme.spaces[4]}px; + display: block; + word-break: break-all; + } + + & .large-text { + ${getTypographyByKey("p1")}; + color: var(--ads-v2-color-fg); + } + + & .small-text { + ${getTypographyByKey("p3")}; + color: var(--ads-v2-color-fg-muted); + } +`; + +const SpinnerContainer = styled.div<{ isCreateBranchLoading: boolean }>` + align-self: center; + width: 12px; + visibility: ${(props) => + props.isCreateBranchLoading ? "visible" : "hidden"}; +`; + +interface CreateNewBranchProps { + branch: string; + className: string; + currentBranch: string | null; + hovered: boolean; + isCreateBranchLoading: boolean; + onClick: () => void; + shouldScrollIntoView: boolean; +} + +function CreateNewBranch({ + branch, + className, + currentBranch, + hovered, + isCreateBranchLoading, + onClick, + shouldScrollIntoView, +}: CreateNewBranchProps) { + useEffect( + function onInitEffect() { + if (itemRef.current && shouldScrollIntoView) + scrollIntoView(itemRef.current, { + scrollMode: "if-needed", + block: "nearest", + inline: "nearest", + }); + }, + [shouldScrollIntoView], + ); + const itemRef = React.useRef(null); + const theme = useTheme() as Theme; + + return ( +
+
+ + +
{`Create branch: ${branch} `}
+
{`from '${currentBranch}'`}
+
+
+ + + + +
+ ); +} + +export function LoadingRow() { + return ( + +
+ +
+
+ ); +} + +export function BranchesLoading() { + return ( + <> + + + + + ); +} + +interface BranchListHeaderProps { + onClickClose: () => void; + onClickRefresh: () => void; +} + +export function Header({ + onClickClose = noop, + onClickRefresh = noop, +}: BranchListHeaderProps) { + const title = createMessage(SWITCH_BRANCHES); + const theme = useTheme() as Theme; + + return ( +
+
+ + {title} + + + +
+
+ ); +} + +interface BranchListViewProps { + branches: FetchBranchesResponseData | null; + checkoutBranch: (branch: string) => void; + checkoutDestBranch: string | null; + createBranch: (branch: string) => void; + currentBranch: string | null; + defaultBranch: string | null; + deleteBranch: (branch: string) => void; + fetchBranches: () => void; + fetchProtectedBranches: () => void; + isCreateBranchLoading: boolean; + isCheckoutBranchLoading: boolean; + isFetchBranchesLoading: boolean; + isFetchProtectedBranchesLoading: boolean; + protectedBranches: FetchProtectedBranchesResponseData | null; + toggleBranchPopup: (isOpen: boolean) => void; +} + +export default function BranchListView({ + branches = null, + checkoutBranch = noop, + checkoutDestBranch = null, + createBranch = noop, + currentBranch = null, + defaultBranch = null, + deleteBranch = noop, + fetchBranches = noop, + fetchProtectedBranches = noop, + isCheckoutBranchLoading = false, + isCreateBranchLoading = false, + isFetchBranchesLoading = false, + isFetchProtectedBranchesLoading = false, + protectedBranches = null, + toggleBranchPopup = noop, +}: BranchListViewProps) { + const [searchText, changeSearchTextInState] = useState(""); + const changeSearchText = useCallback((text: string) => { + changeSearchTextInState(removeSpecialChars(text)); + }, []); + const branchNames = useMemo( + () => branches?.map((branch) => branch.branchName), + [branches], + ); + + const isCreateNewBranchInputValid = useMemo( + () => + !!( + searchText && + branchNames && + !branchNames.find((branch: string) => branch === searchText) + ), + [searchText, branchNames], + ); + + const filteredBranches = useFilteredBranches(branches ?? [], searchText); + + const localBranches = filteredBranches.filter((b: string) => + isLocalBranch(b), + ); + const remoteBranches = filteredBranches.filter((b: string) => + isRemoteBranch(b), + ); + const { activeHoverIndex, setActiveHoverIndex } = useActiveHoverIndex( + currentBranch ?? "", + filteredBranches, + isCreateNewBranchInputValid, + ); + + const handleClickOnRefresh = useCallback(() => { + AnalyticsUtil.logEvent("GS_SYNC_BRANCHES", { + source: "BRANCH_LIST_POPUP_FROM_BOTTOM_BAR", + }); + fetchBranches(); + fetchProtectedBranches(); + }, [fetchBranches, fetchProtectedBranches]); + + const handleCreateNewBranch = useCallback(() => { + if (isCreateBranchLoading) return; + + AnalyticsUtil.logEvent("GS_CREATE_NEW_BRANCH", { + source: "BRANCH_LIST_POPUP_FROM_BOTTOM_BAR", + }); + const branch = searchText; + + createBranch(branch); + }, [createBranch, isCreateBranchLoading, searchText]); + + const handleUpKey = useCallback( + () => setActiveHoverIndex(activeHoverIndex - 1), + [activeHoverIndex, setActiveHoverIndex], + ); + + const handleDownKey = useCallback( + () => setActiveHoverIndex(activeHoverIndex + 1), + [activeHoverIndex, setActiveHoverIndex], + ); + + const handleSubmitKey = useCallback(() => { + if (isCreateNewBranchInputValid) { + handleCreateNewBranch(); + } else { + checkoutBranch(filteredBranches[activeHoverIndex]); + AnalyticsUtil.logEvent("GS_SWITCH_BRANCH", { + source: "BRANCH_LIST_POPUP_FROM_BOTTOM_BAR", + }); + } + }, [ + activeHoverIndex, + filteredBranches, + handleCreateNewBranch, + isCreateNewBranchInputValid, + checkoutBranch, + ]); + + const handleEscKey = useCallback(() => { + toggleBranchPopup(false); + }, [toggleBranchPopup]); + + const handleClickOnClose = useCallback(() => { + toggleBranchPopup(false); + }, [toggleBranchPopup]); + + const isLoading = isFetchBranchesLoading || isFetchProtectedBranchesLoading; + + return ( + + +
+ +
+ {isLoading && ( +
+ +
+ )} + {!isLoading && ( + + )} +
+ + + {isLoading && } + {!isLoading && ( + + + {isCreateNewBranchInputValid && ( + + )} + + + + + )} + + + ); +} diff --git a/app/client/src/git/components/BranchList/BranchMoreMenu.tsx b/app/client/src/git/components/BranchList/BranchMoreMenu.tsx new file mode 100644 index 000000000000..d035549526a4 --- /dev/null +++ b/app/client/src/git/components/BranchList/BranchMoreMenu.tsx @@ -0,0 +1,132 @@ +import React, { useCallback } from "react"; +import AnalyticsUtil from "ee/utils/AnalyticsUtil"; +import { + createMessage, + DELETE, + DELETE_BRANCH_WARNING_CHECKED_OUT, + DELETE_BRANCH_WARNING_DEFAULT, +} from "ee/constants/messages"; +import { + Button, + Menu, + MenuContent, + MenuItem, + MenuTrigger, + toast, +} from "@appsmith/ads"; +import noop from "lodash/noop"; + +interface DeleteButtonProps { + branch: string | null; + currentBranch: string | null; + defaultBranch: string | null; + deleteBranch: (branch: string) => void; +} + +function DeleteButton({ + branch = null, + currentBranch = null, + defaultBranch = null, + deleteBranch = noop, +}: DeleteButtonProps) { + const saneDelete = useCallback(() => { + if (branch) { + if (defaultBranch === branch) { + toast.show(createMessage(DELETE_BRANCH_WARNING_DEFAULT, branch), { + kind: "error", + }); + } else if (currentBranch === branch) { + toast.show(createMessage(DELETE_BRANCH_WARNING_CHECKED_OUT, branch), { + kind: "error", + }); + } else { + deleteBranch(branch); + } + } + }, [branch, currentBranch, defaultBranch, deleteBranch]); + + const handleClick = useCallback( + (e) => { + e.stopPropagation(); + saneDelete(); + }, + [saneDelete], + ); + + return ( + + {createMessage(DELETE)} + + ); +} + +interface BranchMoreMenuProps { + branch: string | null; + currentBranch: string | null; + defaultBranch: string | null; + deleteBranch: (branch: string) => void; + open: boolean; + setOpen: (open: boolean) => void; +} + +export default function BranchMoreMenu({ + branch = null, + currentBranch = null, + defaultBranch = null, + deleteBranch = noop, + open, + setOpen, +}: BranchMoreMenuProps) { + const buttons = [ + , + ]; + + const handleMenuClose = useCallback(() => { + setOpen(false); + }, [setOpen]); + + const handleClickOnMenu = useCallback( + (e) => { + e.stopPropagation(); + setOpen(true); + AnalyticsUtil.logEvent("GS_BRANCH_MORE_MENU_OPEN", { + source: "GS_OPEN_BRANCH_LIST_POPUP", + }); + }, + [setOpen], + ); + + return ( + + + + ); +} diff --git a/app/client/src/git/components/BranchList/LocalBranchList.tsx b/app/client/src/git/components/BranchList/LocalBranchList.tsx new file mode 100644 index 000000000000..7fa293b236e7 --- /dev/null +++ b/app/client/src/git/components/BranchList/LocalBranchList.tsx @@ -0,0 +1,75 @@ +import LocalBranchListItem from "./LocalBranchListItem"; +import React from "react"; +import { createMessage, LOCAL_BRANCHES } from "ee/constants/messages"; +import { Text } from "@appsmith/ads"; +import type { FetchProtectedBranchesResponseData } from "git/requests/fetchProtectedBranchesRequest.types"; +import noop from "lodash/noop"; +import styled from "styled-components"; + +const Heading = styled(Text)` + font-weight: 600; +`; + +interface LocalBranchListProps { + activeHoverIndex: number; + checkoutBranch: (branch: string) => void; + checkoutDestBranch: string | null; + currentBranch: string | null; + defaultBranch: string | null; + deleteBranch: (branch: string) => void; + isCreateNewBranchInputValid: boolean; + isCheckoutBranchLoading: boolean; + localBranches: string[]; + protectedBranches: FetchProtectedBranchesResponseData | null; +} + +export default function LocalBranchList({ + activeHoverIndex = 0, + checkoutBranch = noop, + checkoutDestBranch = null, + currentBranch = null, + defaultBranch = null, + deleteBranch = noop, + isCheckoutBranchLoading = false, + isCreateNewBranchInputValid = false, + localBranches = [], + protectedBranches = null, +}: LocalBranchListProps) { + return ( +
+ {localBranches?.length > 0 && ( + + {createMessage(LOCAL_BRANCHES)} + + )} + {localBranches.map((branch: string, index: number) => { + const isActive = + (isCreateNewBranchInputValid + ? activeHoverIndex - 1 + : activeHoverIndex) === index; + + return ( + + ); + })} +
+ ); +} diff --git a/app/client/src/git/components/BranchList/LocalBranchListItem.tsx b/app/client/src/git/components/BranchList/LocalBranchListItem.tsx new file mode 100644 index 000000000000..c1bcd0937937 --- /dev/null +++ b/app/client/src/git/components/BranchList/LocalBranchListItem.tsx @@ -0,0 +1,135 @@ +import React, { useCallback, useEffect } from "react"; +import scrollIntoView from "scroll-into-view-if-needed"; +import BranchListItemContainer from "./BranchListItemContainer"; +import useHover from "./hooks/useHover"; +import BranchMoreMenu from "./BranchMoreMenu"; +import { Tooltip, Text, Spinner, Tag, Icon } from "@appsmith/ads"; +import { isEllipsisActive } from "utils/helpers"; +import styled from "styled-components"; +import noop from "lodash/noop"; +import AnalyticsUtil from "ee/utils/AnalyticsUtil"; + +const StyledIcon = styled(Icon)` + margin-right: 8px; + width: 14px; + height: 14px; + margin-top: 1px; +`; + +const OptionsContainer = styled.div` + display: flex; + align-items: center; + justify-content: flex-end; + height: 100%; +`; + +const BranchText = styled(Text)` + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`; + +interface LocalBranchListItemProps { + branch: string; + checkoutBranch: (branch: string) => void; + checkoutDestBranch: string | null; + className?: string; + currentBranch: string | null; + defaultBranch: string | null; + deleteBranch: (branch: string) => void; + isActive: boolean; + isCheckoutBranchLoading: boolean; + isDefault: boolean; + isProtected: boolean; + isSelected: boolean; + shouldScrollIntoView: boolean; +} + +export default function LocalBranchListItem({ + branch, + checkoutBranch = noop, + checkoutDestBranch = null, + className, + currentBranch = null, + defaultBranch = null, + deleteBranch = noop, + isActive = false, + isCheckoutBranchLoading = false, + isDefault = false, + isProtected = false, + isSelected = false, + shouldScrollIntoView = false, +}: LocalBranchListItemProps) { + const itemRef = React.useRef(null); + const [hover] = useHover(itemRef); + const textRef = React.useRef(null); + const [isMoreMenuOpen, setIsMoreMenuOpen] = React.useState(false); + + useEffect( + function scrollIntoViewOnInitEffect() { + if (itemRef.current && shouldScrollIntoView) { + scrollIntoView(itemRef.current, { + scrollMode: "if-needed", + block: "nearest", + inline: "nearest", + }); + } + }, + [shouldScrollIntoView], + ); + + const handleClickOnBranch = useCallback(() => { + checkoutBranch(branch); + AnalyticsUtil.logEvent("GS_SWITCH_BRANCH", { + source: "BRANCH_LIST_POPUP_FROM_BOTTOM_BAR", + }); + }, [branch, checkoutBranch]); + + return ( + + {isProtected && } + + + + {branch} + + {isDefault && ( + + Default + + )} + + + + {checkoutDestBranch === branch && isCheckoutBranchLoading && ( + + )} + {(hover || isMoreMenuOpen) && ( + + )} + + + ); +} diff --git a/app/client/src/git/components/BranchList/RemoteBranchList.tsx b/app/client/src/git/components/BranchList/RemoteBranchList.tsx new file mode 100644 index 000000000000..1d8b7ab865bd --- /dev/null +++ b/app/client/src/git/components/BranchList/RemoteBranchList.tsx @@ -0,0 +1,47 @@ +import RemoteBranchListItem from "./RemoteBranchListItem"; +import React from "react"; +import { createMessage, REMOTE_BRANCHES } from "ee/constants/messages"; +import { Text } from "@appsmith/ads"; +import styled from "styled-components"; +import noop from "lodash/noop"; + +const Heading = styled(Text)` + font-weight: 600; +`; + +interface RemoteBranchListProps { + remoteBranches: string[]; + checkoutBranch: (branch: string) => void; + checkoutDestBranch: string | null; + isCheckoutBranchLoading: boolean; +} + +export default function RemoteBranchList({ + checkoutBranch = noop, + checkoutDestBranch = null, + isCheckoutBranchLoading = false, + remoteBranches = [], +}: RemoteBranchListProps) { + return ( +
+ {remoteBranches?.length > 0 && ( + + {createMessage(REMOTE_BRANCHES)} + + )} + {remoteBranches.map((branch: string) => ( + + ))} +
+ ); +} diff --git a/app/client/src/git/components/BranchList/RemoteBranchListItem.tsx b/app/client/src/git/components/BranchList/RemoteBranchListItem.tsx new file mode 100644 index 000000000000..10546a7b97a1 --- /dev/null +++ b/app/client/src/git/components/BranchList/RemoteBranchListItem.tsx @@ -0,0 +1,74 @@ +import React, { useCallback } from "react"; +import { Spinner, Tooltip } from "@appsmith/ads"; +import { isEllipsisActive } from "utils/helpers"; +import { Text, TextType } from "@appsmith/ads-old"; +import BranchListItemContainer from "./BranchListItemContainer"; +import styled from "styled-components"; +import AnalyticsUtil from "ee/utils/AnalyticsUtil"; +import noop from "lodash/noop"; + +const OptionsContainer = styled.div` + display: flex; + align-items: center; + justify-content: flex-end; + height: 100%; +`; + +const BranchText = styled(Text)` + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`; + +interface RemoteBranchListItemProps { + branch: string; + checkoutBranch: (branch: string) => void; + checkoutDestBranch: string | null; + className?: string; + isCheckoutBranchLoading: boolean; +} + +export default function RemoteBranchListItem({ + branch, + checkoutBranch = noop, + checkoutDestBranch = null, + className, + isCheckoutBranchLoading = false, +}: RemoteBranchListItemProps) { + const textRef = React.useRef(null); + + const handleClickOnBranch = useCallback(() => { + checkoutBranch(branch); + AnalyticsUtil.logEvent("GS_SWITCH_BRANCH", { + source: "BRANCH_LIST_POPUP_FROM_BOTTOM_BAR", + }); + }, [branch, checkoutBranch]); + + return ( + + + + {branch} + + + + {checkoutDestBranch === branch && isCheckoutBranchLoading && ( + + )} + + + ); +} diff --git a/app/client/src/git/components/BranchList/hooks/useActiveHoverIndex.ts b/app/client/src/git/components/BranchList/hooks/useActiveHoverIndex.ts new file mode 100644 index 000000000000..96244af0dabb --- /dev/null +++ b/app/client/src/git/components/BranchList/hooks/useActiveHoverIndex.ts @@ -0,0 +1,43 @@ +import { useCallback, useEffect, useState } from "react"; + +export default function useActiveHoverIndex( + currentBranch: string | undefined, + filteredBranches: Array, + isCreateNewBranchInputValid: boolean, +) { + const effectiveLength = isCreateNewBranchInputValid + ? filteredBranches.length + : filteredBranches.length - 1; + + const [activeHoverIndex, setActiveHoverIndexInState] = useState(0); + const setActiveHoverIndex = useCallback( + (index: number) => { + if (index < 0) setActiveHoverIndexInState(effectiveLength); + else if (index > effectiveLength) setActiveHoverIndexInState(0); + else setActiveHoverIndexInState(index); + }, + [effectiveLength], + ); + + useEffect( + function activeHoverIndexEffect() { + const activeBranchIdx = filteredBranches.indexOf(currentBranch || ""); + + if (activeBranchIdx !== -1) { + setActiveHoverIndex( + isCreateNewBranchInputValid ? activeBranchIdx + 1 : activeBranchIdx, + ); + } else { + setActiveHoverIndex(0); + } + }, + [ + currentBranch, + filteredBranches, + isCreateNewBranchInputValid, + setActiveHoverIndex, + ], + ); + + return { activeHoverIndex, setActiveHoverIndex }; +} diff --git a/app/client/src/git/components/BranchList/hooks/useFilteredBranches.ts b/app/client/src/git/components/BranchList/hooks/useFilteredBranches.ts new file mode 100644 index 000000000000..0674ad81bdef --- /dev/null +++ b/app/client/src/git/components/BranchList/hooks/useFilteredBranches.ts @@ -0,0 +1,29 @@ +import type { Branch } from "entities/GitSync"; +import { useEffect, useState } from "react"; + +export function useFilteredBranches( + branches: Array, + searchText: string, +) { + const lowercaseSearchText = searchText.toLowerCase(); + const [filteredBranches, setFilteredBranches] = useState>([]); + + useEffect( + function setFilteredBranchesEffect() { + const matched = branches.filter((b: Branch) => + lowercaseSearchText + ? b.branchName.toLowerCase().includes(lowercaseSearchText) + : true, + ); + const branchNames = [ + ...matched.filter((b: Branch) => b.default), + ...matched.filter((b: Branch) => !b.default), + ].map((b: Branch) => b.branchName); + + setFilteredBranches(branchNames); + }, + [branches, lowercaseSearchText], + ); + + return filteredBranches; +} diff --git a/app/client/src/git/components/BranchList/hooks/useHover.ts b/app/client/src/git/components/BranchList/hooks/useHover.ts new file mode 100644 index 000000000000..bcd6475f152c --- /dev/null +++ b/app/client/src/git/components/BranchList/hooks/useHover.ts @@ -0,0 +1,24 @@ +import { useEffect, useState } from "react"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export default function useHover(ref: any) { + const [hover, setHover] = useState(false); + const onMouseEnter = () => setHover(true); + const onMouseLeave = () => setHover(false); + + useEffect(function onInitEffect() { + const target = ref.current; + + if (target) { + target.addEventListener("mouseenter", onMouseEnter); + target.addEventListener("mouseleave", onMouseLeave); + + return () => { + target.removeEventListener("mouseenter", onMouseEnter); + target.removeEventListener("mouseleave", onMouseLeave); + }; + } + }); + + return [hover]; +} diff --git a/app/client/src/git/components/BranchList/index.tsx b/app/client/src/git/components/BranchList/index.tsx new file mode 100644 index 000000000000..ee81302ee097 --- /dev/null +++ b/app/client/src/git/components/BranchList/index.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import BranchListView from "./BranchListView"; +import useBranches from "git/hooks/useBranches"; +import useDefaultBranch from "git/ee/hooks/useDefaultBranch"; +import useProtectedBranches from "git/hooks/useProtectedBranches"; + +function BranchList() { + const { + branches, + checkoutBranch, + checkoutDestBranch, + createBranch, + currentBranch, + deleteBranch, + fetchBranches, + isCheckoutBranchLoading, + isCreateBranchLoading, + isFetchBranchesLoading, + toggleBranchPopup, + } = useBranches(); + const { defaultBranch } = useDefaultBranch(); + const { + fetchProtectedBranches, + isFetchProtectedBranchesLoading, + protectedBranches, + } = useProtectedBranches(); + + return ( + + ); +} + +export default BranchList; diff --git a/app/client/src/git/components/ConflictErrorModal/index.tsx b/app/client/src/git/components/ConflictErrorModal/index.tsx index 0de96380c6d1..db9568836032 100644 --- a/app/client/src/git/components/ConflictErrorModal/index.tsx +++ b/app/client/src/git/components/ConflictErrorModal/index.tsx @@ -1,9 +1,9 @@ import React from "react"; import ConflictErrorModalView from "./ConflictErrorModalView"; -import { useGitContext } from "../GitContextProvider"; +import useOps from "git/hooks/useOps"; export default function ConflictErrorModal() { - const { conflictErrorModalOpen, toggleConflictErrorModal } = useGitContext(); + const { conflictErrorModalOpen, toggleConflictErrorModal } = useOps(); return ( { @@ -44,25 +45,27 @@ describe("AddDeployKey Component", () => { ).toBeInTheDocument(); expect(screen.getByRole("combobox")).toBeInTheDocument(); // Should show ECDSA by default since sshKeyPair includes "ecdsa" - expect(screen.getByText(defaultProps.sshKeyPair)).toBeInTheDocument(); + expect( + screen.getByText(defaultProps.sshPublicKey as string), + ).toBeInTheDocument(); expect( screen.getByText("I've added the deploy key and gave it write access"), ).toBeInTheDocument(); }); - it("calls fetchSSHKeyPair if modal is open and not importing", () => { + it("calls fetchSSHKey if modal is open and not importing", () => { render(); - expect(defaultProps.fetchSSHKeyPair).toHaveBeenCalledTimes(1); + expect(defaultProps.fetchSSHKey).toHaveBeenCalledTimes(1); }); - it("does not call fetchSSHKeyPair if importing", () => { + it("does not call fetchSSHKey if importing", () => { render(); - expect(defaultProps.fetchSSHKeyPair).not.toHaveBeenCalled(); + expect(defaultProps.fetchSSHKey).not.toHaveBeenCalled(); }); it("shows dummy key loader if loading keys", () => { render( - , + , ); // The actual key text should not be displayed expect(screen.queryByText("ecdsa-sha2-nistp256")).not.toBeInTheDocument(); @@ -75,7 +78,7 @@ describe("AddDeployKey Component", () => { , ); @@ -85,46 +88,32 @@ describe("AddDeployKey Component", () => { fireEvent.click(rsaOption); await waitFor(() => { - expect(generateSSHKey).toHaveBeenCalledWith("RSA", expect.any(Object)); + expect(generateSSHKey).toHaveBeenCalledWith("RSA", false); }); }); it("displays a generic error when errorData is provided and error code is not AE-GIT-4032 or AE-GIT-4033", () => { // eslint-disable-next-line react-perf/jsx-no-new-object-as-prop - const errorData = { - data: {}, - responseMeta: { - success: false, - status: 503, - error: { - code: "GENERIC-ERROR", - errorType: "Some Error", - message: "Something went wrong", - }, - }, + const connectError = { + code: "GENERIC-ERROR", + errorType: "Some Error", + message: "Something went wrong", }; - render(); + render(); expect(screen.getByText("Some Error")).toBeInTheDocument(); expect(screen.getByText("Something went wrong")).toBeInTheDocument(); }); it("displays a misconfiguration error if error code is AE-GIT-4032", () => { // eslint-disable-next-line react-perf/jsx-no-new-object-as-prop - const errorData = { - data: {}, - responseMeta: { - success: false, - status: 503, - error: { - code: "AE-GIT-4032", - errorType: "SSH Key Error", - message: "SSH Key misconfiguration", - }, - }, + const connectError = { + code: "AE-GIT-4032", + errorType: "SSH Key Error", + message: "SSH Key misconfiguration", }; - render(); + render(); expect(screen.getByText("SSH key misconfiguration")).toBeInTheDocument(); expect( screen.getByText( @@ -154,7 +143,7 @@ describe("AddDeployKey Component", () => { }); it("hides copy button when connectLoading is true", () => { - render(); + render(); expect(screen.queryByTestId("t--copy-generic")).not.toBeInTheDocument(); }); @@ -172,6 +161,7 @@ describe("AddDeployKey Component", () => { render( , ); @@ -217,54 +207,25 @@ describe("AddDeployKey Component", () => { expect(docsLink).toHaveAttribute("href", DEFAULT_DOCS_URL); }); - it("uses custom documentation link if provided", () => { - render( - , - ); - const docsLink = screen.getByRole("link", { name: "Read Docs" }); - - expect(docsLink).toHaveAttribute("href", "https://custom-docs.com"); - }); - - it("does not generate SSH key if modal is closed", () => { - const generateSSHKey = jest.fn(); - - render( - , - ); - // Should not call generateSSHKey since modal is not open - expect(generateSSHKey).not.toHaveBeenCalled(); - }); - it("generates SSH key if none is present and conditions are met", async () => { - const fetchSSHKeyPair = jest.fn((props) => { - props.onSuccessCallback && props.onSuccessCallback(); - }); + const fetchSSHKey = jest.fn(); const generateSSHKey = jest.fn(); render( , ); - expect(fetchSSHKeyPair).toHaveBeenCalledTimes(1); + expect(fetchSSHKey).toHaveBeenCalledTimes(1); await waitFor(() => { - expect(generateSSHKey).toHaveBeenCalledWith("ECDSA", expect.any(Object)); + expect(generateSSHKey).toHaveBeenCalledWith("ECDSA", false); }); }); }); diff --git a/app/client/src/git/components/ConnectModal/AddDeployKey.tsx b/app/client/src/git/components/ConnectModal/ConnectInitialize/AddDeployKey.tsx similarity index 72% rename from app/client/src/git/components/ConnectModal/AddDeployKey.tsx rename to app/client/src/git/components/ConnectModal/ConnectInitialize/AddDeployKey.tsx index fdce026b034b..75a4a7a50e86 100644 --- a/app/client/src/git/components/ConnectModal/AddDeployKey.tsx +++ b/app/client/src/git/components/ConnectModal/ConnectInitialize/AddDeployKey.tsx @@ -19,7 +19,6 @@ import { Option, Select, Text, - toast, } from "@appsmith/ads"; import styled from "styled-components"; import AnalyticsUtil from "ee/utils/AnalyticsUtil"; @@ -36,8 +35,9 @@ import { import type { GitProvider } from "./ChooseGitProvider"; import { GIT_DEMO_GIF } from "./constants"; import noop from "lodash/noop"; -import type { ApiResponse } from "api/ApiResponses"; import CopyButton from "./CopyButton"; +import type { GitApiError } from "git/store/types"; +import type { ConnectFormDataState } from "./types"; export const DeployedKeyContainer = styled.div` height: 36px; @@ -131,66 +131,52 @@ const getRepositorySettingsUrl = ( } }; -const DEFAULT_DOCS_URL = +const DEPLOY_DOCS_URL = "https://docs.appsmith.com/advanced-concepts/version-control-with-git/connecting-to-git-repository"; -interface AddDeployKeyState { - gitProvider?: GitProvider; - isAddedDeployKey: boolean; - remoteUrl: string; -} - -interface Callback { - onSuccessCallback?: () => void; - onErrorCallback?: () => void; -} - -export interface FetchSSHKeyPairProps extends Callback {} - export interface AddDeployKeyProps { - isModalOpen: boolean; - onChange: (args: Partial) => void; - value: Partial; + connectError: GitApiError | null; + fetchSSHKey: () => void; + generateSSHKey: (keyType: string, isImport?: boolean) => void; + isFetchSSHKeyLoading: boolean; + isGenerateSSHKeyLoading: boolean; isImport?: boolean; - errorData?: ApiResponse; - connectLoading?: boolean; - deployKeyDocUrl?: string; - isFetchingSSHKeyPair: boolean; - fetchSSHKeyPair: (props: FetchSSHKeyPairProps) => void; - generateSSHKey: (keyType: string, callback: Callback) => void; - isGeneratingSSHKey: boolean; - sshKeyPair: string; + isLoading: boolean; + onChange: (args: Partial) => void; + sshPublicKey: string | null; + value: Partial | null; } function AddDeployKey({ - connectLoading = false, - deployKeyDocUrl, - errorData, - fetchSSHKeyPair, - generateSSHKey, - isFetchingSSHKeyPair, - isGeneratingSSHKey, + connectError = null, + fetchSSHKey = noop, + generateSSHKey = noop, + isFetchSSHKeyLoading = false, + isGenerateSSHKeyLoading = false, isImport = false, - isModalOpen, + isLoading = false, onChange = noop, - sshKeyPair, - value = {}, + sshPublicKey = null, + value = null, }: AddDeployKeyProps) { const [fetched, setFetched] = useState(false); const [sshKeyType, setSshKeyType] = useState(); useEffect( - function fetchKeyPair() { - if (isModalOpen && !isImport) { + function fetchKeyPairOnInitEffect() { + if (!isImport) { if (!fetched) { - fetchSSHKeyPair({ - onSuccessCallback: () => { - setFetched(true); - }, - onErrorCallback: () => { - setFetched(true); - }, - }); + fetchSSHKey(); + setFetched(true); + // doesn't support callback anymore + // fetchSSHKey({ + // onSuccessCallback: () => { + // setFetched(true); + // }, + // onErrorCallback: () => { + // setFetched(true); + // }, + // }); } } else { if (!fetched) { @@ -198,16 +184,16 @@ function AddDeployKey({ } } }, - [isImport, isModalOpen, fetched, fetchSSHKeyPair], + [isImport, fetched, fetchSSHKey], ); useEffect( - function setKeyType() { - if (isModalOpen && fetched && !isFetchingSSHKeyPair) { - if (sshKeyPair && sshKeyPair.includes("rsa")) { + function setSSHKeyTypeonInitEffect() { + if (fetched && !isFetchSSHKeyLoading) { + if (sshPublicKey && sshPublicKey.includes("rsa")) { setSshKeyType("RSA"); } else if ( - !sshKeyPair && + !sshPublicKey && value?.remoteUrl && value.remoteUrl.toString().toLocaleLowerCase().includes("azure") ) { @@ -217,24 +203,25 @@ function AddDeployKey({ } } }, - [isModalOpen, fetched, sshKeyPair, isFetchingSSHKeyPair, value.remoteUrl], + [fetched, sshPublicKey, isFetchSSHKeyLoading, value?.remoteUrl], ); useEffect( - function generateSSH() { + function generateSSHOnInitEffect() { if ( - isModalOpen && - ((sshKeyType && !sshKeyPair) || - (sshKeyType && !sshKeyPair?.includes(sshKeyType.toLowerCase()))) + (sshKeyType && !sshPublicKey) || + (sshKeyType && !sshPublicKey?.includes(sshKeyType.toLowerCase())) ) { - generateSSHKey(sshKeyType, { - onSuccessCallback: () => { - toast.show("SSH Key generated successfully", { kind: "success" }); - }, - }); + generateSSHKey(sshKeyType, isImport); + // doesn't support callback anymore + // generateSSHKey(sshKeyType, { + // onSuccessCallback: () => { + // toast.show("SSH Key generated successfully", { kind: "success" }); + // }, + // }); } }, - [sshKeyType, sshKeyPair, isModalOpen, generateSSHKey], + [sshKeyType, sshPublicKey, generateSSHKey, isImport], ); const repositorySettingsUrl = getRepositorySettingsUrl( @@ -242,13 +229,13 @@ function AddDeployKey({ value?.remoteUrl, ); - const loading = isFetchingSSHKeyPair || isGeneratingSSHKey; + const loading = isFetchSSHKeyLoading || isGenerateSSHKeyLoading; const onCopy = useCallback(() => { AnalyticsUtil.logEvent("GS_COPY_SSH_KEY_BUTTON_CLICK"); }, []); - const onDeployKeyAddedCheckChange = useCallback( + const handleAddedKeyCheck = useCallback( (isAddedDeployKey: boolean) => { onChange({ isAddedDeployKey }); }, @@ -257,19 +244,19 @@ function AddDeployKey({ return ( <> - {errorData && - errorData?.responseMeta?.error?.code !== "AE-GIT-4033" && - errorData?.responseMeta?.error?.code !== "AE-GIT-4032" && ( + {connectError && + connectError.code !== "AE-GIT-4033" && + connectError.code !== "AE-GIT-4032" && ( - {errorData?.responseMeta?.error?.errorType} + {connectError.errorType} - {errorData?.responseMeta?.error?.message} + {connectError.message} )} {/* hardcoding message because server doesn't support feature flag. Will change this later */} - {errorData && errorData?.responseMeta?.error?.code === "AE-GIT-4032" && ( + {connectError && connectError.code === "AE-GIT-4032" && ( {createMessage(ERROR_SSH_KEY_MISCONF_TITLE)} @@ -286,7 +273,7 @@ function AddDeployKey({ {createMessage(ADD_DEPLOY_KEY_STEP_TITLE)} + )} + {possibleSteps.includes(activeStep) && + currentIndex > 0 && + !isLoading && ( + + )} + + + ); +} + +export default ConnectInitialize; diff --git a/app/client/src/git/components/ConnectModal/ConnectInitialize/types.ts b/app/client/src/git/components/ConnectModal/ConnectInitialize/types.ts new file mode 100644 index 000000000000..024375064f35 --- /dev/null +++ b/app/client/src/git/components/ConnectModal/ConnectInitialize/types.ts @@ -0,0 +1,10 @@ +import type { GitProvider } from "./ChooseGitProvider"; + +export interface ConnectFormDataState { + gitProvider?: GitProvider; + gitEmptyRepoExists?: string; + gitExistingRepoExists?: boolean; + remoteUrl?: string; + isAddedDeployKey?: boolean; + sshKeyType?: "RSA" | "ECDSA"; +} diff --git a/app/client/src/git/components/ConnectModal/ConnectModalView.tsx b/app/client/src/git/components/ConnectModal/ConnectModalView.tsx new file mode 100644 index 000000000000..0146a5cf9237 --- /dev/null +++ b/app/client/src/git/components/ConnectModal/ConnectModalView.tsx @@ -0,0 +1,103 @@ +import { Modal, ModalContent } from "@appsmith/ads"; +import type { ConnectRequestParams } from "git/requests/connectRequest.types"; +import type { GitImportRequestParams } from "git/requests/gitImportRequest.types"; +import type { GitApiError } from "git/store/types"; +import React, { useCallback } from "react"; +import ConnectInitialize from "./ConnectInitialize"; +import ConnectSuccess from "./ConnectSuccess"; +import { noop } from "lodash"; +import type { GitSettingsTab } from "git/constants/enums"; +import styled from "styled-components"; + +const StyledModalContent = styled(ModalContent)` + &&& { + width: 640px; + transform: none !important; + top: 100px; + left: calc(50% - 320px); + max-height: calc(100vh - 200px); + } +`; + +interface ConnectModalViewProps { + artifactType: string; + connect: (params: ConnectRequestParams) => void; + connectError: GitApiError | null; + fetchSSHKey: () => void; + generateSSHKey: (keyType: string) => void; + gitImport: (params: GitImportRequestParams) => void; + isConnectLoading: boolean; + isConnectModalOpen: boolean; + isFetchSSHKeyLoading: boolean; + isGenerateSSHKeyLoading: boolean; + isGitImportLoading: boolean; + isImport: boolean; + resetFetchSSHKey: () => void; + resetGenerateSSHKey: () => void; + sshPublicKey: string | null; + toggleConnectModal: (open: boolean) => void; + isGitConnected: boolean; + remoteUrl: string | null; + toggleSettingsModal: ( + open: boolean, + tab?: keyof typeof GitSettingsTab, + ) => void; + defaultBranch: string | null; + repoName: string | null; + setImportWorkspaceId: () => void; + isCreateArtifactPermitted: boolean; +} + +function ConnectModalView({ + defaultBranch = null, + isConnectModalOpen = false, + isGitConnected = false, + remoteUrl = null, + repoName = null, + resetFetchSSHKey = noop, + resetGenerateSSHKey = noop, + toggleConnectModal = noop, + toggleSettingsModal = noop, + ...rest +}: ConnectModalViewProps) { + const handleModalOpenChange = useCallback( + (open: boolean) => { + if (!open) { + resetFetchSSHKey(); + resetGenerateSSHKey(); + } + + toggleConnectModal(open); + }, + [resetFetchSSHKey, resetGenerateSSHKey, toggleConnectModal], + ); + + return ( + + + {isConnectModalOpen ? ( + // need fragment to arrange conditions properly + // eslint-disable-next-line react/jsx-no-useless-fragment + <> + {isGitConnected ? ( + + ) : ( + + )} + + ) : null} + + + ); +} + +export default ConnectModalView; diff --git a/app/client/src/git/components/ConnectModal/ConnectSuccess/index.tsx b/app/client/src/git/components/ConnectModal/ConnectSuccess/index.tsx new file mode 100644 index 000000000000..47f762b6bb5b --- /dev/null +++ b/app/client/src/git/components/ConnectModal/ConnectSuccess/index.tsx @@ -0,0 +1,178 @@ +import { + GIT_CONNECT_SUCCESS_PROTECTION_MSG, + GIT_CONNECT_SUCCESS_TITLE, + GIT_CONNECT_SUCCESS_ACTION_SETTINGS, + GIT_CONNECT_SUCCESS_ACTION_CONTINUE, + createMessage, + GIT_CONNECT_SUCCESS_PROTECTION_DOC_CTA, + GIT_CONNECT_SUCCESS_DEFAULT_BRANCH, + GIT_CONNECT_SUCCESS_REPO_NAME, + GIT_CONNECT_SUCCESS_DEFAULT_BRANCH_TOOLTIP, +} from "ee/constants/messages"; +import { + Button, + Icon, + ModalBody, + ModalFooter, + Text, + Link, + Tooltip, +} from "@appsmith/ads"; +import React, { useCallback } from "react"; +import styled from "styled-components"; +import AnalyticsUtil from "ee/utils/AnalyticsUtil"; +import { DOCS_BRANCH_PROTECTION_URL } from "constants/ThirdPartyConstants"; +import noop from "lodash/noop"; +import type { GitSettingsTab } from "git/constants/enums"; + +const TitleText = styled(Text)` + flex: 1; + font-weight: 600; +`; + +const LinkText = styled(Text)` + span { + font-weight: 500; + } +`; + +function ConnectionSuccessTitle() { + return ( +
+ + + {createMessage(GIT_CONNECT_SUCCESS_TITLE)} + +
+ ); +} + +interface ConnectSuccessModalViewProps { + repoName: string | null; + defaultBranch: string | null; +} + +function ConnectSuccessModalView({ + defaultBranch, + repoName, +}: ConnectSuccessModalViewProps) { + return ( + <> +
+
+
+ + + {createMessage(GIT_CONNECT_SUCCESS_REPO_NAME)} + +
+ {repoName || "-"} +
+
+
+ + + {createMessage(GIT_CONNECT_SUCCESS_DEFAULT_BRANCH)} + + + + +
+ {defaultBranch || "-"} +
+
+
+ + {createMessage(GIT_CONNECT_SUCCESS_PROTECTION_MSG)} + +
+ + + {createMessage(GIT_CONNECT_SUCCESS_PROTECTION_DOC_CTA)} + + + + ); +} + +interface ConnectSuccessProps { + defaultBranch: string | null; + remoteUrl: string | null; + repoName: string | null; + toggleConnectModal: (open: boolean) => void; + toggleSettingsModal: ( + open: boolean, + tab?: keyof typeof GitSettingsTab, + ) => void; +} + +function ConnectSuccess({ + defaultBranch, + remoteUrl = null, + repoName, + toggleConnectModal = noop, + toggleSettingsModal = noop, +}: ConnectSuccessProps) { + const handleStartGit = useCallback(() => { + toggleConnectModal(false); + AnalyticsUtil.logEvent("GS_START_USING_GIT", { + repoUrl: remoteUrl, + }); + }, [remoteUrl, toggleConnectModal]); + + const handleOpenSettings = useCallback(() => { + toggleConnectModal(false); + toggleSettingsModal(true); + AnalyticsUtil.logEvent("GS_OPEN_GIT_SETTINGS", { + repoUrl: remoteUrl, + }); + }, [remoteUrl, toggleConnectModal, toggleSettingsModal]); + + return ( + <> + + + + + + + + + + ); +} + +export default ConnectSuccess; diff --git a/app/client/src/git/components/ConnectModal/index.tsx b/app/client/src/git/components/ConnectModal/index.tsx index f7c8d097213d..a8f268132d95 100644 --- a/app/client/src/git/components/ConnectModal/index.tsx +++ b/app/client/src/git/components/ConnectModal/index.tsx @@ -1,338 +1,68 @@ -import React, { useCallback, useState } from "react"; -import styled from "styled-components"; +import React from "react"; +import ConnectModalView from "./ConnectModalView"; +import { useGitContext } from "../GitContextProvider"; +import useConnect from "git/hooks/useConnect"; +import useMetadata from "git/hooks/useMetadata"; +import useSettings from "git/hooks/useSettings"; -import AddDeployKey, { type AddDeployKeyProps } from "./AddDeployKey"; -import AnalyticsUtil from "ee/utils/AnalyticsUtil"; -import ChooseGitProvider from "./ChooseGitProvider"; -import GenerateSSH from "./GenerateSSH"; -import Steps from "./Steps"; -import Statusbar from "../Statusbar"; -import { Button, ModalBody, ModalFooter } from "@appsmith/ads"; -import { GIT_CONNECT_STEPS } from "./constants"; -import type { GitProvider } from "./ChooseGitProvider"; -import { - ADD_DEPLOY_KEY_STEP, - CHOOSE_A_GIT_PROVIDER_STEP, - CONFIGURE_GIT, - CONNECT_GIT_TEXT, - GENERATE_SSH_KEY_STEP, - GIT_CONNECT_WAITING, - GIT_IMPORT_WAITING, - IMPORT_APP_CTA, - PREVIOUS_STEP, - createMessage, -} from "ee/constants/messages"; -import { isValidGitRemoteUrl } from "../utils"; -import type { ApiResponse } from "api/ApiResponses"; - -const OFFSET = 200; -const OUTER_PADDING = 32; -const FOOTER = 56; -const HEADER = 44; - -const StyledModalBody = styled(ModalBody)` - flex: 1; - overflow-y: initial; - display: flex; - flex-direction: column; - max-height: calc( - 100vh - ${OFFSET}px - ${OUTER_PADDING}px - ${FOOTER}px - ${HEADER}px - ); -`; - -const StyledModalFooter = styled(ModalFooter)` - justify-content: space-between; - flex-direction: ${(p) => (!p.loading ? "row-reverse" : "row")}; -`; - -const steps = [ - { - key: GIT_CONNECT_STEPS.CHOOSE_PROVIDER, - text: createMessage(CHOOSE_A_GIT_PROVIDER_STEP), - }, - { - key: GIT_CONNECT_STEPS.GENERATE_SSH_KEY, - text: createMessage(GENERATE_SSH_KEY_STEP), - }, - { - key: GIT_CONNECT_STEPS.ADD_DEPLOY_KEY, - text: createMessage(ADD_DEPLOY_KEY_STEP), - }, -]; - -const possibleSteps = steps.map((s) => s.key); - -interface StyledModalFooterProps { - loading?: boolean; -} - -interface FormDataState { - gitProvider?: GitProvider; - gitEmptyRepoExists?: string; - gitExistingRepoExists?: boolean; - remoteUrl?: string; - isAddedDeployKey?: boolean; - sshKeyType?: "RSA" | "ECDSA"; -} - -interface GitProfile { - authorName: string; - authorEmail: string; - useDefaultProfile?: boolean; -} - -interface ConnectOrImportPayload { - remoteUrl: string; - gitProfile: GitProfile; -} - -interface ConnectOrImportProps { - payload: ConnectOrImportPayload; - onErrorCallback: (error: Error, response: ApiResponse) => void; -} - -// Remove comments after integration interface ConnectModalProps { isImport?: boolean; - // It replaces const isImportingViaGit in GitConnectionV2/index.tsx - isImporting?: boolean; - // Replaces dispatch(importAppFromGit) - importFrom: (props: ConnectOrImportProps) => void; - // Replaces connectToGit from useGitConnect hook - connectTo: (props: ConnectOrImportProps) => void; - // Replaces isConnectingToGit - isConnectingTo?: boolean; - isConnecting: boolean; - artifactId: string; - artifactType: string; - // Replaces handleImport in original ChooseGitProvider.tsx - onImportFromCalloutLinkClick: () => void; - // Replaces hasCreateNewApplicationPermission = hasCreateNewAppPermission(workspace.userPermissions) - canCreateNewArtifact: boolean; - isModalOpen: boolean; - deployKeyDocUrl: AddDeployKeyProps["deployKeyDocUrl"]; - isFetchingSSHKeyPair: AddDeployKeyProps["isFetchingSSHKeyPair"]; - fetchSSHKeyPair: AddDeployKeyProps["fetchSSHKeyPair"]; - generateSSHKey: AddDeployKeyProps["generateSSHKey"]; - isGeneratingSSHKey: AddDeployKeyProps["isGeneratingSSHKey"]; - sshKeyPair: AddDeployKeyProps["sshKeyPair"]; } -function ConnectModal({ - artifactId, - artifactType, - canCreateNewArtifact, - connectTo, - deployKeyDocUrl, - fetchSSHKeyPair, - generateSSHKey, - importFrom, - isConnecting = false, - isFetchingSSHKeyPair, - isGeneratingSSHKey, - isImport = false, - isImporting = false, - isModalOpen, - onImportFromCalloutLinkClick, - sshKeyPair, -}: ConnectModalProps) { - const [errorData, setErrorData] = useState>(); - - const nextStepText = { - [GIT_CONNECT_STEPS.CHOOSE_PROVIDER]: createMessage(CONFIGURE_GIT), - [GIT_CONNECT_STEPS.GENERATE_SSH_KEY]: createMessage(GENERATE_SSH_KEY_STEP), - [GIT_CONNECT_STEPS.ADD_DEPLOY_KEY]: createMessage( - isImport ? IMPORT_APP_CTA : CONNECT_GIT_TEXT, - ), - }; - - const [formData, setFormData] = useState({ - gitProvider: undefined, - gitEmptyRepoExists: undefined, - gitExistingRepoExists: false, - remoteUrl: undefined, - isAddedDeployKey: false, - sshKeyType: "ECDSA", - }); - - const handleChange = (partialFormData: Partial) => { - setFormData((s) => ({ ...s, ...partialFormData })); - }; - - const [activeStep, setActiveStep] = useState( - GIT_CONNECT_STEPS.CHOOSE_PROVIDER, - ); - const currentIndex = steps.findIndex((s) => s.key === activeStep); - - const isDisabled = { - [GIT_CONNECT_STEPS.CHOOSE_PROVIDER]: !isImport - ? !formData.gitProvider || - !formData.gitEmptyRepoExists || - formData.gitEmptyRepoExists === "no" - : !formData.gitProvider || !formData.gitExistingRepoExists, - [GIT_CONNECT_STEPS.GENERATE_SSH_KEY]: - typeof formData?.remoteUrl !== "string" || - !isValidGitRemoteUrl(formData?.remoteUrl), - [GIT_CONNECT_STEPS.ADD_DEPLOY_KEY]: !formData.isAddedDeployKey, - }; - - const handlePreviousStep = useCallback(() => { - if (currentIndex > 0) { - setActiveStep(steps[currentIndex - 1].key); - } - }, [currentIndex]); - - const handleNextStep = useCallback(() => { - if (currentIndex < steps.length) { - switch (activeStep) { - case GIT_CONNECT_STEPS.CHOOSE_PROVIDER: { - setActiveStep(GIT_CONNECT_STEPS.GENERATE_SSH_KEY); - AnalyticsUtil.logEvent("GS_CONFIGURE_GIT"); - break; - } - case GIT_CONNECT_STEPS.GENERATE_SSH_KEY: { - setActiveStep(GIT_CONNECT_STEPS.ADD_DEPLOY_KEY); - AnalyticsUtil.logEvent("GS_GENERATE_KEY_BUTTON_CLICK", { - repoUrl: formData?.remoteUrl, - connectFlow: "v2", - }); - break; - } - case GIT_CONNECT_STEPS.ADD_DEPLOY_KEY: { - const gitProfile = { - authorName: "", - authorEmail: "", - useGlobalProfile: true, - }; - - if (formData.remoteUrl) { - if (!isImport) { - connectTo({ - payload: { - remoteUrl: formData.remoteUrl, - gitProfile, - }, - onErrorCallback: (error, response) => { - // AE-GIT-4033 is repo not empty error - if (response?.responseMeta?.error?.code === "AE-GIT-4033") { - setActiveStep(GIT_CONNECT_STEPS.GENERATE_SSH_KEY); - } - - setErrorData(response); - }, - }); - AnalyticsUtil.logEvent( - "GS_CONNECT_BUTTON_ON_GIT_SYNC_MODAL_CLICK", - { repoUrl: formData?.remoteUrl, connectFlow: "v2" }, - ); - } else { - importFrom({ - payload: { - remoteUrl: formData.remoteUrl, - gitProfile, - // isDefaultProfile: true, - }, - onErrorCallback(error, response) { - setErrorData(response); - }, - }); - } - } - - break; - } - } - } - }, [ - activeStep, - connectTo, - currentIndex, - formData.remoteUrl, - importFrom, - isImport, - ]); - - const stepProps = { - onChange: handleChange, - value: formData, - isImport, - errorData, - }; - - const loading = (!isImport && isConnecting) || (isImport && isImporting); +function ConnectModal({ isImport = false }: ConnectModalProps) { + const { artifactDef, isCreateArtifactPermitted, setImportWorkspaceId } = + useGitContext(); + const { + connect, + connectError, + fetchSSHKey, + generateSSHKey, + gitImport, + isConnectLoading, + isConnectModalOpen, + isFetchSSHKeyLoading, + isGenerateSSHKeyLoading, + isGitImportLoading, + resetFetchSSHKey, + resetGenerateSSHKey, + sshKey, + toggleConnectModal, + } = useConnect(); + const { isGitConnected, metadata } = useMetadata(); + const { toggleSettingsModal } = useSettings(); + + const { artifactType } = artifactDef; + const sshPublicKey = sshKey?.publicKey ?? null; + const remoteUrl = metadata?.remoteUrl ?? null; + const repoName = metadata?.repoName ?? null; + const defaultBranch = metadata?.defaultBranchName ?? null; return ( - <> - - {possibleSteps.includes(activeStep) && ( - - )} - {activeStep === GIT_CONNECT_STEPS.CHOOSE_PROVIDER && ( - - )} - {activeStep === GIT_CONNECT_STEPS.GENERATE_SSH_KEY && ( - - )} - {activeStep === GIT_CONNECT_STEPS.ADD_DEPLOY_KEY && ( - - )} - - - {loading && ( - - )} - {!loading && ( - - )} - {possibleSteps.includes(activeStep) && currentIndex > 0 && !loading && ( - - )} - - + ); } diff --git a/app/client/src/git/components/GitContextProvider/hooks/useGitBranches.ts b/app/client/src/git/components/GitContextProvider/hooks/useGitBranches.ts deleted file mode 100644 index 551338860fad..000000000000 --- a/app/client/src/git/components/GitContextProvider/hooks/useGitBranches.ts +++ /dev/null @@ -1,139 +0,0 @@ -import type { GitArtifactType } from "git/constants/enums"; -import type { FetchBranchesResponseData } from "git/requests/fetchBranchesRequest.types"; -import { gitArtifactActions } from "git/store/gitArtifactSlice"; -import { - selectBranches, - selectCheckoutBranch, - selectCreateBranch, - selectCurrentBranch, - selectDeleteBranch, -} from "git/store/selectors/gitSingleArtifactSelectors"; -import type { GitApiError, GitRootState } from "git/store/types"; -import { useCallback, useMemo } from "react"; -import { useDispatch, useSelector } from "react-redux"; - -interface UseGitBranchesParams { - artifactType: keyof typeof GitArtifactType; - baseArtifactId: string; -} - -export interface UseGitBranchesReturnValue { - branches: FetchBranchesResponseData | null; - fetchBranchesLoading: boolean; - fetchBranchesError: GitApiError | null; - fetchBranches: () => void; - createBranchLoading: boolean; - createBranchError: GitApiError | null; - createBranch: (branchName: string) => void; - deleteBranchLoading: boolean; - deleteBranchError: GitApiError | null; - deleteBranch: (branchName: string) => void; - checkoutBranchLoading: boolean; - checkoutBranchError: GitApiError | null; - checkoutBranch: (branchName: string) => void; - currentBranch: string | null; - toggleBranchListPopup: (open: boolean) => void; -} - -export default function useGitBranches({ - artifactType, - baseArtifactId, -}: UseGitBranchesParams): UseGitBranchesReturnValue { - const basePayload = useMemo( - () => ({ artifactType, baseArtifactId }), - [artifactType, baseArtifactId], - ); - const dispatch = useDispatch(); - - // fetch branches - const branchesState = useSelector((state: GitRootState) => - selectBranches(state, basePayload), - ); - const fetchBranches = useCallback(() => { - dispatch( - gitArtifactActions.fetchBranchesInit({ - ...basePayload, - pruneBranches: true, - }), - ); - }, [basePayload, dispatch]); - - // create branch - const createBranchState = useSelector((state: GitRootState) => - selectCreateBranch(state, basePayload), - ); - const createBranch = useCallback( - (branchName: string) => { - dispatch( - gitArtifactActions.createBranchInit({ - ...basePayload, - branchName, - }), - ); - }, - [basePayload, dispatch], - ); - // delete branch - const deleteBranchState = useSelector((state: GitRootState) => - selectDeleteBranch(state, basePayload), - ); - const deleteBranch = useCallback( - (branchName: string) => { - dispatch( - gitArtifactActions.deleteBranchInit({ - ...basePayload, - branchName, - }), - ); - }, - [basePayload, dispatch], - ); - // checkout branch - const checkoutBranchState = useSelector((state: GitRootState) => - selectCheckoutBranch(state, basePayload), - ); - const checkoutBranch = useCallback( - (branchName: string) => { - dispatch( - gitArtifactActions.checkoutBranchInit({ - ...basePayload, - branchName, - }), - ); - }, - [basePayload, dispatch], - ); - - // derived - const currentBranch = useSelector((state: GitRootState) => - selectCurrentBranch(state, basePayload), - ); - - // git branch list popup - const toggleBranchListPopup = (open: boolean) => { - dispatch( - gitArtifactActions.toggleBranchListPopup({ - ...basePayload, - open, - }), - ); - }; - - return { - branches: branchesState?.value, - fetchBranchesLoading: branchesState?.loading ?? false, - fetchBranchesError: branchesState?.error, - fetchBranches, - createBranchLoading: createBranchState?.loading ?? false, - createBranchError: createBranchState?.error, - createBranch, - deleteBranchLoading: deleteBranchState?.loading ?? false, - deleteBranchError: deleteBranchState?.error, - deleteBranch, - checkoutBranchLoading: checkoutBranchState?.loading ?? false, - checkoutBranchError: checkoutBranchState?.error, - checkoutBranch, - 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 deleted file mode 100644 index 9741a857e7a3..000000000000 --- a/app/client/src/git/components/GitContextProvider/hooks/useGitConnect.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { GitArtifactType } from "git/constants/enums"; -import { gitArtifactActions } from "git/store/gitArtifactSlice"; -import { useMemo } from "react"; -import { useDispatch } from "react-redux"; - -interface UseGitConnectParams { - artifactType: keyof typeof GitArtifactType; - baseArtifactId: string; -} - -export interface UseGitConnectReturnValue { - toggleConnectModal: (open: boolean) => void; -} - -export default function useGitConnect({ - artifactType, - baseArtifactId, -}: UseGitConnectParams): UseGitConnectReturnValue { - const dispatch = useDispatch(); - const basePayload = useMemo( - () => ({ artifactType, baseArtifactId }), - [artifactType, baseArtifactId], - ); - - const toggleConnectModal = (open: boolean) => { - dispatch( - gitArtifactActions.toggleConnectModal({ - ...basePayload, - open, - }), - ); - }; - - return { - toggleConnectModal, - }; -} diff --git a/app/client/src/git/components/GitContextProvider/hooks/useGitContextValue.ts b/app/client/src/git/components/GitContextProvider/hooks/useGitContextValue.ts deleted file mode 100644 index 9b62bc1ba4cf..000000000000 --- a/app/client/src/git/components/GitContextProvider/hooks/useGitContextValue.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { GitArtifactType } from "git/constants/enums"; -import type { UseGitConnectReturnValue } from "./useGitConnect"; -import type { UseGitOpsReturnValue } from "./useGitOps"; -import type { UseGitBranchesReturnValue } from "./useGitBranches"; -import useGitConnect from "./useGitConnect"; -import useGitOps from "./useGitOps"; -import useGitBranches from "./useGitBranches"; -import { useMemo } from "react"; - -// 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; - statusTransformer: ( - status: FetchStatusResponseData, - ) => StatusTreeStruct[] | null; -} - -export interface GitContextValue - extends UseGitConnectReturnValue, - UseGitOpsReturnValue, - UseGitBranchesReturnValue { - artifactType: keyof typeof GitArtifactType; - baseArtifactId: string; - artifactDef: { - artifactType: keyof typeof GitArtifactType; - baseArtifactId: string; - }; - artifact: ApplicationPayload | null; - statusTransformer: ( - status: FetchStatusResponseData, - ) => StatusTreeStruct[] | null; -} - -export default function useGitContextValue({ - artifact, - artifactType, - baseArtifactId = "", - statusTransformer, -}: UseGitContextValueParams): GitContextValue { - const artifactDef = useMemo( - () => ({ artifactType, baseArtifactId }), - [artifactType, baseArtifactId], - ); - const useGitConnectReturnValue = useGitConnect(artifactDef); - const useGitOpsReturnValue = useGitOps({ - ...artifactDef, - artifactId: artifact?.id ?? null, - }); - const useGitBranchesReturnValue = useGitBranches(artifactDef); - - return { - artifactType, - baseArtifactId, - artifactDef, - statusTransformer, - artifact, - ...useGitOpsReturnValue, - ...useGitBranchesReturnValue, - ...useGitConnectReturnValue, - }; -} diff --git a/app/client/src/git/components/GitContextProvider/hooks/useGitMetadata.ts b/app/client/src/git/components/GitContextProvider/hooks/useGitMetadata.ts deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/app/client/src/git/components/GitContextProvider/hooks/useGitOps.ts b/app/client/src/git/components/GitContextProvider/hooks/useGitOps.ts deleted file mode 100644 index 97b224f33717..000000000000 --- a/app/client/src/git/components/GitContextProvider/hooks/useGitOps.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { GitOpsTab, type GitArtifactType } from "git/constants/enums"; -import type { FetchMergeStatusResponseData } from "git/requests/fetchMergeStatusRequest.types"; -import type { FetchStatusResponseData } from "git/requests/fetchStatusRequest.types"; -import { gitArtifactActions } from "git/store/gitArtifactSlice"; -import { - selectCommit, - selectConflictErrorModalOpen, - selectDiscard, - selectMerge, - selectMergeStatus, - selectOpsModalOpen, - selectOpsModalTab, - selectPull, - selectStatus, -} from "git/store/selectors/gitSingleArtifactSelectors"; -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: GitApiError | null; - commit: (commitMessage: string) => void; - clearCommitError: () => void; - discardLoading: boolean; - discardError: GitApiError | null; - discard: () => void; - clearDiscardError: () => void; - status: FetchStatusResponseData | null; - fetchStatusLoading: boolean; - fetchStatusError: GitApiError | null; - fetchStatus: () => void; - mergeLoading: boolean; - mergeError: GitApiError | null; - merge: () => void; - mergeStatus: FetchMergeStatusResponseData | null; - fetchMergeStatusLoading: boolean; - fetchMergeStatusError: GitApiError | null; - fetchMergeStatus: (sourceBranch: string, destinationBranch: string) => void; - clearMergeStatus: () => void; - pullLoading: boolean; - pullError: GitApiError | null; - pull: () => 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 { - const dispatch = useDispatch(); - const basePayload = useMemo( - () => ({ artifactType, baseArtifactId }), - [artifactType, baseArtifactId], - ); - - // commit - const commitState = useSelector((state: GitRootState) => - selectCommit(state, basePayload), - ); - - const commit = useCallback( - (commitMessage: string) => { - dispatch( - gitArtifactActions.commitInit({ - ...basePayload, - commitMessage, - doPush: true, - }), - ); - }, - [basePayload, dispatch], - ); - - const clearCommitError = useCallback(() => { - dispatch(gitArtifactActions.clearCommitError(basePayload)); - }, [basePayload, dispatch]); - - // discard - const discardState = useSelector((state: GitRootState) => - selectDiscard(state, basePayload), - ); - - const discard = useCallback(() => { - dispatch(gitArtifactActions.discardInit(basePayload)); - }, [basePayload, dispatch]); - - const clearDiscardError = useCallback(() => { - dispatch(gitArtifactActions.clearDiscardError(basePayload)); - }, [basePayload, dispatch]); - - // status - const statusState = useSelector((state: GitRootState) => - selectStatus(state, basePayload), - ); - - const fetchStatus = useCallback(() => { - dispatch( - gitArtifactActions.fetchStatusInit({ - ...basePayload, - compareRemote: true, - }), - ); - }, [basePayload, dispatch]); - - // merge - const mergeState = useSelector((state: GitRootState) => - selectMerge(state, basePayload), - ); - - const merge = useCallback(() => { - dispatch(gitArtifactActions.mergeInit(basePayload)); - }, [basePayload, dispatch]); - - // merge status - const mergeStatusState = useSelector((state: GitRootState) => - selectMergeStatus(state, 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 - const pullState = useSelector((state: GitRootState) => - selectPull(state, basePayload), - ); - - const pull = useCallback(() => { - dispatch( - gitArtifactActions.pullInit({ - ...basePayload, - artifactId: artifactId ?? "", - }), - ); - }, [basePayload, artifactId, dispatch]); - - // ops modal - const opsModalOpen = useSelector((state: GitRootState) => - selectOpsModalOpen(state, basePayload), - ); - - const opsModalTab = useSelector((state: GitRootState) => - selectOpsModalTab(state, basePayload), - ); - - const toggleOpsModal = useCallback( - (open: boolean, tab: keyof typeof GitOpsTab = GitOpsTab.Deploy) => { - dispatch( - 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], - ); - - return { - 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, - fetchStatus, - mergeLoading: mergeState?.loading ?? false, - mergeError: mergeState?.error, - merge, - mergeStatus: mergeStatusState?.value, - fetchMergeStatusLoading: mergeStatusState?.loading ?? false, - fetchMergeStatusError: mergeStatusState?.error, - fetchMergeStatus, - clearMergeStatus, - pullLoading: pullState?.loading ?? false, - pullError: pullState?.error, - pull, - opsModalTab, - opsModalOpen, - toggleOpsModal, - conflictErrorModalOpen, - toggleConflictErrorModal, - }; -} diff --git a/app/client/src/git/components/GitContextProvider/index.tsx b/app/client/src/git/components/GitContextProvider/index.tsx index 671f3b66a605..4d4b88b4f6ad 100644 --- a/app/client/src/git/components/GitContextProvider/index.tsx +++ b/app/client/src/git/components/GitContextProvider/index.tsx @@ -1,7 +1,22 @@ -import React, { createContext, useContext } from "react"; -import useGitContextValue from "./hooks/useGitContextValue"; -import type { UseGitContextValueParams } from "./hooks/useGitContextValue"; -import type { GitContextValue } from "./hooks/useGitContextValue"; +import React, { createContext, useCallback, useContext, useMemo } from "react"; +import type { GitArtifactType } from "git/constants/enums"; +import type { ApplicationPayload } from "entities/Application"; +import type { FetchStatusResponseData } from "git/requests/fetchStatusRequest.types"; +import type { StatusTreeStruct } from "../StatusChanges/StatusTree"; +import { useDispatch } from "react-redux"; + +export interface GitContextValue { + artifactDef: { + artifactType: keyof typeof GitArtifactType; + baseArtifactId: string; + }; + artifact: ApplicationPayload | null; + statusTransformer: ( + status: FetchStatusResponseData, + ) => StatusTreeStruct[] | null; + setImportWorkspaceId: () => void; + isCreateArtifactPermitted: boolean; +} const gitContextInitialValue = {} as GitContextValue; @@ -11,15 +26,65 @@ export const useGitContext = () => { return useContext(GitContext); }; -interface GitContextProviderProps extends UseGitContextValueParams { +interface GitContextProviderProps { + artifactType: keyof typeof GitArtifactType; + baseArtifactId: string; + artifact: ApplicationPayload | null; + isCreateArtifactPermitted: boolean; + setWorkspaceIdForImport: (params: { + workspaceId: string; + editorId: string; + }) => void; + statusTransformer: ( + status: FetchStatusResponseData, + ) => StatusTreeStruct[] | null; children: React.ReactNode; } export default function GitContextProvider({ + artifact = null, + artifactType, + baseArtifactId, children, - ...useContextValueParams + isCreateArtifactPermitted, + setWorkspaceIdForImport, + statusTransformer, }: GitContextProviderProps) { - const contextValue = useGitContextValue(useContextValueParams); + const artifactDef = useMemo( + () => ({ artifactType, baseArtifactId }), + [artifactType, baseArtifactId], + ); + + const dispatch = useDispatch(); + + const { id: artifactId, workspaceId } = artifact ?? {}; + const setImportWorkspaceId = useCallback(() => { + if (workspaceId) { + dispatch( + setWorkspaceIdForImport({ + workspaceId: workspaceId ?? "", + editorId: artifactId ?? "", + }), + ); + } + }, [artifactId, dispatch, setWorkspaceIdForImport, workspaceId]); + + const contextValue: GitContextValue = useMemo( + () => ({ + artifactDef, + artifact, + statusTransformer, + isCreateArtifactPermitted, + setImportWorkspaceId, + }), + [ + artifactDef, + artifact, + statusTransformer, + isCreateArtifactPermitted, + setImportWorkspaceId, + ], + ); return ( {children} diff --git a/app/client/src/git/components/ImportModal/index.tsx b/app/client/src/git/components/ImportModal/index.tsx new file mode 100644 index 000000000000..f55a468d702b --- /dev/null +++ b/app/client/src/git/components/ImportModal/index.tsx @@ -0,0 +1,8 @@ +import React from "react"; +import ConnectModal from "../ConnectModal"; + +function ImportModal() { + return ; +} + +export default ImportModal; diff --git a/app/client/src/git/components/LocalProfile/LocalProfileView.tsx b/app/client/src/git/components/LocalProfile/LocalProfileView.tsx index beefbb66b29f..03c027824cb3 100644 --- a/app/client/src/git/components/LocalProfile/LocalProfileView.tsx +++ b/app/client/src/git/components/LocalProfile/LocalProfileView.tsx @@ -277,7 +277,7 @@ function LocalProfileView({ {...register("authorName", { required: createMessage(AUTHOR_NAME_CANNOT_BE_EMPTY), })} - onChange={handleInputChange("authorEmail")} + onChange={handleInputChange("authorName")} /> ) : ( diff --git a/app/client/src/git/components/OpsModal/TabDeploy/index.tsx b/app/client/src/git/components/OpsModal/TabDeploy/index.tsx index 53d1a8f4c6af..44bd8ee91cd2 100644 --- a/app/client/src/git/components/OpsModal/TabDeploy/index.tsx +++ b/app/client/src/git/components/OpsModal/TabDeploy/index.tsx @@ -2,25 +2,23 @@ import React from "react"; import TabDeployView from "./TabDeployView"; import { useGitContext } from "git/components/GitContextProvider"; import useMetadata from "git/hooks/useMetadata"; +import useBranches from "git/hooks/useBranches"; +import useCommit from "git/hooks/useCommit"; +import useDiscard from "git/hooks/useDiscard"; +import usePull from "git/hooks/usePull"; +import useStatus from "git/hooks/useStatus"; export default function TabDeploy() { - const { - artifact, - clearCommitError, - clearDiscardError, - commit, - commitError, - commitLoading, - currentBranch, - discard, - discardError, - discardLoading, - fetchStatusLoading, - pull, - pullError, - pullLoading, - status, - } = useGitContext(); + const { artifact } = useGitContext(); + const { clearCommitError, commit, commitError, isCommitLoading } = + useCommit(); + + const { clearDiscardError, discard, discardError, isDiscardLoading } = + useDiscard(); + + const { isPullLoading, pull, pullError } = usePull(); + const { isFetchStatusLoading, status } = useStatus(); + const { currentBranch } = useBranches(); const { metadata } = useMetadata(); const lastDeployedAt = artifact?.lastDeployedAt ?? null; @@ -38,11 +36,11 @@ export default function TabDeploy() { currentBranch={currentBranch} discard={discard} discardError={discardError} - isCommitLoading={commitLoading} - isDiscardLoading={discardLoading} - isFetchStatusLoading={fetchStatusLoading} + isCommitLoading={isCommitLoading} + isDiscardLoading={isDiscardLoading} + isFetchStatusLoading={isFetchStatusLoading} isPullFailing={isPullFailing} - isPullLoading={pullLoading} + isPullLoading={isPullLoading} lastDeployedAt={lastDeployedAt} pull={pull} remoteUrl={remoteUrl} diff --git a/app/client/src/git/components/OpsModal/TabMerge/index.tsx b/app/client/src/git/components/OpsModal/TabMerge/index.tsx index adeb24ee9914..a192d93dfcda 100644 --- a/app/client/src/git/components/OpsModal/TabMerge/index.tsx +++ b/app/client/src/git/components/OpsModal/TabMerge/index.tsx @@ -1,24 +1,23 @@ import React from "react"; import TabMergeView from "./TabMergeView"; -import { useGitContext } from "git/components/GitContextProvider"; import useProtectedBranches from "git/hooks/useProtectedBranches"; +import useBranches from "git/hooks/useBranches"; +import useMerge from "git/hooks/useMerge"; +import useStatus from "git/hooks/useStatus"; export default function TabMerge() { const { - branches, clearMergeStatus, - currentBranch, - fetchBranches, - fetchBranchesLoading, fetchMergeStatus, - fetchMergeStatusLoading, - fetchStatusLoading, + isFetchMergeStatusLoading, + isMergeLoading, merge, mergeError, - mergeLoading, mergeStatus, - status, - } = useGitContext(); + } = useMerge(); + const { isFetchStatusLoading, status } = useStatus(); + const { branches, currentBranch, fetchBranches, isFetchBranchesLoading } = + useBranches(); const { protectedBranches } = useProtectedBranches(); const isStatusClean = status?.isClean ?? false; @@ -30,10 +29,10 @@ export default function TabMerge() { currentBranch={currentBranch} fetchBranches={fetchBranches} fetchMergeStatus={fetchMergeStatus} - isFetchBranchesLoading={fetchBranchesLoading} - isFetchMergeStatusLoading={fetchMergeStatusLoading} - isFetchStatusLoading={fetchStatusLoading} - isMergeLoading={mergeLoading} + isFetchBranchesLoading={isFetchBranchesLoading} + isFetchMergeStatusLoading={isFetchMergeStatusLoading} + isFetchStatusLoading={isFetchStatusLoading} + isMergeLoading={isMergeLoading} isStatusClean={isStatusClean} merge={merge} mergeError={mergeError} diff --git a/app/client/src/git/components/OpsModal/index.tsx b/app/client/src/git/components/OpsModal/index.tsx index 1450d74cf7e8..114439c29902 100644 --- a/app/client/src/git/components/OpsModal/index.tsx +++ b/app/client/src/git/components/OpsModal/index.tsx @@ -1,12 +1,13 @@ import React from "react"; import OpsModalView from "./OpsModalView"; -import { useGitContext } from "../GitContextProvider"; import useProtectedBranches from "git/hooks/useProtectedBranches"; import useMetadata from "git/hooks/useMetadata"; +import useStatus from "git/hooks/useStatus"; +import useOps from "git/hooks/useOps"; export default function OpsModal() { - const { fetchStatus, opsModalOpen, opsModalTab, toggleOpsModal } = - useGitContext(); + const { opsModalOpen, opsModalTab, toggleOpsModal } = useOps(); + const { fetchStatus } = useStatus(); const { isProtectedMode } = useProtectedBranches(); const { metadata } = useMetadata(); diff --git a/app/client/src/git/components/ProtectedBranches/index.tsx b/app/client/src/git/components/ProtectedBranches/index.tsx index 7da03a947e51..81c98f49aba3 100644 --- a/app/client/src/git/components/ProtectedBranches/index.tsx +++ b/app/client/src/git/components/ProtectedBranches/index.tsx @@ -1,12 +1,12 @@ import React from "react"; import ProtectedBranchesView from "./ProtectedBranchesView"; -import { useGitContext } from "../GitContextProvider"; import useProtectedBranches from "git/hooks/useProtectedBranches"; import useGitFeatureFlags from "git/hooks/useGitFeatureFlags"; import useDefaultBranch from "git/ee/hooks/useDefaultBranch"; +import useBranches from "git/hooks/useBranches"; function ProtectedBranches() { - const { branches } = useGitContext(); + const { branches } = useBranches(); const { defaultBranch } = useDefaultBranch(); const { isUpdateProtectedBranchesLoading, diff --git a/app/client/src/git/components/QuickActions/BranchButton/index.tsx b/app/client/src/git/components/QuickActions/BranchButton.tsx similarity index 75% rename from app/client/src/git/components/QuickActions/BranchButton/index.tsx rename to app/client/src/git/components/QuickActions/BranchButton.tsx index d73cba35e40e..0f220d97feb4 100644 --- a/app/client/src/git/components/QuickActions/BranchButton/index.tsx +++ b/app/client/src/git/components/QuickActions/BranchButton.tsx @@ -1,23 +1,14 @@ import { Button, Icon, Tooltip } from "@appsmith/ads"; -import React, { useCallback, useEffect, useState } from "react"; +import React, { useCallback, useMemo } from "react"; import styled from "styled-components"; import noop from "lodash/noop"; -import BranchList from "./BranchList"; +import BranchList from "../BranchList"; import { Popover2 } from "@blueprintjs/popover2"; // internal dependencies import { isEllipsisActive } from "utils/helpers"; import AnalyticsUtil from "ee/utils/AnalyticsUtil"; -interface BranchButtonProps { - currentBranch?: string; - isOpen?: boolean; - setIsOpen?: (isOpen: boolean) => void; - isDisabled?: boolean; - isProtectedMode?: boolean; - isStatusClean?: boolean; -} - const ButtonContainer = styled(Button)` display: flex; align-items: center; @@ -45,20 +36,32 @@ const popoverModifiers: { offset: Record } = { offset: { enabled: true, options: { offset: [7, 10] } }, }; +interface BranchButtonProps { + currentBranch: string | null; + isAutocommitPolling: boolean; + isBranchPopupOpen: boolean; + isProtectedMode: boolean; + isStatusClean: boolean; + isTriggerAutocommitLoading: boolean; + toggleBranchPopup: (open: boolean) => void; +} + export default function BranchButton({ - currentBranch = "", - isDisabled = false, - isOpen = false, + currentBranch = null, + isAutocommitPolling = false, + isBranchPopupOpen = false, isProtectedMode = false, isStatusClean = false, - setIsOpen = noop, + isTriggerAutocommitLoading = false, + toggleBranchPopup = noop, }: BranchButtonProps) { - const [isEllipsis, setIsEllipsis] = useState(false); const labelTarget = React.useRef(null); + const isDisabled = isTriggerAutocommitLoading || isAutocommitPolling; + const onPopoverInteraction = useCallback( (nextState: boolean) => { - setIsOpen(nextState); + toggleBranchPopup(nextState); if (nextState) { AnalyticsUtil.logEvent("GS_OPEN_BRANCH_LIST_POPUP", { @@ -66,24 +69,20 @@ export default function BranchButton({ }); } }, - [setIsOpen], + [toggleBranchPopup], ); - useEffect(function ellipsisCheck() { - setIsEllipsis(isEllipsisActive(labelTarget.current) ?? false); - }, []); - - const renderContent = useCallback(() => { + const content = useMemo(() => { return ; }, []); return ( Test; -} diff --git a/app/client/src/git/components/QuickActions/QuickActionsView.test.tsx b/app/client/src/git/components/QuickActions/QuickActionsView.test.tsx index a1d595b923c8..6e696ddffe55 100644 --- a/app/client/src/git/components/QuickActions/QuickActionsView.test.tsx +++ b/app/client/src/git/components/QuickActions/QuickActionsView.test.tsx @@ -21,9 +21,11 @@ jest.mock("./../Statusbar", () => () => ( describe("QuickActionsView Component", () => { const defaultProps = { + currentBranch: "main", discard: jest.fn(), isAutocommitEnabled: false, isAutocommitPolling: false, + isBranchPopupOpen: false, isConnectPermitted: true, isDiscardLoading: false, isFetchStatusLoading: false, @@ -32,12 +34,14 @@ describe("QuickActionsView Component", () => { isPullFailing: false, isPullLoading: false, isStatusClean: true, + isTriggerAutocommitLoading: false, pull: jest.fn(), statusBehindCount: 0, statusChangeCount: 0, toggleConnectModal: jest.fn(), toggleOpsModal: jest.fn(), toggleSettingsModal: jest.fn(), + toggleBranchPopup: jest.fn(), }; afterEach(() => { diff --git a/app/client/src/git/components/QuickActions/QuickActionsView.tsx b/app/client/src/git/components/QuickActions/QuickActionsView.tsx index eedb67bf3787..396984788015 100644 --- a/app/client/src/git/components/QuickActions/QuickActionsView.tsx +++ b/app/client/src/git/components/QuickActions/QuickActionsView.tsx @@ -16,6 +16,7 @@ import QuickActionButton from "./QuickActionButton"; import AutocommitStatusbar from "../Statusbar"; import getPullBtnStatus from "./helpers/getPullButtonStatus"; import noop from "lodash/noop"; +import BranchButton from "./BranchButton"; const Container = styled.div` height: 100%; @@ -24,9 +25,11 @@ const Container = styled.div` `; interface QuickActionsViewProps { + currentBranch: string | null; discard: () => void; isAutocommitEnabled: boolean; isAutocommitPolling: boolean; + isBranchPopupOpen: boolean; isConnectPermitted: boolean; isDiscardLoading: boolean; isFetchStatusLoading: boolean; @@ -35,6 +38,7 @@ interface QuickActionsViewProps { isPullFailing: boolean; isPullLoading: boolean; isStatusClean: boolean; + isTriggerAutocommitLoading: boolean; pull: () => void; statusBehindCount: number; statusChangeCount: number; @@ -44,12 +48,15 @@ interface QuickActionsViewProps { open: boolean, tab: keyof typeof GitSettingsTab, ) => void; + toggleBranchPopup: (open: boolean) => void; } function QuickActionsView({ + currentBranch = null, discard = noop, isAutocommitEnabled = false, isAutocommitPolling = false, + isBranchPopupOpen = false, isConnectPermitted = false, isDiscardLoading = false, isFetchStatusLoading = false, @@ -58,9 +65,11 @@ function QuickActionsView({ isPullFailing = false, isPullLoading = false, isStatusClean = false, + isTriggerAutocommitLoading = false, pull = noop, statusBehindCount = 0, statusChangeCount = 0, + toggleBranchPopup = noop, toggleConnectModal = noop, toggleOpsModal = noop, toggleSettingsModal = noop, @@ -95,8 +104,6 @@ function QuickActionsView({ if (isProtectedMode) { discard(); } else { - // ! case: why is triggeredFromBottomBar this needed? - // pull({ triggeredFromBottomBar: true }); pull(); } } @@ -126,7 +133,16 @@ function QuickActionsView({ return isGitConnected ? ( - {/* */} + + {isAutocommitEnabled && isAutocommitPolling ? ( ) : ( diff --git a/app/client/src/git/components/QuickActions/index.tsx b/app/client/src/git/components/QuickActions/index.tsx index 88055bc5ccb4..2b03ee8bdd1d 100644 --- a/app/client/src/git/components/QuickActions/index.tsx +++ b/app/client/src/git/components/QuickActions/index.tsx @@ -1,30 +1,34 @@ import React from "react"; import QuickActionsView from "./QuickActionsView"; -import { useGitContext } from "../GitContextProvider"; import useStatusChangeCount from "./hooks/useStatusChangeCount"; import useProtectedBranches from "git/hooks/useProtectedBranches"; import useGitPermissions from "git/hooks/useGitPermissions"; import useAutocommit from "git/hooks/useAutocommit"; import useSettings from "git/hooks/useSettings"; import useMetadata from "git/hooks/useMetadata"; +import useConnect from "git/hooks/useConnect"; +import useDiscard from "git/hooks/useDiscard"; +import usePull from "git/hooks/usePull"; +import useStatus from "git/hooks/useStatus"; +import useOps from "git/hooks/useOps"; +import useBranches from "git/hooks/useBranches"; function QuickActions() { - const { - discard, - discardLoading, - fetchStatusLoading, - pull, - pullError, - pullLoading, - status, - toggleConnectModal, - toggleOpsModal, - } = useGitContext(); + const { toggleOpsModal } = useOps(); + const { isFetchStatusLoading, status } = useStatus(); + const { isPullLoading, pull, pullError } = usePull(); + const { discard, isDiscardLoading } = useDiscard(); const { isGitConnected } = useMetadata(); const { isProtectedMode } = useProtectedBranches(); const { isConnectPermitted } = useGitPermissions(); - const { isAutocommitEnabled, isAutocommitPolling } = useAutocommit(); + const { + isAutocommitEnabled, + isAutocommitPolling, + isTriggerAutocommitLoading, + } = useAutocommit(); const { toggleSettingsModal } = useSettings(); + const { toggleConnectModal } = useConnect(); + const { currentBranch, isBranchPopupOpen, toggleBranchPopup } = useBranches(); const isPullFailing = !!pullError; const isStatusClean = status?.isClean ?? false; @@ -33,20 +37,24 @@ function QuickActions() { return ( ; } -export default TabContinuosDelivery; +export default TabContinuousDelivery; diff --git a/app/client/src/git/components/StatusChanges/index.tsx b/app/client/src/git/components/StatusChanges/index.tsx index 05b8e280189b..fc39867932f8 100644 --- a/app/client/src/git/components/StatusChanges/index.tsx +++ b/app/client/src/git/components/StatusChanges/index.tsx @@ -1,13 +1,15 @@ import React from "react"; import { useGitContext } from "../GitContextProvider"; import StatusChangesView from "./StatusChangesView"; +import useStatus from "git/hooks/useStatus"; function StatusChanges() { - const { fetchStatusLoading, status, statusTransformer } = useGitContext(); + const { statusTransformer } = useGitContext(); + const { isFetchStatusLoading, status } = useStatus(); return ( diff --git a/app/client/src/git/components/index.tsx b/app/client/src/git/components/index.tsx index 1de56f66d1e0..c3a1640dbd01 100644 --- a/app/client/src/git/components/index.tsx +++ b/app/client/src/git/components/index.tsx @@ -1,2 +1,3 @@ export { default as GitModals } from "git/ee/components/GitModals"; +export { default as GitImportModal } from "./ImportModal"; 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 244f2237a760..b3950c1b2fea 100644 --- a/app/client/src/git/constants/enums.ts +++ b/app/client/src/git/constants/enums.ts @@ -4,18 +4,6 @@ export enum GitArtifactType { Workflow = "Workflow", } -export enum GitConnectStep { - Provider = "Provider", - Remote = "Remote", - SSH = "SSH", -} - -export enum GitImportStep { - Provider = "Provider", - remote = "remote", - SSH = "SSH", -} - export enum GitOpsTab { Deploy = "Deploy", Merge = "Merge", @@ -45,6 +33,7 @@ export enum MergeStatusState { } export enum GitErrorCodes { + REPO_NOT_EMPTY = "AE-GIT-4033", REPO_LIMIT_REACHED = "AE-GIT-4043", PUSH_FAILED_REMOTE_COUNTERPART_IS_AHEAD = "AE-GIT-4048", } diff --git a/app/client/src/git/hooks/useAutocommit.ts b/app/client/src/git/hooks/useAutocommit.ts index b342b7442b29..73d7d955caca 100644 --- a/app/client/src/git/hooks/useAutocommit.ts +++ b/app/client/src/git/hooks/useAutocommit.ts @@ -5,6 +5,7 @@ import { selectAutocommitEnabled, selectAutocommitPolling, selectToggleAutocommitState, + selectTriggerAutocommitState, } from "git/store/selectors/gitSingleArtifactSelectors"; import type { GitRootState } from "git/store/types"; import { useCallback } from "react"; @@ -18,6 +19,10 @@ export default function useAutocommit() { selectToggleAutocommitState(state, artifactDef), ); + const triggerAutocommitState = useSelector((state: GitRootState) => + selectTriggerAutocommitState(state, artifactDef), + ); + const toggleAutocommit = useCallback(() => { dispatch(gitArtifactActions.toggleAutocommitInit(artifactDef)); }, [artifactDef, dispatch]); @@ -50,6 +55,8 @@ export default function useAutocommit() { isToggleAutocommitLoading: toggleAutocommitState?.loading ?? false, toggleAutocommitError: toggleAutocommitState?.error ?? null, toggleAutocommit, + isTriggerAutocommitLoading: triggerAutocommitState?.loading ?? false, + triggerAutocommitError: triggerAutocommitState?.error ?? null, isAutocommitDisableModalOpen, toggleAutocommitDisableModal, isAutocommitEnabled, diff --git a/app/client/src/git/hooks/useBranches.ts b/app/client/src/git/hooks/useBranches.ts new file mode 100644 index 000000000000..4a0ecead44d7 --- /dev/null +++ b/app/client/src/git/hooks/useBranches.ts @@ -0,0 +1,125 @@ +import { useGitContext } from "git/components/GitContextProvider"; +import { gitArtifactActions } from "git/store/gitArtifactSlice"; +import { + selectFetchBranchesState, + selectBranchPopupOpen, + selectCheckoutBranchState, + selectCheckoutDestBranch, + selectCreateBranchState, + selectDeleteBranchState, + selectCurrentBranch, +} from "git/store/selectors/gitSingleArtifactSelectors"; +import type { GitRootState } from "git/store/types"; +import { useCallback } from "react"; +import { useDispatch, useSelector } from "react-redux"; + +export default function useBranches() { + const { artifactDef } = useGitContext(); + + const dispatch = useDispatch(); + + // fetch branches + const branchesState = useSelector((state: GitRootState) => + selectFetchBranchesState(state, artifactDef), + ); + const fetchBranches = useCallback(() => { + dispatch( + gitArtifactActions.fetchBranchesInit({ + ...artifactDef, + pruneBranches: true, + }), + ); + }, [artifactDef, dispatch]); + + // create branch + const createBranchState = useSelector((state: GitRootState) => + selectCreateBranchState(state, artifactDef), + ); + const createBranch = useCallback( + (branchName: string) => { + dispatch( + gitArtifactActions.createBranchInit({ + ...artifactDef, + branchName, + }), + ); + }, + [artifactDef, dispatch], + ); + // delete branch + const deleteBranchState = useSelector((state: GitRootState) => + selectDeleteBranchState(state, artifactDef), + ); + const deleteBranch = useCallback( + (branchName: string) => { + dispatch( + gitArtifactActions.deleteBranchInit({ + ...artifactDef, + branchName, + }), + ); + }, + [artifactDef, dispatch], + ); + // checkout branch + const checkoutBranchState = useSelector((state: GitRootState) => + selectCheckoutBranchState(state, artifactDef), + ); + const checkoutBranch = useCallback( + (branchName: string) => { + dispatch( + gitArtifactActions.checkoutBranchInit({ + ...artifactDef, + branchName, + }), + ); + }, + [artifactDef, dispatch], + ); + + const checkoutDestBranch = useSelector((state: GitRootState) => + selectCheckoutDestBranch(state, artifactDef), + ); + + // derived + const currentBranch = useSelector((state: GitRootState) => + selectCurrentBranch(state, artifactDef), + ); + + // git branch list popup + const isBranchPopupOpen = useSelector((state: GitRootState) => + selectBranchPopupOpen(state, artifactDef), + ); + + const toggleBranchPopup = useCallback( + (open: boolean) => { + dispatch( + gitArtifactActions.toggleBranchPopup({ + ...artifactDef, + open, + }), + ); + }, + [artifactDef, dispatch], + ); + + return { + branches: branchesState?.value, + isFetchBranchesLoading: branchesState?.loading ?? false, + fetchBranchesError: branchesState?.error ?? null, + fetchBranches, + isCreateBranchLoading: createBranchState?.loading ?? false, + createBranchError: createBranchState?.error ?? null, + createBranch, + isDeleteBranchLoading: deleteBranchState?.loading ?? false, + deleteBranchError: deleteBranchState?.error ?? null, + deleteBranch, + isCheckoutBranchLoading: checkoutBranchState?.loading ?? false, + checkoutBranchError: checkoutBranchState?.error ?? null, + checkoutBranch, + checkoutDestBranch, + currentBranch: currentBranch ?? null, + isBranchPopupOpen, + toggleBranchPopup, + }; +} diff --git a/app/client/src/git/hooks/useCommit.ts b/app/client/src/git/hooks/useCommit.ts new file mode 100644 index 000000000000..56f78cc11bcd --- /dev/null +++ b/app/client/src/git/hooks/useCommit.ts @@ -0,0 +1,39 @@ +import { useGitContext } from "git/components/GitContextProvider"; +import { gitArtifactActions } from "git/store/gitArtifactSlice"; +import { selectCommitState } from "git/store/selectors/gitSingleArtifactSelectors"; +import type { GitRootState } from "git/store/types"; +import { useCallback } from "react"; +import { useDispatch, useSelector } from "react-redux"; + +export default function useCommit() { + const { artifactDef } = useGitContext(); + const dispatch = useDispatch(); + + const commitState = useSelector((state: GitRootState) => + selectCommitState(state, artifactDef), + ); + + const commit = useCallback( + (commitMessage: string) => { + dispatch( + gitArtifactActions.commitInit({ + ...artifactDef, + commitMessage, + doPush: true, + }), + ); + }, + [artifactDef, dispatch], + ); + + const clearCommitError = useCallback(() => { + dispatch(gitArtifactActions.clearCommitError(artifactDef)); + }, [artifactDef, dispatch]); + + return { + isCommitLoading: commitState?.loading ?? false, + commitError: commitState?.error, + commit, + clearCommitError, + }; +} diff --git a/app/client/src/git/hooks/useConnect.ts b/app/client/src/git/hooks/useConnect.ts new file mode 100644 index 000000000000..1064ce7ff26e --- /dev/null +++ b/app/client/src/git/hooks/useConnect.ts @@ -0,0 +1,106 @@ +import { useGitContext } from "git/components/GitContextProvider"; +import type { ConnectRequestParams } from "git/requests/connectRequest.types"; +import { gitArtifactActions } from "git/store/gitArtifactSlice"; +import { + selectConnectModalOpen, + selectConnectState, + selectFetchSSHKeysState, + selectGenerateSSHKeyState, + selectGitImportState, +} from "git/store/selectors/gitSingleArtifactSelectors"; +import type { GitRootState } from "git/store/types"; +import { useCallback } from "react"; +import { useDispatch } from "react-redux"; +import { useSelector } from "react-redux"; + +export default function useConnect() { + const { artifactDef } = useGitContext(); + + const dispatch = useDispatch(); + + const connectState = useSelector((state: GitRootState) => + selectConnectState(state, artifactDef), + ); + + const connect = useCallback( + (params: ConnectRequestParams) => { + dispatch(gitArtifactActions.connectInit({ ...artifactDef, ...params })); + }, + [artifactDef, dispatch], + ); + + const gitImportState = useSelector((state: GitRootState) => + selectGitImportState(state, artifactDef), + ); + + const gitImport = useCallback( + (params) => { + dispatch(gitArtifactActions.gitImportInit({ ...artifactDef, ...params })); + }, + [artifactDef, dispatch], + ); + + const fetchSSHKeyState = useSelector((state: GitRootState) => + selectFetchSSHKeysState(state, artifactDef), + ); + + const fetchSSHKey = useCallback(() => { + dispatch(gitArtifactActions.fetchSSHKeyInit(artifactDef)); + }, [artifactDef, dispatch]); + + const resetFetchSSHKey = useCallback(() => { + dispatch(gitArtifactActions.resetFetchSSHKey(artifactDef)); + }, [artifactDef, dispatch]); + + const generateSSHKeyState = useSelector((state: GitRootState) => + selectGenerateSSHKeyState(state, artifactDef), + ); + + const generateSSHKey = useCallback( + (keyType: string, isImport: boolean = false) => { + dispatch( + gitArtifactActions.generateSSHKeyInit({ + ...artifactDef, + keyType, + isImport, + }), + ); + }, + [artifactDef, dispatch], + ); + + const resetGenerateSSHKey = useCallback(() => { + dispatch(gitArtifactActions.resetGenerateSSHKey(artifactDef)); + }, [artifactDef, dispatch]); + + const isConnectModalOpen = useSelector((state: GitRootState) => + selectConnectModalOpen(state, artifactDef), + ); + + const toggleConnectModal = useCallback( + (open: boolean) => { + dispatch(gitArtifactActions.toggleConnectModal({ ...artifactDef, open })); + }, + [artifactDef, dispatch], + ); + + return { + isConnectLoading: connectState?.loading ?? false, + connectError: connectState?.error ?? null, + connect, + isGitImportLoading: gitImportState?.loading ?? false, + gitImportError: gitImportState?.error ?? null, + gitImport, + sshKey: fetchSSHKeyState?.value ?? null, + isFetchSSHKeyLoading: fetchSSHKeyState?.loading ?? false, + fetchSSHKeyError: fetchSSHKeyState?.error ?? null, + fetchSSHKey, + resetFetchSSHKey, + isGenerateSSHKeyLoading: generateSSHKeyState?.loading ?? false, + generateSSHKeyError: generateSSHKeyState?.error ?? null, + generateSSHKey, + resetGenerateSSHKey, + isConnectModalOpen, + toggleConnectModal, + }; +} diff --git a/app/client/src/git/hooks/useDiscard.ts b/app/client/src/git/hooks/useDiscard.ts new file mode 100644 index 000000000000..2f71c11608f9 --- /dev/null +++ b/app/client/src/git/hooks/useDiscard.ts @@ -0,0 +1,30 @@ +import { useGitContext } from "git/components/GitContextProvider"; +import { gitArtifactActions } from "git/store/gitArtifactSlice"; +import { selectDiscardState } from "git/store/selectors/gitSingleArtifactSelectors"; +import type { GitRootState } from "git/store/types"; +import { useCallback } from "react"; +import { useDispatch, useSelector } from "react-redux"; + +export default function useDiscard() { + const { artifactDef } = useGitContext(); + const dispatch = useDispatch(); + + const discardState = useSelector((state: GitRootState) => + selectDiscardState(state, artifactDef), + ); + + const discard = useCallback(() => { + dispatch(gitArtifactActions.discardInit(artifactDef)); + }, [artifactDef, dispatch]); + + const clearDiscardError = useCallback(() => { + dispatch(gitArtifactActions.clearDiscardError(artifactDef)); + }, [artifactDef, dispatch]); + + return { + isDiscardLoading: discardState?.loading ?? false, + discardError: discardState?.error ?? null, + discard, + clearDiscardError, + }; +} diff --git a/app/client/src/git/hooks/useMerge.ts b/app/client/src/git/hooks/useMerge.ts new file mode 100644 index 000000000000..f7394601b191 --- /dev/null +++ b/app/client/src/git/hooks/useMerge.ts @@ -0,0 +1,58 @@ +import { useGitContext } from "git/components/GitContextProvider"; +import { gitArtifactActions } from "git/store/gitArtifactSlice"; +import { + selectMergeState, + selectMergeStatusState, +} from "git/store/selectors/gitSingleArtifactSelectors"; +import type { GitRootState } from "git/store/types"; +import { useCallback } from "react"; +import { useDispatch, useSelector } from "react-redux"; + +export default function useMerge() { + const { artifact, artifactDef } = useGitContext(); + const artifactId = artifact?.id; + const dispatch = useDispatch(); + + // merge + const mergeState = useSelector((state: GitRootState) => + selectMergeState(state, artifactDef), + ); + + const merge = useCallback(() => { + dispatch(gitArtifactActions.mergeInit(artifactDef)); + }, [artifactDef, dispatch]); + + // merge status + const mergeStatusState = useSelector((state: GitRootState) => + selectMergeStatusState(state, artifactDef), + ); + + const fetchMergeStatus = useCallback( + (sourceBranch: string, destinationBranch: string) => { + dispatch( + gitArtifactActions.fetchMergeStatusInit({ + ...artifactDef, + artifactId: artifactId ?? "", + sourceBranch, + destinationBranch, + }), + ); + }, + [artifactId, artifactDef, dispatch], + ); + + const clearMergeStatus = useCallback(() => { + dispatch(gitArtifactActions.clearMergeStatus(artifactDef)); + }, [artifactDef, dispatch]); + + return { + isMergeLoading: mergeState?.loading ?? false, + mergeError: mergeState?.error, + merge, + mergeStatus: mergeStatusState?.value, + isFetchMergeStatusLoading: mergeStatusState?.loading ?? false, + fetchMergeStatusError: mergeStatusState?.error, + fetchMergeStatus, + clearMergeStatus, + }; +} diff --git a/app/client/src/git/hooks/useMetadata.ts b/app/client/src/git/hooks/useMetadata.ts index 15ed4346dea3..9bd5356ad763 100644 --- a/app/client/src/git/hooks/useMetadata.ts +++ b/app/client/src/git/hooks/useMetadata.ts @@ -18,9 +18,9 @@ export default function useMetadata() { ); return { - metadata: metadataState.value ?? null, - isFetchMetadataLoading: metadataState.loading ?? false, - fetchMetadataError: metadataState.error ?? null, + metadata: metadataState?.value ?? null, + isFetchMetadataLoading: metadataState?.loading ?? false, + fetchMetadataError: metadataState?.error ?? null, isGitConnected, }; } diff --git a/app/client/src/git/hooks/useOps.ts b/app/client/src/git/hooks/useOps.ts new file mode 100644 index 000000000000..32eec997cf25 --- /dev/null +++ b/app/client/src/git/hooks/useOps.ts @@ -0,0 +1,57 @@ +import { useGitContext } from "git/components/GitContextProvider"; +import { GitOpsTab } from "git/constants/enums"; +import { gitArtifactActions } from "git/store/gitArtifactSlice"; +import { + selectConflictErrorModalOpen, + selectOpsModalOpen, + selectOpsModalTab, +} from "git/store/selectors/gitSingleArtifactSelectors"; +import type { GitRootState } from "git/store/types"; +import { useCallback } from "react"; +import { useDispatch, useSelector } from "react-redux"; + +export default function useOps() { + const { artifactDef } = useGitContext(); + + const dispatch = useDispatch(); + + // ops modal + const opsModalOpen = useSelector((state: GitRootState) => + selectOpsModalOpen(state, artifactDef), + ); + + const opsModalTab = useSelector((state: GitRootState) => + selectOpsModalTab(state, artifactDef), + ); + + const toggleOpsModal = useCallback( + (open: boolean, tab: keyof typeof GitOpsTab = GitOpsTab.Deploy) => { + dispatch( + gitArtifactActions.toggleOpsModal({ ...artifactDef, open, tab }), + ); + }, + [artifactDef, dispatch], + ); + + // conflict error modal + const conflictErrorModalOpen = useSelector((state: GitRootState) => + selectConflictErrorModalOpen(state, artifactDef), + ); + + const toggleConflictErrorModal = useCallback( + (open: boolean) => { + dispatch( + gitArtifactActions.toggleConflictErrorModal({ ...artifactDef, open }), + ); + }, + [artifactDef, dispatch], + ); + + return { + opsModalTab, + opsModalOpen, + toggleOpsModal, + conflictErrorModalOpen, + toggleConflictErrorModal, + }; +} diff --git a/app/client/src/git/hooks/useProtectedBranches.ts b/app/client/src/git/hooks/useProtectedBranches.ts index 3d26df24066e..fa3b2ef89307 100644 --- a/app/client/src/git/hooks/useProtectedBranches.ts +++ b/app/client/src/git/hooks/useProtectedBranches.ts @@ -6,6 +6,7 @@ import { selectUpdateProtectedBranchesState, } from "git/store/selectors/gitSingleArtifactSelectors"; import type { GitRootState } from "git/store/types"; +import { useCallback } from "react"; import { useDispatch, useSelector } from "react-redux"; function useProtectedBranches() { @@ -17,22 +18,25 @@ function useProtectedBranches() { selectFetchProtectedBranchesState(state, artifactDef), ); - const fetchProtectedBranches = () => { + const fetchProtectedBranches = useCallback(() => { dispatch(gitArtifactActions.fetchProtectedBranchesInit(artifactDef)); - }; + }, [dispatch, artifactDef]); const updateProtectedBranchesState = useSelector((state: GitRootState) => selectUpdateProtectedBranchesState(state, artifactDef), ); - const updateProtectedBranches = (branches: string[]) => { - dispatch( - gitArtifactActions.updateProtectedBranchesInit({ - ...artifactDef, - branchNames: branches, - }), - ); - }; + const updateProtectedBranches = useCallback( + (branches: string[]) => { + dispatch( + gitArtifactActions.updateProtectedBranchesInit({ + ...artifactDef, + branchNames: branches, + }), + ); + }, + [dispatch, artifactDef], + ); const isProtectedMode = useSelector((state: GitRootState) => selectProtectedMode(state, artifactDef), diff --git a/app/client/src/git/hooks/usePull.ts b/app/client/src/git/hooks/usePull.ts new file mode 100644 index 000000000000..992866f2c277 --- /dev/null +++ b/app/client/src/git/hooks/usePull.ts @@ -0,0 +1,31 @@ +import { useGitContext } from "git/components/GitContextProvider"; +import { gitArtifactActions } from "git/store/gitArtifactSlice"; +import { selectPullState } from "git/store/selectors/gitSingleArtifactSelectors"; +import type { GitRootState } from "git/store/types"; +import { useCallback } from "react"; +import { useDispatch, useSelector } from "react-redux"; + +export default function usePull() { + const { artifact, artifactDef } = useGitContext(); + const artifactId = artifact?.id; + const dispatch = useDispatch(); + + const pullState = useSelector((state: GitRootState) => + selectPullState(state, artifactDef), + ); + + const pull = useCallback(() => { + dispatch( + gitArtifactActions.pullInit({ + ...artifactDef, + artifactId: artifactId ?? "", + }), + ); + }, [artifactDef, artifactId, dispatch]); + + return { + isPullLoading: pullState?.loading ?? false, + pullError: pullState?.error, + pull, + }; +} diff --git a/app/client/src/git/hooks/useStatus.ts b/app/client/src/git/hooks/useStatus.ts new file mode 100644 index 000000000000..97b09b57498a --- /dev/null +++ b/app/client/src/git/hooks/useStatus.ts @@ -0,0 +1,31 @@ +import { useGitContext } from "git/components/GitContextProvider"; +import { gitArtifactActions } from "git/store/gitArtifactSlice"; +import { selectStatusState } from "git/store/selectors/gitSingleArtifactSelectors"; +import type { GitRootState } from "git/store/types"; +import { useCallback } from "react"; +import { useDispatch, useSelector } from "react-redux"; + +export default function useStatus() { + const { artifactDef } = useGitContext(); + const dispatch = useDispatch(); + + const statusState = useSelector((state: GitRootState) => + selectStatusState(state, artifactDef), + ); + + const fetchStatus = useCallback(() => { + dispatch( + gitArtifactActions.fetchStatusInit({ + ...artifactDef, + compareRemote: true, + }), + ); + }, [artifactDef, dispatch]); + + return { + status: statusState?.value, + isFetchStatusLoading: statusState?.loading ?? false, + fetchStatusError: statusState?.error, + fetchStatus, + }; +} diff --git a/app/client/src/git/requests/fetchSSHKeyRequest.types.ts b/app/client/src/git/requests/fetchSSHKeyRequest.types.ts index 55b4f305b666..b507a15c8077 100644 --- a/app/client/src/git/requests/fetchSSHKeyRequest.types.ts +++ b/app/client/src/git/requests/fetchSSHKeyRequest.types.ts @@ -1,6 +1,10 @@ -export interface FetchSSHKeyResponse { +import type { ApiResponse } from "api/types"; + +export interface FetchSSHKeyResponseData { publicKey: string; docUrl: string; isRegeneratedKey: boolean; regeneratedKey: boolean; } + +export type FetchSSHKeyResponse = ApiResponse; diff --git a/app/client/src/git/requests/generateSSHKeyRequest.ts b/app/client/src/git/requests/generateSSHKeyRequest.ts index 7c747f94371e..896426347f30 100644 --- a/app/client/src/git/requests/generateSSHKeyRequest.ts +++ b/app/client/src/git/requests/generateSSHKeyRequest.ts @@ -10,9 +10,9 @@ export default async function generateSSHKeyRequest( baseApplicationId: string, params: GenerateSSHKeyRequestParams, ): AxiosPromise { - const url = params.isImporting + const url = params.isImport ? `${GIT_BASE_URL}/import/keys?keyType=${params.keyType}` : `${APPLICATION_BASE_URL}/ssh-keypair/${baseApplicationId}?keyType=${params.keyType}`; - return params.isImporting ? Api.get(url) : Api.post(url); + return params.isImport ? Api.get(url) : Api.post(url); } diff --git a/app/client/src/git/requests/generateSSHKeyRequest.types.ts b/app/client/src/git/requests/generateSSHKeyRequest.types.ts index ced29ad6dbc0..45374e42d5b1 100644 --- a/app/client/src/git/requests/generateSSHKeyRequest.types.ts +++ b/app/client/src/git/requests/generateSSHKeyRequest.types.ts @@ -1,11 +1,15 @@ +import type { ApiResponse } from "api/types"; + export interface GenerateSSHKeyRequestParams { keyType: string; - isImporting: boolean; + isImport: boolean; } -export interface GenerateSSHKeyResponse { +export interface GenerateSSHKeyResponseData { publicKey: string; docUrl: string; isRegeneratedKey: boolean; regeneratedKey: boolean; } + +export type GenerateSSHKeyResponse = ApiResponse; diff --git a/app/client/src/git/requests/importGitRequest.ts b/app/client/src/git/requests/gitImportRequest.ts similarity index 52% rename from app/client/src/git/requests/importGitRequest.ts rename to app/client/src/git/requests/gitImportRequest.ts index 522cd187ebe3..94ad9b5d95c9 100644 --- a/app/client/src/git/requests/importGitRequest.ts +++ b/app/client/src/git/requests/gitImportRequest.ts @@ -1,14 +1,14 @@ import Api from "api/Api"; import { GIT_BASE_URL } from "./constants"; import type { - ImportGitRequestParams, - ImportGitResponse, -} from "./importGitRequest.types"; + GitImportRequestParams, + GitImportResponse, +} from "./gitImportRequest.types"; import type { AxiosPromise } from "axios"; -export default async function importGitRequest( +export default async function gitImportRequest( workspaceId: string, - params: ImportGitRequestParams, -): AxiosPromise { + params: GitImportRequestParams, +): AxiosPromise { return Api.post(`${GIT_BASE_URL}/import/${workspaceId}`, params); } diff --git a/app/client/src/git/requests/importGitRequest.types.ts b/app/client/src/git/requests/gitImportRequest.types.ts similarity index 69% rename from app/client/src/git/requests/importGitRequest.types.ts rename to app/client/src/git/requests/gitImportRequest.types.ts index b0f3113d7a6e..9f76b379c9cc 100644 --- a/app/client/src/git/requests/importGitRequest.types.ts +++ b/app/client/src/git/requests/gitImportRequest.types.ts @@ -1,4 +1,6 @@ -export interface ImportGitRequestParams { +import type { ApiResponse } from "api/types"; + +export interface GitImportRequestParams { remoteUrl: string; gitProfile?: { authorName: string; @@ -7,7 +9,7 @@ export interface ImportGitRequestParams { }; } -export interface ImportGitResponse { +export interface GitImportResponseData { id: string; baseId: string; gitApplicationMetadata: { @@ -22,3 +24,5 @@ export interface ImportGitResponse { repoName: string; }; } + +export type GitImportResponse = ApiResponse; diff --git a/app/client/src/git/sagas/checkoutBranchSaga.ts b/app/client/src/git/sagas/checkoutBranchSaga.ts index 18eaea3532ab..34e48434626c 100644 --- a/app/client/src/git/sagas/checkoutBranchSaga.ts +++ b/app/client/src/git/sagas/checkoutBranchSaga.ts @@ -48,7 +48,7 @@ export default function* checkoutBranchSaga( ); yield put( - gitArtifactActions.toggleBranchListPopup({ + gitArtifactActions.toggleBranchPopup({ ...basePayload, open: false, }), diff --git a/app/client/src/git/sagas/connectSaga.ts b/app/client/src/git/sagas/connectSaga.ts index b1bfa286e778..b13c2f6c2c2a 100644 --- a/app/client/src/git/sagas/connectSaga.ts +++ b/app/client/src/git/sagas/connectSaga.ts @@ -53,6 +53,13 @@ export default function* connectSaga( history.replace(newUrl); // ! case for updating lastDeployedAt in application manually? } + + yield put( + gitArtifactActions.initGitForEditor({ + ...basePayload, + artifact: response.data, + }), + ); } } catch (e) { if (response && response.responseMeta.error) { @@ -67,12 +74,7 @@ export default function* connectSaga( ); } - yield put( - gitArtifactActions.connectError({ - ...basePayload, - error, - }), - ); + yield put(gitArtifactActions.connectError({ ...basePayload, error })); } else { log.error(e); captureException(e); diff --git a/app/client/src/git/sagas/createBranchSaga.ts b/app/client/src/git/sagas/createBranchSaga.ts index 99604bd86602..7796f43d74bd 100644 --- a/app/client/src/git/sagas/createBranchSaga.ts +++ b/app/client/src/git/sagas/createBranchSaga.ts @@ -30,6 +30,12 @@ export default function* createBranchSaga( if (isValidResponse) { yield put(gitArtifactActions.createBranchSuccess(basePayload)); + yield put( + gitArtifactActions.toggleBranchPopup({ + ...basePayload, + open: false, + }), + ); yield put( gitArtifactActions.fetchBranchesInit({ ...basePayload, diff --git a/app/client/src/git/sagas/fetchSSHKeySaga.ts b/app/client/src/git/sagas/fetchSSHKeySaga.ts new file mode 100644 index 000000000000..02f797edae66 --- /dev/null +++ b/app/client/src/git/sagas/fetchSSHKeySaga.ts @@ -0,0 +1,37 @@ +import { captureException } from "@sentry/react"; +import fetchSSHKeyRequest from "git/requests/fetchSSHKeyRequest"; +import type { FetchSSHKeyResponse } from "git/requests/fetchSSHKeyRequest.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"; + +export function* fetchSSHKeySaga(action: GitArtifactPayloadAction) { + const { artifactType, baseArtifactId } = action.payload; + const artifactDef = { artifactType, baseArtifactId }; + let response: FetchSSHKeyResponse | undefined; + + try { + response = yield call(fetchSSHKeyRequest, baseArtifactId); + const isValidResponse: boolean = yield validateResponse(response, false); + + if (response && isValidResponse) { + yield put( + gitArtifactActions.fetchSSHKeySuccess({ + ...artifactDef, + responseData: response.data, + }), + ); + } + } catch (e) { + if (response && response.responseMeta.error) { + const { error } = response.responseMeta; + + yield put(gitArtifactActions.fetchSSHKeyError({ ...artifactDef, error })); + } else { + log.error(e); + captureException(e); + } + } +} diff --git a/app/client/src/git/sagas/generateSSHKeySaga.ts b/app/client/src/git/sagas/generateSSHKeySaga.ts new file mode 100644 index 000000000000..09773f9dc0d4 --- /dev/null +++ b/app/client/src/git/sagas/generateSSHKeySaga.ts @@ -0,0 +1,60 @@ +import { captureException } from "@sentry/react"; +import { GitErrorCodes } from "git/constants/enums"; +import generateSSHKeyRequest from "git/requests/generateSSHKeyRequest"; +import type { + GenerateSSHKeyRequestParams, + GenerateSSHKeyResponse, +} from "git/requests/generateSSHKeyRequest.types"; +import type { GenerateSSHKeyInitPayload } from "git/store/actions/generateSSHKeyActions"; +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 function* generateSSHKeySaga( + action: GitArtifactPayloadAction, +) { + const { artifactType, baseArtifactId } = action.payload; + const artifactDef = { artifactType, baseArtifactId }; + let response: GenerateSSHKeyResponse | undefined; + + try { + const params: GenerateSSHKeyRequestParams = { + keyType: action.payload.keyType, + isImport: action.payload.isImport, + }; + + response = yield call(generateSSHKeyRequest, baseArtifactId, params); + const isValidResponse: boolean = yield validateResponse(response); + + if (response && isValidResponse) { + yield put( + gitArtifactActions.generateSSHKeySuccess({ + ...artifactDef, + responseData: response.data, + }), + ); + } + } catch (e) { + if (response && response.responseMeta.error) { + const { error } = response.responseMeta; + + if (GitErrorCodes.REPO_LIMIT_REACHED === error.code) { + yield put( + gitArtifactActions.toggleRepoLimitErrorModal({ + ...artifactDef, + open: true, + }), + ); + } + + yield put( + gitArtifactActions.generateSSHKeyError({ ...artifactDef, error }), + ); + } else { + log.error(e); + captureException(e); + } + } +} diff --git a/app/client/src/git/sagas/index.ts b/app/client/src/git/sagas/index.ts index 4b2ca0720b0d..13e19bd4c9f8 100644 --- a/app/client/src/git/sagas/index.ts +++ b/app/client/src/git/sagas/index.ts @@ -27,6 +27,11 @@ import updateProtectedBranchesSaga from "./updateProtectedBranchesSaga"; import fetchMetadataSaga from "./fetchMetadataSaga"; import toggleAutocommitSaga from "./toggleAutocommitSaga"; import disconnectSaga from "./disconnectSaga"; +import { fetchSSHKeySaga } from "./fetchSSHKeySaga"; +import { generateSSHKeySaga } from "./generateSSHKeySaga"; +import createBranchSaga from "./createBranchSaga"; +import deleteBranchSaga from "./deleteBranchSaga"; +import checkoutBranchSaga from "./checkoutBranchSaga"; import { blockingActionSagas as blockingActionSagasExtended, @@ -53,6 +58,9 @@ const blockingActionSagas: Record< // branches [gitArtifactActions.fetchBranchesInit.type]: fetchBranchesSaga, + [gitArtifactActions.createBranchInit.type]: createBranchSaga, + [gitArtifactActions.deleteBranchInit.type]: deleteBranchSaga, + [gitArtifactActions.checkoutBranchInit.type]: checkoutBranchSaga, // user profiles [gitArtifactActions.fetchLocalProfileInit.type]: fetchLocalProfileSaga, @@ -82,6 +90,10 @@ const nonBlockingActionSagas: Record< [gitArtifactActions.initGitForEditor.type]: initGitForEditorSaga, [gitArtifactActions.toggleAutocommitInit.type]: toggleAutocommitSaga, + // ssh key + [gitArtifactActions.fetchSSHKeyInit.type]: fetchSSHKeySaga, + [gitArtifactActions.generateSSHKeyInit.type]: generateSSHKeySaga, + // EE ...nonBlockingActionSagasExtended, }; diff --git a/app/client/src/git/sagas/initGitSaga.ts b/app/client/src/git/sagas/initGitSaga.ts index 2c62fd31851b..aa50efce17b1 100644 --- a/app/client/src/git/sagas/initGitSaga.ts +++ b/app/client/src/git/sagas/initGitSaga.ts @@ -13,7 +13,7 @@ export default function* initGitForEditorSaga( yield put(gitArtifactActions.mount(basePayload)); if (artifactType === GitArtifactType.Application) { - if (!!artifact.gitApplicationMetadata) { + if (!!artifact.gitApplicationMetadata?.remoteUrl) { yield put(gitArtifactActions.fetchMetadataInit(basePayload)); yield take(gitArtifactActions.fetchMetadataSuccess.type); yield put( diff --git a/app/client/src/git/store/actions/checkoutBranchActions.ts b/app/client/src/git/store/actions/checkoutBranchActions.ts index c46edf84baff..c771d1887e34 100644 --- a/app/client/src/git/store/actions/checkoutBranchActions.ts +++ b/app/client/src/git/store/actions/checkoutBranchActions.ts @@ -6,9 +6,10 @@ export interface CheckoutBranchInitPayload extends CheckoutBranchRequestParams {} export const checkoutBranchInitAction = - createSingleArtifactAction((state) => { + createSingleArtifactAction((state, action) => { state.apiResponses.checkoutBranch.loading = true; state.apiResponses.checkoutBranch.error = null; + state.ui.checkoutDestBranch = action.payload.branchName; return state; }); @@ -16,6 +17,8 @@ export const checkoutBranchInitAction = export const checkoutBranchSuccessAction = createSingleArtifactAction( (state) => { state.apiResponses.checkoutBranch.loading = false; + state.apiResponses.checkoutBranch.error = null; + state.ui.checkoutDestBranch = null; return state; }, @@ -27,6 +30,7 @@ export const checkoutBranchErrorAction = state.apiResponses.checkoutBranch.loading = false; state.apiResponses.checkoutBranch.error = error; + state.ui.checkoutDestBranch = null; return state; }); diff --git a/app/client/src/git/store/actions/fetchSSHKeyActions.ts b/app/client/src/git/store/actions/fetchSSHKeyActions.ts index b802160ce0af..828cf6ebcb75 100644 --- a/app/client/src/git/store/actions/fetchSSHKeyActions.ts +++ b/app/client/src/git/store/actions/fetchSSHKeyActions.ts @@ -1,9 +1,6 @@ -import type { - GitArtifactPayloadAction, - GitArtifactErrorPayloadAction, - GitSSHKey, -} from "../types"; +import type { GitAsyncErrorPayload, GitAsyncSuccessPayload } from "../types"; import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction"; +import type { FetchSSHKeyResponseData } from "git/requests/fetchSSHKeyRequest.types"; export const fetchSSHKeyInitAction = createSingleArtifactAction((state) => { state.apiResponses.sshKey.loading = true; @@ -12,22 +9,30 @@ export const fetchSSHKeyInitAction = createSingleArtifactAction((state) => { return state; }); -export const fetchSSHKeySuccessAction = createSingleArtifactAction( - (state, action: GitArtifactPayloadAction<{ sshKey: GitSSHKey }>) => { - state.apiResponses.sshKey.loading = false; - state.apiResponses.sshKey.value = action.payload.sshKey; +export const fetchSSHKeySuccessAction = createSingleArtifactAction< + GitAsyncSuccessPayload +>((state, action) => { + state.apiResponses.sshKey.loading = false; + state.apiResponses.sshKey.error = null; + state.apiResponses.sshKey.value = action.payload.responseData; - return state; - }, -); + return state; +}); -export const fetchSSHKeyErrorAction = createSingleArtifactAction( - (state, action: GitArtifactErrorPayloadAction) => { +export const fetchSSHKeyErrorAction = + createSingleArtifactAction((state, action) => { const { error } = action.payload; state.apiResponses.sshKey.loading = false; state.apiResponses.sshKey.error = error; return state; - }, -); + }); + +export const resetFetchSSHKeyAction = createSingleArtifactAction((state) => { + state.apiResponses.sshKey.loading = false; + state.apiResponses.sshKey.error = null; + state.apiResponses.sshKey.value = null; + + return state; +}); diff --git a/app/client/src/git/store/actions/generateSSHKey.ts b/app/client/src/git/store/actions/generateSSHKey.ts deleted file mode 100644 index 52698c65a8e1..000000000000 --- a/app/client/src/git/store/actions/generateSSHKey.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction"; -import type { GitArtifactErrorPayloadAction } from "../types"; - -export const generateSSHKeyInitAction = createSingleArtifactAction((state) => { - state.apiResponses.generateSSHKey.loading = true; - state.apiResponses.generateSSHKey.error = null; - - return state; -}); - -export const generateSSHKeySuccessAction = createSingleArtifactAction( - (state) => { - state.apiResponses.generateSSHKey.loading = false; - - return state; - }, -); - -export const generateSSHKeyErrorAction = createSingleArtifactAction( - (state, action: GitArtifactErrorPayloadAction) => { - const { error } = action.payload; - - state.apiResponses.generateSSHKey.loading = false; - state.apiResponses.generateSSHKey.error = error; - - return state; - }, -); diff --git a/app/client/src/git/store/actions/generateSSHKeyActions.ts b/app/client/src/git/store/actions/generateSSHKeyActions.ts new file mode 100644 index 000000000000..fa70c3a1ba28 --- /dev/null +++ b/app/client/src/git/store/actions/generateSSHKeyActions.ts @@ -0,0 +1,44 @@ +import type { + GenerateSSHKeyRequestParams, + GenerateSSHKeyResponseData, +} from "git/requests/generateSSHKeyRequest.types"; +import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction"; +import type { GitAsyncErrorPayload, GitAsyncSuccessPayload } from "../types"; + +export interface GenerateSSHKeyInitPayload + extends GenerateSSHKeyRequestParams {} + +export const generateSSHKeyInitAction = + createSingleArtifactAction((state) => { + state.apiResponses.generateSSHKey.loading = true; + state.apiResponses.generateSSHKey.error = null; + + return state; + }); + +export const generateSSHKeySuccessAction = createSingleArtifactAction< + GitAsyncSuccessPayload +>((state, action) => { + state.apiResponses.generateSSHKey.loading = false; + state.apiResponses.generateSSHKey.error = null; + state.apiResponses.sshKey.value = action.payload.responseData; + + return state; +}); + +export const generateSSHKeyErrorAction = + createSingleArtifactAction((state, action) => { + const { error } = action.payload; + + state.apiResponses.generateSSHKey.loading = false; + state.apiResponses.generateSSHKey.error = error; + + return state; + }); + +export const resetGenerateSSHKeyAction = createSingleArtifactAction((state) => { + state.apiResponses.generateSSHKey.loading = false; + state.apiResponses.generateSSHKey.error = null; + + return state; +}); diff --git a/app/client/src/git/store/actions/gitImportActions.ts b/app/client/src/git/store/actions/gitImportActions.ts new file mode 100644 index 000000000000..49411b55dd55 --- /dev/null +++ b/app/client/src/git/store/actions/gitImportActions.ts @@ -0,0 +1,25 @@ +import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction"; +import type { GitAsyncErrorPayload } from "../types"; + +export const gitImportInitAction = createSingleArtifactAction((state) => { + state.apiResponses.gitImport.loading = true; + state.apiResponses.gitImport.error = null; + + return state; +}); + +export const gitImportSuccessAction = createSingleArtifactAction((state) => { + state.apiResponses.gitImport.loading = false; + + return state; +}); + +export const gitImportErrorAction = + createSingleArtifactAction((state, action) => { + const { error } = action.payload; + + state.apiResponses.gitImport.loading = false; + state.apiResponses.gitImport.error = error; + + return state; + }); diff --git a/app/client/src/git/store/actions/repoLimitErrorModalActions.ts b/app/client/src/git/store/actions/repoLimitErrorModalActions.ts index d79b29d61d15..b1867c1d5926 100644 --- a/app/client/src/git/store/actions/repoLimitErrorModalActions.ts +++ b/app/client/src/git/store/actions/repoLimitErrorModalActions.ts @@ -9,7 +9,7 @@ export const toggleRepoLimitErrorModalAction = (state, action) => { const { open } = action.payload; - state.ui.repoLimitErrorModal.open = open; + state.ui.repoLimitErrorModalOpen = open; return state; }, diff --git a/app/client/src/git/store/actions/uiActions.ts b/app/client/src/git/store/actions/uiActions.ts index 6780ee64a4bb..577d1dfa3d10 100644 --- a/app/client/src/git/store/actions/uiActions.ts +++ b/app/client/src/git/store/actions/uiActions.ts @@ -10,7 +10,7 @@ export const toggleConnectModalAction = createSingleArtifactAction((state, action) => { const { open } = action.payload; - state.ui.connectModal.open = open; + state.ui.connectModalOpen = open; return state; }); @@ -87,15 +87,15 @@ export const toggleAutocommitDisableModalAction = ); // branch popup -interface BranchListPopupPayload { +interface BranchPopupPayload { open: boolean; } -export const toggleBranchListPopupAction = - createSingleArtifactAction((state, action) => { +export const toggleBranchPopupAction = + createSingleArtifactAction((state, action) => { const { open } = action.payload; - state.ui.branchListPopup.open = open; + state.ui.branchPopupOpen = open; return state; }); @@ -109,7 +109,7 @@ export const toggleRepoLimitErrorModalAction = createSingleArtifactAction((state, action) => { const { open } = action.payload; - state.ui.repoLimitErrorModal.open = open; + state.ui.repoLimitErrorModalOpen = open; return state; }); diff --git a/app/client/src/git/store/gitArtifactSlice.ts b/app/client/src/git/store/gitArtifactSlice.ts index 75c3e64403b1..924785f80c76 100644 --- a/app/client/src/git/store/gitArtifactSlice.ts +++ b/app/client/src/git/store/gitArtifactSlice.ts @@ -53,7 +53,7 @@ import { deleteBranchSuccessAction, } from "./actions/deleteBranchActions"; import { - toggleBranchListPopupAction, + toggleBranchPopupAction, toggleConnectModalAction, toggleOpsModalAction, toggleSettingsModalAction, @@ -119,6 +119,23 @@ import { disconnectInitAction, disconnectSuccessAction, } from "./actions/disconnectActions"; +import { + gitImportErrorAction, + gitImportInitAction, + gitImportSuccessAction, +} from "./actions/gitImportActions"; +import { + fetchSSHKeyErrorAction, + fetchSSHKeyInitAction, + fetchSSHKeySuccessAction, + resetFetchSSHKeyAction, +} from "./actions/fetchSSHKeyActions"; +import { + generateSSHKeyErrorAction, + generateSSHKeyInitAction, + generateSSHKeySuccessAction, + resetGenerateSSHKeyAction, +} from "./actions/generateSSHKeyActions"; const initialState: GitArtifactReduxState = {}; @@ -139,6 +156,17 @@ export const gitArtifactSlice = createSlice({ connectInit: connectInitAction, connectSuccess: connectSuccessAction, connectError: connectErrorAction, + gitImportInit: gitImportInitAction, + gitImportSuccess: gitImportSuccessAction, + gitImportError: gitImportErrorAction, + fetchSSHKeyInit: fetchSSHKeyInitAction, + fetchSSHKeySuccess: fetchSSHKeySuccessAction, + fetchSSHKeyError: fetchSSHKeyErrorAction, + resetFetchSSHKey: resetFetchSSHKeyAction, + generateSSHKeyInit: generateSSHKeyInitAction, + generateSSHKeySuccess: generateSSHKeySuccessAction, + generateSSHKeyError: generateSSHKeyErrorAction, + resetGenerateSSHKey: resetGenerateSSHKeyAction, disconnectInit: disconnectInitAction, disconnectSuccess: disconnectSuccessAction, disconnectError: disconnectErrorAction, @@ -185,7 +213,7 @@ export const gitArtifactSlice = createSlice({ checkoutBranchInit: checkoutBranchInitAction, checkoutBranchSuccess: checkoutBranchSuccessAction, checkoutBranchError: checkoutBranchErrorAction, - toggleBranchListPopup: toggleBranchListPopupAction, + toggleBranchPopup: toggleBranchPopupAction, // settings toggleSettingsModal: toggleSettingsModalAction, diff --git a/app/client/src/git/store/helpers/gitSingleArtifactInitialState.ts b/app/client/src/git/store/helpers/gitSingleArtifactInitialState.ts index 605ed2748668..b17c93a126cb 100644 --- a/app/client/src/git/store/helpers/gitSingleArtifactInitialState.ts +++ b/app/client/src/git/store/helpers/gitSingleArtifactInitialState.ts @@ -2,12 +2,7 @@ import { gitArtifactAPIResponsesInitialState as gitArtifactAPIResponsesInitialStateExtended, gitArtifactUIInitialState as gitArtifactUIInitialStateExtended, } from "git/ee/store/helpers/initialState"; -import { - GitConnectStep, - GitImportStep, - GitOpsTab, - GitSettingsTab, -} from "../../constants/enums"; +import { GitOpsTab, GitSettingsTab } from "../../constants/enums"; import type { GitSingleArtifactAPIResponsesReduxState, GitSingleArtifactUIReduxState, @@ -15,19 +10,11 @@ import type { } from "../types"; const gitSingleArtifactInitialUIState: GitSingleArtifactUIReduxState = { - connectModal: { - open: false, - step: GitConnectStep.Provider, - }, + connectModalOpen: false, disconnectBaseArtifactId: null, disconnectArtifactName: null, - importModal: { - open: false, - step: GitImportStep.Provider, - }, - branchListPopup: { - open: false, - }, + branchPopupOpen: false, + checkoutDestBranch: null, opsModalOpen: false, opsModalTab: GitOpsTab.Deploy, settingsModalOpen: false, @@ -35,9 +22,7 @@ const gitSingleArtifactInitialUIState: GitSingleArtifactUIReduxState = { autocommitDisableModalOpen: false, autocommitPolling: false, conflictErrorModalOpen: false, - repoLimitErrorModal: { - open: false, - }, + repoLimitErrorModalOpen: false, // EE ...gitArtifactUIInitialStateExtended, }; @@ -53,6 +38,10 @@ const gitSingleArtifactInitialAPIResponses: GitSingleArtifactAPIResponsesReduxSt loading: false, error: null, }, + gitImport: { + loading: false, + error: null, + }, status: { value: null, loading: false, diff --git a/app/client/src/git/store/selectors/gitSingleArtifactSelectors.ts b/app/client/src/git/store/selectors/gitSingleArtifactSelectors.ts index a42338ee507c..845a2cfe41fe 100644 --- a/app/client/src/git/store/selectors/gitSingleArtifactSelectors.ts +++ b/app/client/src/git/store/selectors/gitSingleArtifactSelectors.ts @@ -27,6 +27,31 @@ export const selectGitConnected = ( ) => !!selectMetadataState(state, artifactDef)?.value; // CONNECT +export const selectConnectState = ( + state: GitRootState, + artifactDef: GitArtifactDef, +) => selectGitArtifact(state, artifactDef)?.apiResponses.connect; + +export const selectGitImportState = ( + state: GitRootState, + artifactDef: GitArtifactDef, +) => selectGitArtifact(state, artifactDef)?.apiResponses.gitImport; + +export const selectFetchSSHKeysState = ( + state: GitRootState, + artifactDef: GitArtifactDef, +) => selectGitArtifact(state, artifactDef)?.apiResponses.sshKey; + +export const selectGenerateSSHKeyState = ( + state: GitRootState, + artifactDef: GitArtifactDef, +) => selectGitArtifact(state, artifactDef)?.apiResponses.generateSSHKey; + +export const selectConnectModalOpen = ( + state: GitRootState, + artifactDef: GitArtifactDef, +) => selectGitArtifact(state, artifactDef)?.ui.connectModalOpen; + export const selectDisconnectState = ( state: GitRootState, artifactDef: GitArtifactDef, @@ -43,31 +68,35 @@ export const selectDisconnectArtifactName = ( ) => selectGitArtifact(state, artifactDef)?.ui.disconnectArtifactName; // git ops -export const selectCommit = ( +export const selectCommitState = ( state: GitRootState, artifactDef: GitArtifactDef, ) => selectGitArtifact(state, artifactDef)?.apiResponses?.commit; -export const selectDiscard = ( +export const selectDiscardState = ( state: GitRootState, artifactDef: GitArtifactDef, ) => selectGitArtifact(state, artifactDef)?.apiResponses?.discard; -export const selectStatus = ( +export const selectStatusState = ( state: GitRootState, artifactDef: GitArtifactDef, ) => selectGitArtifact(state, artifactDef)?.apiResponses?.status; -export const selectMerge = (state: GitRootState, artifactDef: GitArtifactDef) => - selectGitArtifact(state, artifactDef)?.apiResponses?.merge; +export const selectMergeState = ( + state: GitRootState, + artifactDef: GitArtifactDef, +) => selectGitArtifact(state, artifactDef)?.apiResponses?.merge; -export const selectMergeStatus = ( +export const selectMergeStatusState = ( state: GitRootState, artifactDef: GitArtifactDef, ) => selectGitArtifact(state, artifactDef)?.apiResponses?.mergeStatus; -export const selectPull = (state: GitRootState, artifactDef: GitArtifactDef) => - selectGitArtifact(state, artifactDef)?.apiResponses?.pull; +export const selectPullState = ( + state: GitRootState, + artifactDef: GitArtifactDef, +) => selectGitArtifact(state, artifactDef)?.apiResponses?.pull; export const selectOpsModalOpen = ( state: GitRootState, @@ -95,26 +124,36 @@ export const selectCurrentBranch = ( return gitMetadataState?.branchName; }; -export const selectBranches = ( +export const selectFetchBranchesState = ( state: GitRootState, artifactDef: GitArtifactDef, ) => selectGitArtifact(state, artifactDef)?.apiResponses?.branches; -export const selectCreateBranch = ( +export const selectCreateBranchState = ( state: GitRootState, artifactDef: GitArtifactDef, ) => selectGitArtifact(state, artifactDef)?.apiResponses?.createBranch; -export const selectDeleteBranch = ( +export const selectDeleteBranchState = ( state: GitRootState, artifactDef: GitArtifactDef, ) => selectGitArtifact(state, artifactDef)?.apiResponses?.deleteBranch; -export const selectCheckoutBranch = ( +export const selectCheckoutBranchState = ( state: GitRootState, artifactDef: GitArtifactDef, ) => selectGitArtifact(state, artifactDef)?.apiResponses.checkoutBranch; +export const selectCheckoutDestBranch = ( + state: GitRootState, + artifactDef: GitArtifactDef, +) => selectGitArtifact(state, artifactDef)?.ui.checkoutDestBranch; + +export const selectBranchPopupOpen = ( + state: GitRootState, + artifactDef: GitArtifactDef, +) => selectGitArtifact(state, artifactDef)?.ui.branchPopupOpen; + // SETTINGS // local profile @@ -134,6 +173,11 @@ export const selectToggleAutocommitState = ( artifactDef: GitArtifactDef, ) => selectGitArtifact(state, artifactDef)?.apiResponses.toggleAutocommit; +export const selectTriggerAutocommitState = ( + state: GitRootState, + artifactDef: GitArtifactDef, +) => selectGitArtifact(state, artifactDef)?.apiResponses.triggerAutocommit; + export const selectAutocommitDisableModalOpen = ( state: GitRootState, artifactDef: GitArtifactDef, diff --git a/app/client/src/git/store/types.ts b/app/client/src/git/store/types.ts index 3c439ac128c7..ccb67a2e7a8b 100644 --- a/app/client/src/git/store/types.ts +++ b/app/client/src/git/store/types.ts @@ -1,8 +1,6 @@ import type { PayloadAction } from "@reduxjs/toolkit"; import type { GitArtifactType, - GitConnectStep, - GitImportStep, GitOpsTab, GitSettingsTab, } from "../constants/enums"; @@ -14,13 +12,12 @@ import type { FetchMergeStatusResponseData } from "git/requests/fetchMergeStatus import type { FetchMetadataResponseData } from "git/requests/fetchMetadataRequest.types"; import type { FetchProtectedBranchesResponseData } from "git/requests/fetchProtectedBranchesRequest.types"; import type { ApiResponseError } from "api/types"; +import type { FetchSSHKeyResponseData } from "git/requests/fetchSSHKeyRequest.types"; import type { GitArtifactAPIResponsesReduxState as GitArtifactAPIResponsesReduxStateExtended, GitArtifactUIReduxState as GitArtifactUIReduxStateExtended, } from "git/ee/store/types"; -export type GitSSHKey = Record; - export interface GitApiError extends ApiResponseError { errorType?: string; referenceDoc?: string; @@ -40,6 +37,7 @@ export interface GitSingleArtifactAPIResponsesReduxState extends GitArtifactAPIResponsesReduxStateExtended { metadata: GitAsyncState; connect: GitAsyncStateWithoutValue; + gitImport: GitAsyncStateWithoutValue; status: GitAsyncState; commit: GitAsyncStateWithoutValue; pull: GitAsyncStateWithoutValue; @@ -58,25 +56,17 @@ export interface GitSingleArtifactAPIResponsesReduxState autocommitProgress: GitAsyncStateWithoutValue; toggleAutocommit: GitAsyncStateWithoutValue; triggerAutocommit: GitAsyncStateWithoutValue; - sshKey: GitAsyncState; + sshKey: GitAsyncState; generateSSHKey: GitAsyncStateWithoutValue; } export interface GitSingleArtifactUIReduxState extends GitArtifactUIReduxStateExtended { - connectModal: { - open: boolean; - step: keyof typeof GitConnectStep; - }; + connectModalOpen: boolean; disconnectBaseArtifactId: string | null; disconnectArtifactName: string | null; - importModal: { - open: boolean; - step: keyof typeof GitImportStep; - }; - branchListPopup: { - open: boolean; - }; + branchPopupOpen: boolean; + checkoutDestBranch: string | null; opsModalOpen: boolean; opsModalTab: keyof typeof GitOpsTab; settingsModalOpen: boolean; @@ -84,9 +74,7 @@ export interface GitSingleArtifactUIReduxState autocommitDisableModalOpen: boolean; autocommitPolling: boolean; conflictErrorModalOpen: boolean; - repoLimitErrorModal: { - open: boolean; - }; + repoLimitErrorModalOpen: boolean; } export interface GitSingleArtifactReduxState { ui: GitSingleArtifactUIReduxState;