diff --git a/app/client/packages/design-system/ads/src/Icon/Icon.provider.tsx b/app/client/packages/design-system/ads/src/Icon/Icon.provider.tsx index 8a89050016db..d5eaf80dc941 100644 --- a/app/client/packages/design-system/ads/src/Icon/Icon.provider.tsx +++ b/app/client/packages/design-system/ads/src/Icon/Icon.provider.tsx @@ -572,6 +572,10 @@ const AppsLineIcon = importRemixIcon( async () => import("remixicon-react/AppsLineIcon"), ); +const ProtectedIcon = importRemixIcon( + async () => import("remixicon-react/ShieldKeyholeLineIcon"), +); + const CornerDownLeftLineIcon = importSvg( async () => import("../__assets__/icons/ads/corner-down-left-line.svg"), ); @@ -1394,6 +1398,7 @@ const ICON_LOOKUP = { "warning-triangle": WarningTriangleIcon, "widgets-v3": WidgetsV3Icon, "workflows-mono": WorkflowsMonochromeIcon, + "protected-icon": ProtectedIcon, billing: BillingIcon, binding: Binding, book: BookIcon, diff --git a/app/client/src/git/actions/checkoutBranchActions.ts b/app/client/src/git/actions/checkoutBranchActions.ts deleted file mode 100644 index 65640d5859cd..000000000000 --- a/app/client/src/git/actions/checkoutBranchActions.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { createSingleArtifactAction } from "./helpers/createSingleArtifactAction"; -import type { GitArtifactErrorPayloadAction } from "../types"; - -export const checkoutBranchInitAction = createSingleArtifactAction((state) => { - state.apiResponses.checkoutBranch.loading = true; - state.apiResponses.checkoutBranch.error = null; - - return state; -}); - -export const checkoutBranchSuccessAction = createSingleArtifactAction( - (state) => { - state.apiResponses.checkoutBranch.loading = false; - - return state; - }, -); - -export const checkoutBranchErrorAction = createSingleArtifactAction( - (state, action: GitArtifactErrorPayloadAction) => { - const { error } = action.payload; - - state.apiResponses.checkoutBranch.loading = false; - state.apiResponses.checkoutBranch.error = error; - - return state; - }, -); diff --git a/app/client/src/git/actions/createBranchActions.ts b/app/client/src/git/actions/createBranchActions.ts deleted file mode 100644 index 82963f12d949..000000000000 --- a/app/client/src/git/actions/createBranchActions.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { createSingleArtifactAction } from "./helpers/createSingleArtifactAction"; -import type { GitArtifactErrorPayloadAction } from "../types"; - -export const createBranchInitAction = createSingleArtifactAction((state) => { - state.apiResponses.createBranch.loading = true; - state.apiResponses.createBranch.error = null; - - return state; -}); - -export const createBranchSuccessAction = createSingleArtifactAction((state) => { - state.apiResponses.createBranch.loading = false; - - return state; -}); - -export const createBranchErrorAction = createSingleArtifactAction( - (state, action: GitArtifactErrorPayloadAction) => { - const { error } = action.payload; - - state.apiResponses.createBranch.loading = false; - state.apiResponses.createBranch.error = error; - - return state; - }, -); diff --git a/app/client/src/git/actions/deleteBranchActions.ts b/app/client/src/git/actions/deleteBranchActions.ts deleted file mode 100644 index 5d3ae8293ae4..000000000000 --- a/app/client/src/git/actions/deleteBranchActions.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { createSingleArtifactAction } from "./helpers/createSingleArtifactAction"; -import type { GitArtifactErrorPayloadAction } from "../types"; - -export const deleteBranchInitAction = createSingleArtifactAction((state) => { - state.apiResponses.deleteBranch.loading = true; - state.apiResponses.deleteBranch.error = null; - - return state; -}); - -export const deleteBranchSuccessAction = createSingleArtifactAction((state) => { - state.apiResponses.deleteBranch.loading = false; - - return state; -}); - -export const deleteBranchErrorAction = createSingleArtifactAction( - (state, action: GitArtifactErrorPayloadAction) => { - const { error } = action.payload; - - state.apiResponses.deleteBranch.loading = false; - state.apiResponses.deleteBranch.error = error; - - return state; - }, -); diff --git a/app/client/src/git/actions/fetchMergeStatusActions.ts b/app/client/src/git/actions/fetchMergeStatusActions.ts deleted file mode 100644 index e81a387fe675..000000000000 --- a/app/client/src/git/actions/fetchMergeStatusActions.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { - GitArtifactPayloadAction, - GitArtifactErrorPayloadAction, - GitMergeStatus, -} from "../types"; -import { createSingleArtifactAction } from "./helpers/createSingleArtifactAction"; - -export const fetchMergeStatusInitAction = createSingleArtifactAction( - (state) => { - state.apiResponses.mergeStatus.loading = true; - state.apiResponses.mergeStatus.error = null; - - return state; - }, -); - -export const fetchMergeStatusSuccessAction = createSingleArtifactAction( - ( - state, - action: GitArtifactPayloadAction<{ mergeStatus: GitMergeStatus }>, - ) => { - state.apiResponses.mergeStatus.loading = false; - state.apiResponses.mergeStatus.value = action.payload.mergeStatus; - - return state; - }, -); - -export const fetchMergeStatusErrorAction = createSingleArtifactAction( - (state, action: GitArtifactErrorPayloadAction) => { - const { error } = action.payload; - - state.apiResponses.mergeStatus.loading = false; - state.apiResponses.mergeStatus.error = error; - - return state; - }, -); diff --git a/app/client/src/git/actions/fetchStatusActions.ts b/app/client/src/git/actions/fetchStatusActions.ts deleted file mode 100644 index 21633eeb424e..000000000000 --- a/app/client/src/git/actions/fetchStatusActions.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { FetchStatusRequestParams } from "git/requests/fetchStatusRequest.types"; -import type { - GitArtifactPayloadAction, - GitArtifactErrorPayloadAction, - GitStatus, -} from "../types"; -import { createSingleArtifactAction } from "./helpers/createSingleArtifactAction"; - -export interface FetchStatusInitPayload extends FetchStatusRequestParams {} - -export const fetchStatusInitAction = - createSingleArtifactAction((state) => { - state.apiResponses.status.loading = true; - state.apiResponses.status.error = null; - - return state; - }); - -export const fetchStatusSuccessAction = createSingleArtifactAction( - (state, action: GitArtifactPayloadAction<{ status: GitStatus }>) => { - state.apiResponses.status.loading = false; - state.apiResponses.status.value = action.payload.status; - - return state; - }, -); - -export const fetchStatusErrorAction = createSingleArtifactAction( - (state, action: GitArtifactErrorPayloadAction) => { - const { error } = action.payload; - - state.apiResponses.status.loading = false; - state.apiResponses.status.error = error; - - return state; - }, -); diff --git a/app/client/src/git/components/CtxAwareGitQuickActions/hooks/useStatusChangeCount.ts b/app/client/src/git/components/CtxAwareGitQuickActions/hooks/useStatusChangeCount.ts new file mode 100644 index 000000000000..bcc59083c7f3 --- /dev/null +++ b/app/client/src/git/components/CtxAwareGitQuickActions/hooks/useStatusChangeCount.ts @@ -0,0 +1,40 @@ +import type { FetchStatusResponseData } from "git/requests/fetchStatusRequest.types"; +import { useMemo } from "react"; + +// does not include modules or moduleinstances +export const calcStatusChangeCount = (status: FetchStatusResponseData) => { + const { + modified = [], + modifiedDatasources = 0, + modifiedJSLibs = 0, + modifiedJSObjects = 0, + modifiedModules = 0, + modifiedPages = 0, + modifiedQueries = 0, + } = status || {}; + const themeCount = modified.includes("theme.json") ? 1 : 0; + const settingsCount = modified.includes("application.json") ? 1 : 0; + + // does not include ahead and behind remote counts + return ( + modifiedDatasources + + modifiedJSLibs + + modifiedJSObjects + + modifiedModules + + modifiedPages + + modifiedQueries + + themeCount + + settingsCount + ); +}; + +export default function useStatusChangeCount( + status: FetchStatusResponseData | null, +) { + const statusChangeCount = useMemo( + () => (status ? calcStatusChangeCount(status) : 0), + [status], + ); + + return statusChangeCount; +} diff --git a/app/client/src/git/components/CtxAwareGitQuickActions/index.tsx b/app/client/src/git/components/CtxAwareGitQuickActions/index.tsx new file mode 100644 index 000000000000..9deb2f2d7a55 --- /dev/null +++ b/app/client/src/git/components/CtxAwareGitQuickActions/index.tsx @@ -0,0 +1,54 @@ +import React from "react"; +import GitQuickActions from "../GitQuickActions"; +import { useGitContext } from "../GitContextProvider"; +import useStatusChangeCount from "./hooks/useStatusChangeCount"; + +function CtxAwareGitQuickActions() { + const { + discard, + discardLoading, + fetchStatusLoading, + pull, + pullError, + pullLoading, + status, + toggleGitConnectModal, + toggleGitOpsModal, + toggleGitSettingsModal, + } = useGitContext(); + + const isGitConnected = false; + const isAutocommitEnabled = true; + const isAutocommitPolling = false; + const isConnectPermitted = true; + const isProtectedMode = false; + + const isPullFailing = !!pullError; + const isStatusClean = status?.isClean ?? false; + const statusBehindCount = status?.behindCount ?? 0; + const statusChangeCount = useStatusChangeCount(status); + + return ( + + ); +} + +export default CtxAwareGitQuickActions; diff --git a/app/client/src/git/components/GitContextProvider/hooks/useGitBranches.ts b/app/client/src/git/components/GitContextProvider/hooks/useGitBranches.ts new file mode 100644 index 000000000000..da20d0e19ca3 --- /dev/null +++ b/app/client/src/git/components/GitContextProvider/hooks/useGitBranches.ts @@ -0,0 +1,131 @@ +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, + selectDeleteBranch, +} from "git/store/selectors/gitSingleArtifactSelectors"; +import type { 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: string | null; + fetchBranches: () => void; + createBranchLoading: boolean; + createBranchError: string | null; + createBranch: (branchName: string) => void; + deleteBranchLoading: boolean; + deleteBranchError: string | null; + deleteBranch: (branchName: string) => void; + checkoutBranchLoading: boolean; + checkoutBranchError: string | null; + checkoutBranch: (branchName: string) => void; + toggleGitBranchListPopup: (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], + ); + + // git branch list popup + const toggleGitBranchListPopup = (open: boolean) => { + dispatch( + gitArtifactActions.toggleGitBranchListPopup({ + ...basePayload, + open, + }), + ); + }; + + return { + branches: branchesState?.value ?? null, + fetchBranchesLoading: branchesState?.loading ?? false, + fetchBranchesError: branchesState?.error ?? null, + fetchBranches, + createBranchLoading: createBranchState?.loading ?? false, + createBranchError: createBranchState?.error ?? null, + createBranch, + deleteBranchLoading: deleteBranchState?.loading ?? false, + deleteBranchError: deleteBranchState?.error ?? null, + deleteBranch, + checkoutBranchLoading: checkoutBranchState?.loading ?? false, + checkoutBranchError: checkoutBranchState?.error ?? null, + checkoutBranch, + toggleGitBranchListPopup, + }; +} diff --git a/app/client/src/git/components/GitContextProvider/hooks/useGitConnect.ts b/app/client/src/git/components/GitContextProvider/hooks/useGitConnect.ts new file mode 100644 index 000000000000..900f1716c8c5 --- /dev/null +++ b/app/client/src/git/components/GitContextProvider/hooks/useGitConnect.ts @@ -0,0 +1,37 @@ +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 { + toggleGitConnectModal: (open: boolean) => void; +} + +export default function useGitConnect({ + artifactType, + baseArtifactId, +}: UseGitConnectParams): UseGitConnectReturnValue { + const dispatch = useDispatch(); + const basePayload = useMemo( + () => ({ artifactType, baseArtifactId }), + [artifactType, baseArtifactId], + ); + + const toggleGitConnectModal = (open: boolean) => { + dispatch( + gitArtifactActions.toggleGitConnectModal({ + ...basePayload, + open, + }), + ); + }; + + return { + toggleGitConnectModal, + }; +} diff --git a/app/client/src/git/components/GitContextProvider/hooks/useGitContextValue.ts b/app/client/src/git/components/GitContextProvider/hooks/useGitContextValue.ts new file mode 100644 index 000000000000..36f1b2a988f2 --- /dev/null +++ b/app/client/src/git/components/GitContextProvider/hooks/useGitContextValue.ts @@ -0,0 +1,42 @@ +import type { GitArtifactType } from "git/constants/enums"; +import type { UseGitConnectReturnValue } from "./useGitConnect"; +import type { UseGitOpsReturnValue } from "./useGitOps"; +import type { UseGitSettingsReturnValue } from "./useGitSettings"; +import type { UseGitBranchesReturnValue } from "./useGitBranches"; +import useGitConnect from "./useGitConnect"; +import useGitOps from "./useGitOps"; +import useGitBranches from "./useGitBranches"; +import useGitSettings from "./useGitSettings"; +import { useMemo } from "react"; + +interface UseGitContextValueParams { + artifactType: keyof typeof GitArtifactType; + baseArtifactId: string; +} + +export interface GitContextValue + extends UseGitConnectReturnValue, + UseGitOpsReturnValue, + UseGitSettingsReturnValue, + UseGitBranchesReturnValue {} + +export default function useGitContextValue({ + artifactType, + baseArtifactId, +}: UseGitContextValueParams): GitContextValue { + const basePayload = useMemo( + () => ({ artifactType, baseArtifactId }), + [artifactType, baseArtifactId], + ); + const useGitConnectReturnValue = useGitConnect(basePayload); + const useGitOpsReturnValue = useGitOps(basePayload); + const useGitBranchesReturnValue = useGitBranches(basePayload); + const useGitSettingsReturnValue = useGitSettings(basePayload); + + return { + ...useGitOpsReturnValue, + ...useGitBranchesReturnValue, + ...useGitConnectReturnValue, + ...useGitSettingsReturnValue, + }; +} diff --git a/app/client/src/git/components/GitContextProvider/hooks/useGitOps.ts b/app/client/src/git/components/GitContextProvider/hooks/useGitOps.ts new file mode 100644 index 000000000000..3c9f3990d6e9 --- /dev/null +++ b/app/client/src/git/components/GitContextProvider/hooks/useGitOps.ts @@ -0,0 +1,157 @@ +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, + selectDiscard, + selectMerge, + selectMergeStatus, + selectPull, + selectStatus, +} from "git/store/selectors/gitSingleArtifactSelectors"; +import type { GitRootState } from "git/store/types"; +import { useCallback, useMemo } from "react"; +import { useDispatch, useSelector } from "react-redux"; + +interface UseGitOpsParams { + artifactType: keyof typeof GitArtifactType; + baseArtifactId: string; +} + +export interface UseGitOpsReturnValue { + commitLoading: boolean; + commitError: string | null; + commit: (commitMessage: string) => void; + discardLoading: boolean; + discardError: string | null; + discard: () => void; + status: FetchStatusResponseData | null; + fetchStatusLoading: boolean; + fetchStatusError: string | null; + fetchStatus: () => void; + mergeLoading: boolean; + mergeError: string | null; + merge: () => void; + mergeStatus: FetchMergeStatusResponseData | null; + fetchMergeStatusLoading: boolean; + fetchMergeStatusError: string | null; + fetchMergeStatus: () => void; + pullLoading: boolean; + pullError: string | null; + pull: () => void; + toggleGitOpsModal: (open: boolean, tab?: keyof typeof GitOpsTab) => void; +} + +export default function useGitOps({ + 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], + ); + + // discard + const discardState = useSelector((state: GitRootState) => + selectDiscard(state, basePayload), + ); + + const discard = useCallback(() => { + dispatch(gitArtifactActions.discardInit(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(() => { + dispatch(gitArtifactActions.fetchMergeStatusInit(basePayload)); + }, [basePayload, dispatch]); + + // pull + const pullState = useSelector((state: GitRootState) => + selectPull(state, basePayload), + ); + + const pull = useCallback(() => { + dispatch(gitArtifactActions.pullInit(basePayload)); + }, [basePayload, dispatch]); + + // git ops modal + const toggleGitOpsModal = useCallback( + (open: boolean, tab: keyof typeof GitOpsTab = GitOpsTab.Deploy) => { + dispatch( + gitArtifactActions.toggleGitOpsModal({ ...basePayload, open, tab }), + ); + }, + [basePayload, dispatch], + ); + + return { + commitLoading: commitState?.loading ?? false, + commitError: commitState?.error ?? null, + commit, + discardLoading: discardState?.loading ?? false, + discardError: discardState?.error ?? null, + discard, + status: statusState?.value ?? null, + fetchStatusLoading: statusState?.loading ?? false, + fetchStatusError: statusState?.error ?? null, + fetchStatus, + mergeLoading: mergeState?.loading ?? false, + mergeError: mergeState?.error ?? null, + merge, + mergeStatus: mergeStatusState?.value ?? null, + fetchMergeStatusLoading: mergeStatusState?.loading ?? false, + fetchMergeStatusError: mergeStatusState?.error ?? null, + fetchMergeStatus, + pullLoading: pullState?.loading ?? false, + pullError: pullState?.error ?? null, + pull, + toggleGitOpsModal, + }; +} diff --git a/app/client/src/git/components/GitContextProvider/hooks/useGitSettings.ts b/app/client/src/git/components/GitContextProvider/hooks/useGitSettings.ts new file mode 100644 index 000000000000..096e934d7d5e --- /dev/null +++ b/app/client/src/git/components/GitContextProvider/hooks/useGitSettings.ts @@ -0,0 +1,44 @@ +import type { GitArtifactType, GitSettingsTab } from "git/constants/enums"; +import { gitArtifactActions } from "git/store/gitArtifactSlice"; +import { useMemo } from "react"; +import { useDispatch } from "react-redux"; + +interface UseGitSettingsParams { + artifactType: keyof typeof GitArtifactType; + baseArtifactId: string; +} + +export interface UseGitSettingsReturnValue { + toggleGitSettingsModal: ( + open: boolean, + tab: keyof typeof GitSettingsTab, + ) => void; +} + +export default function useGitSettings({ + artifactType, + baseArtifactId, +}: UseGitSettingsParams): UseGitSettingsReturnValue { + const dispatch = useDispatch(); + const basePayload = useMemo( + () => ({ artifactType, baseArtifactId }), + [artifactType, baseArtifactId], + ); + + const toggleGitSettingsModal = ( + open: boolean, + tab: keyof typeof GitSettingsTab, + ) => { + dispatch( + gitArtifactActions.toggleGitSettingsModal({ + ...basePayload, + open, + tab, + }), + ); + }; + + return { + toggleGitSettingsModal, + }; +} diff --git a/app/client/src/git/components/GitContextProvider/index.tsx b/app/client/src/git/components/GitContextProvider/index.tsx new file mode 100644 index 000000000000..2d057c30670b --- /dev/null +++ b/app/client/src/git/components/GitContextProvider/index.tsx @@ -0,0 +1,39 @@ +import React, { createContext, useContext, useEffect } from "react"; +import type { GitArtifactType } from "git/constants/enums"; +import type { GitContextValue } from "./hooks/useGitContextValue"; +import useGitContextValue from "./hooks/useGitContextValue"; + +const gitContextInitialValue = {} as GitContextValue; + +export const GitContext = createContext(gitContextInitialValue); + +export const useGitContext = () => { + return useContext(GitContext); +}; + +interface GitContextProviderProps { + artifactType: keyof typeof GitArtifactType; + baseArtifactId: string; + children: React.ReactNode; +} + +export default function GitContextProvider({ + artifactType, + baseArtifactId, + children, +}: GitContextProviderProps) { + const contextValue = useGitContextValue({ artifactType, baseArtifactId }); + + const { fetchBranches } = contextValue; + + useEffect( + function gitInitEffect() { + fetchBranches(); + }, + [fetchBranches], + ); + + return ( + {children} + ); +} diff --git a/app/client/src/git/components/QuickActions/AutocommitStatusbar.test.tsx b/app/client/src/git/components/GitQuickActions/AutocommitStatusbar.test.tsx similarity index 100% rename from app/client/src/git/components/QuickActions/AutocommitStatusbar.test.tsx rename to app/client/src/git/components/GitQuickActions/AutocommitStatusbar.test.tsx diff --git a/app/client/src/git/components/QuickActions/AutocommitStatusbar.tsx b/app/client/src/git/components/GitQuickActions/AutocommitStatusbar.tsx similarity index 100% rename from app/client/src/git/components/QuickActions/AutocommitStatusbar.tsx rename to app/client/src/git/components/GitQuickActions/AutocommitStatusbar.tsx diff --git a/app/client/src/git/components/GitQuickActions/BranchButton/BranchList.tsx b/app/client/src/git/components/GitQuickActions/BranchButton/BranchList.tsx new file mode 100644 index 000000000000..4a582fc0ed47 --- /dev/null +++ b/app/client/src/git/components/GitQuickActions/BranchButton/BranchList.tsx @@ -0,0 +1,5 @@ +import React from "react"; + +export default function BranchList() { + return
Test
; +} diff --git a/app/client/src/git/components/GitQuickActions/BranchButton/index.tsx b/app/client/src/git/components/GitQuickActions/BranchButton/index.tsx new file mode 100644 index 000000000000..d73cba35e40e --- /dev/null +++ b/app/client/src/git/components/GitQuickActions/BranchButton/index.tsx @@ -0,0 +1,116 @@ +import { Button, Icon, Tooltip } from "@appsmith/ads"; +import React, { useCallback, useEffect, useState } from "react"; +import styled from "styled-components"; +import noop from "lodash/noop"; +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; + margin: 0 ${(props) => props.theme.spaces[4]}px; + max-width: 122px; + min-width: unset !important; + + :active { + border: 1px solid var(--ads-v2-color-border-muted); + } +`; + +const BranchButtonIcon = styled(Icon)` + margin-right: 4px; +`; + +const BranchButtonLabel = styled.span` + max-width: 82px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`; + +const popoverModifiers: { offset: Record } = { + offset: { enabled: true, options: { offset: [7, 10] } }, +}; + +export default function BranchButton({ + currentBranch = "", + isDisabled = false, + isOpen = false, + isProtectedMode = false, + isStatusClean = false, + setIsOpen = noop, +}: BranchButtonProps) { + const [isEllipsis, setIsEllipsis] = useState(false); + const labelTarget = React.useRef(null); + + const onPopoverInteraction = useCallback( + (nextState: boolean) => { + setIsOpen(nextState); + + if (nextState) { + AnalyticsUtil.logEvent("GS_OPEN_BRANCH_LIST_POPUP", { + source: "BOTTOM_BAR_ACTIVE_BRANCH_NAME", + }); + } + }, + [setIsOpen], + ); + + useEffect(function ellipsisCheck() { + setIsEllipsis(isEllipsisActive(labelTarget.current) ?? false); + }, []); + + const renderContent = useCallback(() => { + return ; + }, []); + + return ( + + + + {isProtectedMode ? ( + + ) : ( + + )} + + {currentBranch} + + {!isStatusClean && !isProtectedMode && "*"} + + + + ); +} diff --git a/app/client/src/git/components/QuickActions/ConnectButton.test.tsx b/app/client/src/git/components/GitQuickActions/ConnectButton.test.tsx similarity index 77% rename from app/client/src/git/components/QuickActions/ConnectButton.test.tsx rename to app/client/src/git/components/GitQuickActions/ConnectButton.test.tsx index 3b017c0c018f..ee01a5af95da 100644 --- a/app/client/src/git/components/QuickActions/ConnectButton.test.tsx +++ b/app/client/src/git/components/GitQuickActions/ConnectButton.test.tsx @@ -2,7 +2,6 @@ import React from "react"; import { render, screen, fireEvent } from "@testing-library/react"; import ConnectButton from "./ConnectButton"; import AnalyticsUtil from "ee/utils/AnalyticsUtil"; -import { GitSyncModalTab } from "entities/GitSync"; import "@testing-library/jest-dom"; import { theme } from "constants/DefaultTheme"; import { ThemeProvider } from "styled-components"; @@ -27,7 +26,7 @@ jest.mock("@appsmith/ads", () => ({ })); describe("ConnectButton Component", () => { - const openGitSyncModalMock = jest.fn(); + const onClickMock = jest.fn(); afterEach(() => { jest.clearAllMocks(); @@ -36,10 +35,7 @@ describe("ConnectButton Component", () => { it("should render correctly when isConnectPermitted is true", () => { render( - + , ); @@ -64,10 +60,7 @@ describe("ConnectButton Component", () => { it("should handle click when isConnectPermitted is true", () => { render( - + , ); @@ -75,25 +68,25 @@ describe("ConnectButton Component", () => { fireEvent.click(button); - expect(AnalyticsUtil.logEvent).toHaveBeenCalledWith( - "GS_CONNECT_GIT_CLICK", - { - source: "BOTTOM_BAR_GIT_CONNECT_BUTTON", - }, - ); + expect(onClickMock).toHaveBeenCalled(); + + // ! might have to move this to Quick Actions instead + // expect(AnalyticsUtil.logEvent).toHaveBeenCalledWith( + // "GS_CONNECT_GIT_CLICK", + // { + // source: "BOTTOM_BAR_GIT_CONNECT_BUTTON", + // }, + // ); - expect(openGitSyncModalMock).toHaveBeenCalledWith({ - tab: GitSyncModalTab.GIT_CONNECTION, - }); + // expect(onClickMock).toHaveBeenCalledWith({ + // tab: GitSyncModalTab.GIT_CONNECTION, + // }); }); it("should render correctly when isConnectPermitted is false", () => { render( - + , ); @@ -121,10 +114,7 @@ describe("ConnectButton Component", () => { it("should not handle click when isConnectPermitted is false", () => { render( - + , ); @@ -133,16 +123,13 @@ describe("ConnectButton Component", () => { fireEvent.click(button); expect(AnalyticsUtil.logEvent).not.toHaveBeenCalled(); - expect(openGitSyncModalMock).not.toHaveBeenCalled(); + expect(onClickMock).not.toHaveBeenCalled(); }); it("should display correct tooltip content when isConnectPermitted is true", () => { render( - + , ); diff --git a/app/client/src/git/components/QuickActions/ConnectButton.tsx b/app/client/src/git/components/GitQuickActions/ConnectButton.tsx similarity index 72% rename from app/client/src/git/components/QuickActions/ConnectButton.tsx rename to app/client/src/git/components/GitQuickActions/ConnectButton.tsx index 426b2a05ec85..4ea835d79a2e 100644 --- a/app/client/src/git/components/QuickActions/ConnectButton.tsx +++ b/app/client/src/git/components/GitQuickActions/ConnectButton.tsx @@ -1,5 +1,4 @@ -import React, { useCallback, useMemo } from "react"; -import { GitSyncModalTab } from "entities/GitSync"; +import React, { useMemo } from "react"; import styled from "styled-components"; import { COMING_SOON, @@ -8,13 +7,8 @@ import { createMessage, NOT_LIVE_FOR_YOU_YET, } from "ee/constants/messages"; -import AnalyticsUtil from "ee/utils/AnalyticsUtil"; -import { Button, Icon, Tooltip } from "@appsmith/ads"; -interface ConnectButtonProps { - isConnectPermitted: boolean; - openGitSyncModal: (options: { tab: GitSyncModalTab }) => void; -} +import { Button, Icon, Tooltip } from "@appsmith/ads"; const CenterDiv = styled.div` text-align: center; @@ -38,10 +32,12 @@ const OuterContainer = styled.div` height: 100%; `; -function ConnectButton({ - isConnectPermitted, - openGitSyncModal, -}: ConnectButtonProps) { +interface ConnectButtonProps { + isConnectPermitted: boolean; + onClick: () => void; +} + +function ConnectButton({ isConnectPermitted, onClick }: ConnectButtonProps) { const isTooltipEnabled = !isConnectPermitted; const tooltipContent = useMemo(() => { if (!isConnectPermitted) { @@ -56,16 +52,6 @@ function ConnectButton({ ); }, [isConnectPermitted]); - const handleClickOnGitConnect = useCallback(() => { - AnalyticsUtil.logEvent("GS_CONNECT_GIT_CLICK", { - source: "BOTTOM_BAR_GIT_CONNECT_BUTTON", - }); - - openGitSyncModal({ - tab: GitSyncModalTab.GIT_CONNECTION, - }); - }, [openGitSyncModal]); - return ( @@ -79,7 +65,7 @@ function ConnectButton({ className="t--connect-git-bottom-bar" isDisabled={!isConnectPermitted} kind="secondary" - onClick={handleClickOnGitConnect} + onClick={onClick} size="sm" > {createMessage(CONNECT_GIT_BETA)} diff --git a/app/client/src/git/components/QuickActions/QuickActionButton.test.tsx b/app/client/src/git/components/GitQuickActions/QuickActionButton.test.tsx similarity index 100% rename from app/client/src/git/components/QuickActions/QuickActionButton.test.tsx rename to app/client/src/git/components/GitQuickActions/QuickActionButton.test.tsx diff --git a/app/client/src/git/components/QuickActions/QuickActionButton.tsx b/app/client/src/git/components/GitQuickActions/QuickActionButton.tsx similarity index 97% rename from app/client/src/git/components/QuickActions/QuickActionButton.tsx rename to app/client/src/git/components/GitQuickActions/QuickActionButton.tsx index 42d7721ab297..c22306b748fa 100644 --- a/app/client/src/git/components/QuickActions/QuickActionButton.tsx +++ b/app/client/src/git/components/GitQuickActions/QuickActionButton.tsx @@ -1,9 +1,9 @@ import React from "react"; import styled from "styled-components"; -import { capitalizeFirstLetter } from "./helpers"; import SpinnerLoader from "pages/common/SpinnerLoader"; import { Button, Tooltip, Text } from "@appsmith/ads"; import { getTypographyByKey } from "@appsmith/ads-old"; +import { capitalizeFirstLetter } from "utils/helpers"; interface QuickActionButtonProps { className?: string; diff --git a/app/client/src/git/components/GitQuickActions/helpers/getPullButtonStatus.test.ts b/app/client/src/git/components/GitQuickActions/helpers/getPullButtonStatus.test.ts new file mode 100644 index 000000000000..8f8d27562143 --- /dev/null +++ b/app/client/src/git/components/GitQuickActions/helpers/getPullButtonStatus.test.ts @@ -0,0 +1,140 @@ +import getPullBtnStatus from "./getPullButtonStatus"; +import type { GetPullButtonStatusParams } from "./getPullButtonStatus"; + +describe("getPullBtnStatus", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return disabled with message "No commits to pull" when behindCount is 0', () => { + const params: GetPullButtonStatusParams = { + isProtectedMode: false, + isPullFailing: false, + isStatusClean: true, + statusBehindCount: 0, + }; + + const result = getPullBtnStatus(params); + + expect(result).toEqual({ + isDisabled: true, + message: + "No commits to pull. This branch is in sync with the remote repository", + }); + }); + + it('should return disabled with message "Cannot pull with local uncommitted changes" when isClean is false and isProtectedMode is false', () => { + const params: GetPullButtonStatusParams = { + isProtectedMode: false, + isPullFailing: false, + isStatusClean: false, + statusBehindCount: 5, + }; + + const result = getPullBtnStatus(params); + + expect(result).toEqual({ + isDisabled: true, + message: + "You have uncommitted changes. Please commit or discard before pulling the remote changes.", + }); + }); + + it('should return enabled with message "Pull changes" when isClean is false, isProtectedMode is true, and behindCount > 0', () => { + const params: GetPullButtonStatusParams = { + isProtectedMode: true, + isPullFailing: false, + isStatusClean: false, + statusBehindCount: 3, + }; + + const result = getPullBtnStatus(params); + + expect(result).toEqual({ + isDisabled: false, + message: "Pull changes", + }); + }); + + it('should return message "Conflicts found" when pullFailed is true', () => { + const params: GetPullButtonStatusParams = { + isProtectedMode: false, + isPullFailing: true, + isStatusClean: true, + statusBehindCount: 2, + }; + + const result = getPullBtnStatus(params); + + expect(result).toEqual({ + isDisabled: false, + message: "Conflicts found. Please resolve them and pull again.", + }); + }); + + it('should return enabled with message "Pull changes" when behindCount > 0 and no other conditions met', () => { + const params: GetPullButtonStatusParams = { + isProtectedMode: false, + isPullFailing: false, + isStatusClean: true, + statusBehindCount: 1, + }; + + const result = getPullBtnStatus(params); + + expect(result).toEqual({ + isDisabled: false, + message: "Pull changes", + }); + }); + + it('should return disabled with message "No commits to pull" when behindCount is 0 regardless of isClean and isProtectedMode', () => { + const params: GetPullButtonStatusParams = { + isProtectedMode: true, + isPullFailing: false, + isStatusClean: false, + statusBehindCount: 0, + }; + + const result = getPullBtnStatus(params); + + expect(result).toEqual({ + isDisabled: true, + message: + "No commits to pull. This branch is in sync with the remote repository", + }); + }); + + it("should prioritize pullFailed over other conditions", () => { + const params: GetPullButtonStatusParams = { + isProtectedMode: false, + isPullFailing: true, + isStatusClean: true, + statusBehindCount: 0, + }; + + const result = getPullBtnStatus(params); + + expect(result).toEqual({ + isDisabled: true, + message: "Conflicts found. Please resolve them and pull again.", + }); + }); + + it("should handle edge case when isClean is false, isProtectedMode is true, behindCount is 0", () => { + const params: GetPullButtonStatusParams = { + isProtectedMode: true, + isPullFailing: false, + isStatusClean: false, + statusBehindCount: 0, + }; + + const result = getPullBtnStatus(params); + + expect(result).toEqual({ + isDisabled: true, + message: + "No commits to pull. This branch is in sync with the remote repository", + }); + }); +}); diff --git a/app/client/src/git/components/GitQuickActions/helpers/getPullButtonStatus.ts b/app/client/src/git/components/GitQuickActions/helpers/getPullButtonStatus.ts new file mode 100644 index 000000000000..ad300d8f6685 --- /dev/null +++ b/app/client/src/git/components/GitQuickActions/helpers/getPullButtonStatus.ts @@ -0,0 +1,43 @@ +import { + CANNOT_PULL_WITH_LOCAL_UNCOMMITTED_CHANGES, + CONFLICTS_FOUND, + createMessage, + NO_COMMITS_TO_PULL, + PULL_CHANGES, +} from "ee/constants/messages"; + +export interface GetPullButtonStatusParams { + isProtectedMode: boolean; + isStatusClean: boolean; + isPullFailing: boolean; + statusBehindCount: number; +} + +const getPullBtnStatus = ({ + isProtectedMode = false, + isPullFailing = false, + isStatusClean = false, + statusBehindCount = 0, +}: GetPullButtonStatusParams) => { + let message = createMessage(NO_COMMITS_TO_PULL); + let isDisabled = statusBehindCount === 0; + + if (!isStatusClean && !isProtectedMode) { + isDisabled = true; + message = createMessage(CANNOT_PULL_WITH_LOCAL_UNCOMMITTED_CHANGES); + } else if (!isStatusClean && isProtectedMode && statusBehindCount > 0) { + isDisabled = false; + message = createMessage(PULL_CHANGES); + } else if (isPullFailing) { + message = createMessage(CONFLICTS_FOUND); + } else if (statusBehindCount > 0) { + message = createMessage(PULL_CHANGES); + } + + return { + isDisabled, + message, + }; +}; + +export default getPullBtnStatus; diff --git a/app/client/src/git/components/QuickActions/index.test.tsx b/app/client/src/git/components/GitQuickActions/index.test.tsx similarity index 84% rename from app/client/src/git/components/QuickActions/index.test.tsx rename to app/client/src/git/components/GitQuickActions/index.test.tsx index 189b3ea67be9..72138f9b9b38 100644 --- a/app/client/src/git/components/QuickActions/index.test.tsx +++ b/app/client/src/git/components/GitQuickActions/index.test.tsx @@ -2,11 +2,10 @@ import React from "react"; import { render, screen, fireEvent } from "@testing-library/react"; import AnalyticsUtil from "ee/utils/AnalyticsUtil"; import QuickActions from "."; -import { GitSettingsTab } from "../../constants/enums"; -import { GitSyncModalTab } from "entities/GitSync"; import { theme } from "constants/DefaultTheme"; import { ThemeProvider } from "styled-components"; import "@testing-library/jest-dom/extend-expect"; +import { GitOpsTab, GitSettingsTab } from "git/constants/enums"; jest.mock("ee/utils/AnalyticsUtil", () => ({ logEvent: jest.fn(), @@ -22,25 +21,23 @@ jest.mock("./AutocommitStatusbar", () => () => ( describe("QuickActions Component", () => { const defaultProps = { - isGitConnected: false, - gitStatus: { - behindCount: 0, - isClean: true, - }, - pullFailed: false, - isProtectedMode: false, - isDiscardInProgress: false, - isPollingAutocommit: false, - isPullInProgress: false, - isFetchingGitStatus: false, - changesToCommit: 0, - gitMetadata: {}, + discard: jest.fn(), isAutocommitEnabled: false, + isAutocommitPolling: false, isConnectPermitted: true, - openGitSyncModal: jest.fn(), - openGitSettingsModal: jest.fn(), - discardChanges: jest.fn(), + isDiscardLoading: false, + isFetchStatusLoading: false, + isGitConnected: false, + isProtectedMode: false, + isPullFailing: false, + isPullLoading: false, + isStatusClean: true, pull: jest.fn(), + statusBehindCount: 0, + statusChangeCount: 0, + toggleGitConnectModal: jest.fn(), + toggleGitOpsModal: jest.fn(), + toggleGitSettingsModal: jest.fn(), }; afterEach(() => { @@ -86,12 +83,8 @@ describe("QuickActions Component", () => { const props = { ...defaultProps, isGitConnected: true, - gitMetadata: { - autoCommitConfig: { - enabled: true, - }, - }, - isPollingAutocommit: true, + isAutocommitEnabled: true, + isAutocommitPolling: true, }; const { container } = render( @@ -122,9 +115,10 @@ describe("QuickActions Component", () => { )[0]; fireEvent.click(commitButton); - expect(props.openGitSyncModal).toHaveBeenCalledWith({ - tab: GitSyncModalTab.DEPLOY, - }); + expect(props.toggleGitOpsModal).toHaveBeenCalledWith( + true, + GitOpsTab.Deploy, + ); expect(AnalyticsUtil.logEvent).toHaveBeenCalledWith( "GS_DEPLOY_GIT_MODAL_TRIGGERED", { @@ -137,14 +131,12 @@ describe("QuickActions Component", () => { const props = { ...defaultProps, isGitConnected: true, - isDiscardInProgress: false, - isPullInProgress: false, - isFetchingGitStatus: false, - pullDisabled: false, - gitStatus: { - behindCount: 1, - isClean: false, - }, + isDiscardLoading: false, + isPullLoading: false, + isFetchStatusLoading: false, + isPullDisabled: false, + statusBehindCount: 1, + statusIsClean: false, isProtectedMode: true, }; @@ -184,10 +176,7 @@ describe("QuickActions Component", () => { source: "BOTTOM_BAR_GIT_MERGE_BUTTON", }, ); - expect(props.openGitSyncModal).toHaveBeenCalledWith({ - tab: GitSyncModalTab.MERGE, - isDeploying: true, - }); + expect(props.toggleGitOpsModal).toHaveBeenCalledWith(true, GitOpsTab.Merge); }); it("should call onSettingsClick when settings button is clicked", () => { @@ -209,9 +198,10 @@ describe("QuickActions Component", () => { expect(AnalyticsUtil.logEvent).toHaveBeenCalledWith("GS_SETTING_CLICK", { source: "BOTTOM_BAR_GIT_SETTING_BUTTON", }); - expect(props.openGitSettingsModal).toHaveBeenCalledWith({ - tab: GitSettingsTab.General, - }); + expect(props.toggleGitSettingsModal).toHaveBeenCalledWith( + true, + GitSettingsTab.General, + ); }); it("should disable commit button when isProtectedMode is true", () => { @@ -237,7 +227,7 @@ describe("QuickActions Component", () => { const props = { ...defaultProps, isGitConnected: true, - isPullInProgress: true, + isPullLoading: true, }; const { container } = render( @@ -259,7 +249,7 @@ describe("QuickActions Component", () => { const props = { ...defaultProps, isGitConnected: true, - changesToCommit: 5, + statusChangeCount: 5, }; render( @@ -277,7 +267,7 @@ describe("QuickActions Component", () => { ...defaultProps, isGitConnected: true, isProtectedMode: true, - changesToCommit: 5, + statusChangeCount: 5, }; render( @@ -289,10 +279,12 @@ describe("QuickActions Component", () => { }); it("should disable pull button when pullDisabled is true", () => { - const mockGetPullBtnStatus = jest.requireMock("./helpers").getPullBtnStatus; + const mockGetPullBtnStatus = jest.requireMock( + "./helpers/getPullButtonStatus", + ).default; mockGetPullBtnStatus.mockReturnValue({ - disabled: true, + isDisabled: true, message: "Pull Disabled", }); @@ -316,10 +308,8 @@ describe("QuickActions Component", () => { const props = { ...defaultProps, isGitConnected: true, - gitStatus: { - behindCount: 3, - isClean: true, - }, + statusBehindCount: 3, + statusIsClean: true, }; render( diff --git a/app/client/src/git/components/GitQuickActions/index.tsx b/app/client/src/git/components/GitQuickActions/index.tsx new file mode 100644 index 000000000000..e43bf2f0d3d1 --- /dev/null +++ b/app/client/src/git/components/GitQuickActions/index.tsx @@ -0,0 +1,179 @@ +import React, { useCallback } from "react"; +import styled from "styled-components"; + +import { + COMMIT_CHANGES, + createMessage, + GIT_SETTINGS, + MERGE, +} from "ee/constants/messages"; + +import AnalyticsUtil from "ee/utils/AnalyticsUtil"; +import { GitOpsTab } from "../../constants/enums"; +import { GitSettingsTab } from "../../constants/enums"; +import ConnectButton from "./ConnectButton"; +import QuickActionButton from "./QuickActionButton"; +import AutocommitStatusbar from "./AutocommitStatusbar"; +import getPullBtnStatus from "./helpers/getPullButtonStatus"; +import noop from "lodash/noop"; + +const Container = styled.div` + height: 100%; + display: flex; + align-items: center; +`; + +interface GitQuickActionsProps { + discard: () => void; + isAutocommitEnabled: boolean; + isAutocommitPolling: boolean; + isConnectPermitted: boolean; + isDiscardLoading: boolean; + isFetchStatusLoading: boolean; + isGitConnected: boolean; + isProtectedMode: boolean; + isPullFailing: boolean; + isPullLoading: boolean; + isStatusClean: boolean; + pull: () => void; + statusBehindCount: number; + statusChangeCount: number; + toggleGitConnectModal: (open: boolean) => void; + toggleGitOpsModal: (open: boolean, tab: keyof typeof GitOpsTab) => void; + toggleGitSettingsModal: ( + open: boolean, + tab: keyof typeof GitSettingsTab, + ) => void; +} + +function GitQuickActions({ + discard = noop, + isAutocommitEnabled = false, + isAutocommitPolling = false, + isConnectPermitted = false, + isDiscardLoading = false, + isFetchStatusLoading = false, + isGitConnected = false, + isProtectedMode = false, + isPullFailing = false, + isPullLoading = false, + isStatusClean = false, + pull = noop, + statusBehindCount = 0, + statusChangeCount = 0, + toggleGitConnectModal = noop, + toggleGitOpsModal = noop, + toggleGitSettingsModal = noop, +}: GitQuickActionsProps) { + const { isDisabled: isPullDisabled, message: pullTooltipMessage } = + getPullBtnStatus({ + isStatusClean, + isProtectedMode, + isPullFailing, + statusBehindCount, + }); + + const isPullButtonLoading = + isDiscardLoading || isPullLoading || isFetchStatusLoading; + + const onCommitBtnClick = useCallback(() => { + if (!isFetchStatusLoading && !isProtectedMode) { + toggleGitOpsModal(true, GitOpsTab.Deploy); + + AnalyticsUtil.logEvent("GS_DEPLOY_GIT_MODAL_TRIGGERED", { + source: "BOTTOM_BAR_GIT_COMMIT_BUTTON", + }); + } + }, [isFetchStatusLoading, isProtectedMode, toggleGitOpsModal]); + + const onPullBtnClick = useCallback(() => { + if (!isPullButtonLoading && !isPullDisabled) { + AnalyticsUtil.logEvent("GS_PULL_GIT_CLICK", { + source: "BOTTOM_BAR_GIT_PULL_BUTTON", + }); + + if (isProtectedMode) { + discard(); + } else { + // pull({ triggeredFromBottomBar: true }); + pull(); + } + } + }, [discard, isProtectedMode, pull, isPullDisabled, isPullButtonLoading]); + + const onMergeBtnClick = useCallback(() => { + AnalyticsUtil.logEvent("GS_MERGE_GIT_MODAL_TRIGGERED", { + source: "BOTTOM_BAR_GIT_MERGE_BUTTON", + }); + toggleGitOpsModal(true, GitOpsTab.Merge); + }, [toggleGitOpsModal]); + + const onSettingsClick = useCallback(() => { + toggleGitSettingsModal(true, GitSettingsTab.General); + AnalyticsUtil.logEvent("GS_SETTING_CLICK", { + source: "BOTTOM_BAR_GIT_SETTING_BUTTON", + }); + }, [toggleGitSettingsModal]); + + const onConnectBtnClick = useCallback(() => { + AnalyticsUtil.logEvent("GS_CONNECT_GIT_CLICK", { + source: "BOTTOM_BAR_GIT_CONNECT_BUTTON", + }); + + toggleGitConnectModal(true); + }, [toggleGitConnectModal]); + + return isGitConnected ? ( + + {/* */} + {isAutocommitEnabled && isAutocommitPolling ? ( + + ) : ( + <> + + + + + + )} + + ) : ( + + ); +} + +export default GitQuickActions; diff --git a/app/client/src/git/components/QuickActions/helper.test.ts b/app/client/src/git/components/QuickActions/helper.test.ts deleted file mode 100644 index a7b3a6d30204..000000000000 --- a/app/client/src/git/components/QuickActions/helper.test.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { getPullBtnStatus, capitalizeFirstLetter } from "./helpers"; - -describe("getPullBtnStatus", () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should return disabled with message "No commits to pull" when behindCount is 0', () => { - const gitStatus: Record = { - behindCount: 0, - isClean: true, - }; - const pullFailed = false; - const isProtected = false; - - const result = getPullBtnStatus(gitStatus, pullFailed, isProtected); - - expect(result).toEqual({ - disabled: true, - message: - "No commits to pull. This branch is in sync with the remote repository", - }); - }); - - it('should return disabled with message "Cannot pull with local uncommitted changes" when isClean is false and isProtected is false', () => { - const gitStatus: Record = { - behindCount: 5, - isClean: false, - }; - const pullFailed = false; - const isProtected = false; - - const result = getPullBtnStatus(gitStatus, pullFailed, isProtected); - - expect(result).toEqual({ - disabled: true, - message: - "You have uncommitted changes. Please commit or discard before pulling the remote changes.", - }); - }); - - it('should return enabled with message "Pull changes" when isClean is false, isProtected is true, and behindCount > 0', () => { - const gitStatus: Record = { - behindCount: 3, - isClean: false, - }; - const pullFailed = false; - const isProtected = true; - - const result = getPullBtnStatus(gitStatus, pullFailed, isProtected); - - expect(result).toEqual({ - disabled: false, - message: "Pull changes", - }); - }); - - it('should return message "Conflicts found" when pullFailed is true', () => { - const gitStatus: Record = { - behindCount: 2, - isClean: true, - }; - const pullFailed = true; - const isProtected = false; - - const result = getPullBtnStatus(gitStatus, pullFailed, isProtected); - - expect(result).toEqual({ - disabled: false, - message: "Conflicts found. Please resolve them and pull again.", - }); - }); - - it('should return enabled with message "Pull changes" when behindCount > 0 and no other conditions met', () => { - const gitStatus: Record = { - behindCount: 1, - isClean: true, - }; - const pullFailed = false; - const isProtected = false; - - const result = getPullBtnStatus(gitStatus, pullFailed, isProtected); - - expect(result).toEqual({ - disabled: false, - message: "Pull changes", - }); - }); - - it('should return disabled with message "No commits to pull" when behindCount is 0 regardless of isClean and isProtected', () => { - const gitStatus: Record = { - behindCount: 0, - isClean: false, - }; - const pullFailed = false; - const isProtected = true; - - const result = getPullBtnStatus(gitStatus, pullFailed, isProtected); - - expect(result).toEqual({ - disabled: true, - message: - "No commits to pull. This branch is in sync with the remote repository", - }); - }); - - it("should prioritize pullFailed over other conditions", () => { - const gitStatus: Record = { - behindCount: 0, - isClean: true, - }; - const pullFailed = true; - const isProtected = false; - - const result = getPullBtnStatus(gitStatus, pullFailed, isProtected); - - expect(result).toEqual({ - disabled: true, - message: "Conflicts found. Please resolve them and pull again.", - }); - }); - - it("should handle edge case when isClean is false, isProtected is true, behindCount is 0", () => { - const gitStatus: Record = { - behindCount: 0, - isClean: false, - }; - const pullFailed = false; - const isProtected = true; - - const result = getPullBtnStatus(gitStatus, pullFailed, isProtected); - - expect(result).toEqual({ - disabled: true, - message: - "No commits to pull. This branch is in sync with the remote repository", - }); - }); -}); - -describe("capitalizeFirstLetter", () => { - it("should capitalize the first letter of a lowercase word", () => { - const result = capitalizeFirstLetter("hello"); - - expect(result).toBe("Hello"); - }); - - it("should capitalize the first letter of an uppercase word", () => { - const result = capitalizeFirstLetter("WORLD"); - - expect(result).toBe("World"); - }); - - it("should handle empty string", () => { - const result = capitalizeFirstLetter(""); - - expect(result).toBe(""); - }); - - it("should handle single character", () => { - const result = capitalizeFirstLetter("a"); - - expect(result).toBe("A"); - }); - - it("should handle strings with spaces", () => { - const result = capitalizeFirstLetter("multiple words here"); - - expect(result).toBe("Multiple words here"); - }); - - it("should handle undefined input", () => { - // The function provides a default value of " " when input is undefined - // So we expect the output to be a single space with capitalized first letter - const result = capitalizeFirstLetter(); - - expect(result).toBe(" "); - }); - - it("should handle strings with special characters", () => { - const result = capitalizeFirstLetter("123abc"); - - expect(result).toBe("123abc"); - }); - - it("should not modify strings where the first character is not a letter", () => { - const result = capitalizeFirstLetter("!test"); - - expect(result).toBe("!test"); - }); -}); diff --git a/app/client/src/git/components/QuickActions/helpers.ts b/app/client/src/git/components/QuickActions/helpers.ts deleted file mode 100644 index 109197264899..000000000000 --- a/app/client/src/git/components/QuickActions/helpers.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { - CANNOT_PULL_WITH_LOCAL_UNCOMMITTED_CHANGES, - CONFLICTS_FOUND, - createMessage, - NO_COMMITS_TO_PULL, - PULL_CHANGES, -} from "ee/constants/messages"; -import type { GitStatus } from "../../types"; - -export const getPullBtnStatus = ( - gitStatus: GitStatus, - pullFailed: boolean, - isProtected: boolean, -) => { - const { behindCount, isClean } = gitStatus; - let message = createMessage(NO_COMMITS_TO_PULL); - let disabled = behindCount === 0; - - if (!isClean && !isProtected) { - disabled = true; - message = createMessage(CANNOT_PULL_WITH_LOCAL_UNCOMMITTED_CHANGES); - // TODO: Remove this when gitStatus typings are finalized - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - } else if (!isClean && isProtected && behindCount > 0) { - disabled = false; - message = createMessage(PULL_CHANGES); - } else if (pullFailed) { - message = createMessage(CONFLICTS_FOUND); - // TODO: Remove this when gitStatus typings are finalized - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - } else if (behindCount > 0) { - message = createMessage(PULL_CHANGES); - } - - return { - disabled, - message, - }; -}; - -export const capitalizeFirstLetter = (string = " ") => { - return string.charAt(0).toUpperCase() + string.toLowerCase().slice(1); -}; diff --git a/app/client/src/git/components/QuickActions/index.tsx b/app/client/src/git/components/QuickActions/index.tsx deleted file mode 100644 index 21483fb83ad4..000000000000 --- a/app/client/src/git/components/QuickActions/index.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import React, { useCallback } from "react"; -import styled from "styled-components"; - -import { - COMMIT_CHANGES, - createMessage, - GIT_SETTINGS, - MERGE, -} from "ee/constants/messages"; - -import { GitSyncModalTab } from "entities/GitSync"; -import AnalyticsUtil from "ee/utils/AnalyticsUtil"; -import type { GitMetadata, GitStatus } from "../../types"; -import { getPullBtnStatus } from "./helpers"; -import { GitSettingsTab } from "../../constants/enums"; -import ConnectButton from "./ConnectButton"; -import QuickActionButton from "./QuickActionButton"; -import AutocommitStatusbar from "./AutocommitStatusbar"; - -interface QuickActionsProps { - isGitConnected: boolean; - gitStatus: GitStatus; - pullFailed: boolean; - isProtectedMode: boolean; - isDiscardInProgress: boolean; - isPollingAutocommit: boolean; - isPullInProgress: boolean; - isFetchingGitStatus: boolean; - changesToCommit: number; - gitMetadata: GitMetadata; - isAutocommitEnabled: boolean; - isConnectPermitted: boolean; - openGitSyncModal: (options: { - tab: GitSyncModalTab; - isDeploying?: boolean; - }) => void; - openGitSettingsModal: (options: { tab: GitSettingsTab }) => void; - discardChanges: () => void; - pull: (options: { triggeredFromBottomBar: boolean }) => void; -} - -const Container = styled.div` - height: 100%; - display: flex; - align-items: center; -`; - -function QuickActions({ - changesToCommit, - discardChanges, - gitMetadata, - gitStatus, - isConnectPermitted, - isDiscardInProgress, - isFetchingGitStatus, - isGitConnected, - isPollingAutocommit, - isProtectedMode, - isPullInProgress, - openGitSettingsModal, - openGitSyncModal, - pull, - pullFailed, -}: QuickActionsProps) { - const { disabled: pullDisabled, message: pullTooltipMessage } = - getPullBtnStatus(gitStatus, !!pullFailed, isProtectedMode); - - const showPullLoadingState = - isDiscardInProgress || isPullInProgress || isFetchingGitStatus; - - // TODO - Update once the gitMetadata typing is added - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const isAutocommitEnabled: boolean = gitMetadata?.autoCommitConfig?.enabled; - const onCommitClick = useCallback(() => { - if (!isFetchingGitStatus && !isProtectedMode) { - openGitSyncModal({ - tab: GitSyncModalTab.DEPLOY, - }); - - AnalyticsUtil.logEvent("GS_DEPLOY_GIT_MODAL_TRIGGERED", { - source: "BOTTOM_BAR_GIT_COMMIT_BUTTON", - }); - } - }, [isFetchingGitStatus, isProtectedMode, openGitSyncModal]); - - const onPullClick = useCallback(() => { - if (!showPullLoadingState && !pullDisabled) { - AnalyticsUtil.logEvent("GS_PULL_GIT_CLICK", { - source: "BOTTOM_BAR_GIT_PULL_BUTTON", - }); - - if (isProtectedMode) { - discardChanges(); - } else { - pull({ triggeredFromBottomBar: true }); - } - } - }, [ - discardChanges, - isProtectedMode, - pull, - pullDisabled, - showPullLoadingState, - ]); - - const onMerge = useCallback(() => { - AnalyticsUtil.logEvent("GS_MERGE_GIT_MODAL_TRIGGERED", { - source: "BOTTOM_BAR_GIT_MERGE_BUTTON", - }); - openGitSyncModal({ - tab: GitSyncModalTab.MERGE, - isDeploying: true, - }); - }, [openGitSyncModal]); - - const onSettingsClick = useCallback(() => { - openGitSettingsModal({ - tab: GitSettingsTab.General, - }); - AnalyticsUtil.logEvent("GS_SETTING_CLICK", { - source: "BOTTOM_BAR_GIT_SETTING_BUTTON", - }); - }, [openGitSettingsModal]); - - return isGitConnected ? ( - - {/* */} - {isAutocommitEnabled && isPollingAutocommit ? ( - - ) : ( - <> - - - - - - )} - - ) : ( - - ); -} - -export default QuickActions; diff --git a/app/client/src/git/requests/checkoutBranchRequest.types.ts b/app/client/src/git/requests/checkoutBranchRequest.types.ts index 8c465fc624ee..38932b20de5f 100644 --- a/app/client/src/git/requests/checkoutBranchRequest.types.ts +++ b/app/client/src/git/requests/checkoutBranchRequest.types.ts @@ -1,8 +1,10 @@ +import type { ApiResponse } from "api/types"; +import type { ApplicationPayload } from "entities/Application"; + export interface CheckoutBranchRequestParams { branchName: string; } -export interface CheckoutBranchResponse { - id: string; // applicationId - baseId: string; // baseApplicationId -} +export interface CheckoutBranchResponseData extends ApplicationPayload {} + +export type CheckoutBranchResponse = ApiResponse; diff --git a/app/client/src/git/requests/createBranchRequest.types.ts b/app/client/src/git/requests/createBranchRequest.types.ts index 28735db75183..2366bd17b4cf 100644 --- a/app/client/src/git/requests/createBranchRequest.types.ts +++ b/app/client/src/git/requests/createBranchRequest.types.ts @@ -1,8 +1,12 @@ +import type { ApiResponse } from "api/types"; + export interface CreateBranchRequestParams { branchName: string; } -export interface CreateBranchResponse { +export interface CreateBranchResponseData { id: string; // applicationId baseId: string; // baseApplicationId } + +export type CreateBranchResponse = ApiResponse; diff --git a/app/client/src/git/requests/deleteBranchRequest.types.ts b/app/client/src/git/requests/deleteBranchRequest.types.ts index f7db6f834859..c3f168303e49 100644 --- a/app/client/src/git/requests/deleteBranchRequest.types.ts +++ b/app/client/src/git/requests/deleteBranchRequest.types.ts @@ -1,8 +1,12 @@ +import type { ApiResponse } from "api/types"; + export interface DeleteBranchRequestParams { branchName: string; } -export interface DeleteBranchResponse { +export interface DeleteBranchResponseData { id: string; // applicationId baseId: string; // baseApplicationId } + +export type DeleteBranchResponse = ApiResponse; diff --git a/app/client/src/git/requests/fetchMergeStatusRequest.types.ts b/app/client/src/git/requests/fetchMergeStatusRequest.types.ts index 76965ee37ff5..b8c928af2630 100644 --- a/app/client/src/git/requests/fetchMergeStatusRequest.types.ts +++ b/app/client/src/git/requests/fetchMergeStatusRequest.types.ts @@ -1,10 +1,15 @@ +import type { ApiResponse } from "api/types"; + export interface FetchMergeStatusRequestParams { sourceBranch: string; destinationBranch: string; } -export interface FetchMergeStatusResponse { +export interface FetchMergeStatusResponseData { isMergeAble: boolean; status: string; // merge status message: string; } + +export type FetchMergeStatusResponse = + ApiResponse; diff --git a/app/client/src/git/requests/fetchStatusRequest.types.ts b/app/client/src/git/requests/fetchStatusRequest.types.ts index 9a63fc879487..975f94b2b483 100644 --- a/app/client/src/git/requests/fetchStatusRequest.types.ts +++ b/app/client/src/git/requests/fetchStatusRequest.types.ts @@ -1,8 +1,9 @@ +import type { ApiResponse } from "api/types"; + export interface FetchStatusRequestParams { compareRemote: boolean; } - -export interface FetchStatusResponse { +export interface FetchStatusResponseData { added: string[]; aheadCount: number; behindCount: number; @@ -36,3 +37,5 @@ export interface FetchStatusResponse { remoteBranch: string; removed: string[]; } + +export type FetchStatusResponse = ApiResponse; diff --git a/app/client/src/git/sagas/checkoutBranchSaga.ts b/app/client/src/git/sagas/checkoutBranchSaga.ts new file mode 100644 index 000000000000..506ecae653d3 --- /dev/null +++ b/app/client/src/git/sagas/checkoutBranchSaga.ts @@ -0,0 +1,121 @@ +import { call, put, select, take } from "redux-saga/effects"; +import type { CheckoutBranchInitPayload } from "../store/actions/checkoutBranchActions"; +import { GitArtifactType } from "../constants/enums"; +import checkoutBranchRequest from "../requests/checkoutBranchRequest"; +import type { + CheckoutBranchRequestParams, + CheckoutBranchResponse, +} from "../requests/checkoutBranchRequest.types"; +import { gitArtifactActions } from "../store/gitArtifactSlice"; +import type { GitArtifactPayloadAction } from "../store/types"; + +// internal dependencies +import { builderURL } from "ee/RouteBuilder"; +import { ReduxActionTypes } from "ee/constants/ReduxActionConstants"; +import { getActions, getJSCollections } from "ee/selectors/entitiesSelector"; +import { addBranchParam } from "constants/routes"; +import type { Action } from "entities/Action"; +import { FocusEntity, identifyEntityFromPath } from "navigation/FocusEntity"; +import { validateResponse } from "sagas/ErrorSagas"; +import history from "utils/history"; +import type { JSCollectionDataState } from "ee/reducers/entityReducers/jsActionsReducer"; + +export default function* checkoutBranchSaga( + action: GitArtifactPayloadAction, +) { + const { artifactType, baseArtifactId, branchName } = action.payload; + const basePayload = { artifactType, baseArtifactId }; + let response: CheckoutBranchResponse | undefined; + + try { + const params: CheckoutBranchRequestParams = { + branchName, + }; + + response = yield call(checkoutBranchRequest, baseArtifactId, params); + const isValidResponse: boolean = yield validateResponse(response); + + if (response && isValidResponse) { + if (artifactType === GitArtifactType.Application) { + yield put(gitArtifactActions.checkoutBranchSuccess(basePayload)); + const trimmedBranch = branchName.replace(/^origin\//, ""); + const destinationHref = addBranchParam(trimmedBranch); + + const entityInfo = identifyEntityFromPath( + destinationHref.slice(0, destinationHref.indexOf("?")), + ); + + yield put( + gitArtifactActions.toggleGitBranchListPopup({ + ...basePayload, + open: false, + }), + ); + // Check if page exists in the branch. If not, instead of 404, take them to + // the app home page + const existingPage = response.data.pages.find( + (page) => page.baseId === entityInfo.params.basePageId, + ); + const defaultPage = response.data.pages.find((page) => page.isDefault); + + if (!existingPage && defaultPage) { + history.push( + builderURL({ + basePageId: defaultPage.baseId, + branch: trimmedBranch, + }), + ); + + return; + } + + // Page exists, so we will try to go to the destination + history.push(destinationHref); + + let shouldGoToHomePage = false; + + // It is possible that the action does not exist in the incoming branch + // so here instead of showing the 404 page, we will navigate them to the + // home page + if ([FocusEntity.API, FocusEntity.QUERY].includes(entityInfo.entity)) { + // Wait for fetch actions success, check if action id in actions state + // or else navigate to home + yield take(ReduxActionTypes.FETCH_ACTIONS_SUCCESS); + const actions: Action[] = yield select(getActions); + + if (!actions.find((action) => action.id === entityInfo.id)) { + shouldGoToHomePage = true; + } + } + + // Same for JS Objects + if (entityInfo.entity === FocusEntity.JS_OBJECT) { + yield take(ReduxActionTypes.FETCH_JS_ACTIONS_SUCCESS); + const jsActions: JSCollectionDataState = + yield select(getJSCollections); + + if (!jsActions.find((action) => action.config.id === entityInfo.id)) { + shouldGoToHomePage = true; + } + } + + if (shouldGoToHomePage && defaultPage) { + // We will replace so that the user does not go back to the 404 url + history.replace( + builderURL({ + basePageId: defaultPage.baseId, + persistExistingParams: true, + }), + ); + } + } + } + } catch (error) { + yield put( + gitArtifactActions.checkoutBranchError({ + ...basePayload, + error: error as string, + }), + ); + } +} diff --git a/app/client/src/git/sagas/commitSaga.ts b/app/client/src/git/sagas/commitSaga.ts index d1926f5b5412..e609a8f5145a 100644 --- a/app/client/src/git/sagas/commitSaga.ts +++ b/app/client/src/git/sagas/commitSaga.ts @@ -1,4 +1,4 @@ -import type { CommitInitPayload } from "../actions/commitActions"; +import type { CommitInitPayload } from "../store/actions/commitActions"; import { GitArtifactType, GitErrorCodes } from "../constants/enums"; import commitRequest from "../requests/commitRequest"; import type { @@ -6,7 +6,7 @@ import type { CommitResponse, } from "../requests/commitRequest.types"; import { gitArtifactActions } from "../store/gitArtifactSlice"; -import type { GitArtifactPayloadAction } from "../types"; +import type { GitArtifactPayloadAction } from "../store/types"; import { call, put } from "redux-saga/effects"; // internal dependencies diff --git a/app/client/src/git/sagas/connectSaga.ts b/app/client/src/git/sagas/connectSaga.ts index a3964934253a..16d4ba1cbe0e 100644 --- a/app/client/src/git/sagas/connectSaga.ts +++ b/app/client/src/git/sagas/connectSaga.ts @@ -5,8 +5,8 @@ import type { ConnectResponse, } from "../requests/connectRequest.types"; import { GitArtifactType, GitErrorCodes } from "../constants/enums"; -import type { GitArtifactPayloadAction } from "../types"; -import type { ConnectInitPayload } from "../actions/connectActions"; +import type { GitArtifactPayloadAction } from "../store/types"; +import type { ConnectInitPayload } from "../store/actions/connectActions"; import { call, put } from "redux-saga/effects"; diff --git a/app/client/src/git/sagas/createBranchSaga.ts b/app/client/src/git/sagas/createBranchSaga.ts new file mode 100644 index 000000000000..37ce2de1afaf --- /dev/null +++ b/app/client/src/git/sagas/createBranchSaga.ts @@ -0,0 +1,48 @@ +import { call, put } from "redux-saga/effects"; +import type { CreateBranchInitPayload } from "../store/actions/createBranchActions"; +import createBranchRequest from "../requests/createBranchRequest"; +import type { + CreateBranchRequestParams, + CreateBranchResponse, +} from "../requests/createBranchRequest.types"; +import { gitArtifactActions } from "../store/gitArtifactSlice"; +import type { GitArtifactPayloadAction } from "../store/types"; + +// internal dependencies +import { validateResponse } from "sagas/ErrorSagas"; + +export default function* createBranchSaga( + action: GitArtifactPayloadAction, +) { + const { artifactType, baseArtifactId } = action.payload; + const basePayload = { artifactType, baseArtifactId }; + let response: CreateBranchResponse | undefined; + + try { + const params: CreateBranchRequestParams = { + branchName: action.payload.branchName, + }; + + response = yield call(createBranchRequest, baseArtifactId, params); + const isValidResponse: boolean = yield validateResponse(response); + + if (isValidResponse) { + yield put(gitArtifactActions.createBranchSuccess(basePayload)); + yield put( + gitArtifactActions.fetchBranchesInit({ + ...basePayload, + pruneBranches: true, + }), + ); + + // ! case to switch to the new branch + } + } catch (error) { + yield put( + gitArtifactActions.createBranchError({ + ...basePayload, + error: error as string, + }), + ); + } +} diff --git a/app/client/src/git/sagas/deleteBranchSaga.ts b/app/client/src/git/sagas/deleteBranchSaga.ts new file mode 100644 index 000000000000..7b138685e6a9 --- /dev/null +++ b/app/client/src/git/sagas/deleteBranchSaga.ts @@ -0,0 +1,46 @@ +import type { DeleteBranchInitPayload } from "../store/actions/deleteBranchActions"; +import deleteBranchRequest from "../requests/deleteBranchRequest"; +import type { + DeleteBranchRequestParams, + DeleteBranchResponse, +} from "../requests/deleteBranchRequest.types"; +import { gitArtifactActions } from "../store/gitArtifactSlice"; +import type { GitArtifactPayloadAction } from "../store/types"; +import { call, put } from "redux-saga/effects"; + +// internal dependencies +import { validateResponse } from "sagas/ErrorSagas"; + +export default function* deleteBranchSaga( + action: GitArtifactPayloadAction, +) { + const { artifactType, baseArtifactId } = action.payload; + const basePayload = { artifactType, baseArtifactId }; + let response: DeleteBranchResponse | undefined; + + try { + const params: DeleteBranchRequestParams = { + branchName: action.payload.branchName, + }; + + response = yield call(deleteBranchRequest, baseArtifactId, params); + const isValidResponse: boolean = yield validateResponse(response); + + if (isValidResponse) { + yield put(gitArtifactActions.deleteBranchSuccess(basePayload)); + yield put( + gitArtifactActions.fetchBranchesInit({ + ...basePayload, + pruneBranches: true, + }), + ); + } + } catch (error) { + yield put( + gitArtifactActions.deleteBranchError({ + ...basePayload, + error: error as string, + }), + ); + } +} diff --git a/app/client/src/git/sagas/fetchBranchesSaga.ts b/app/client/src/git/sagas/fetchBranchesSaga.ts index 0a10286f91df..5909b62e0c45 100644 --- a/app/client/src/git/sagas/fetchBranchesSaga.ts +++ b/app/client/src/git/sagas/fetchBranchesSaga.ts @@ -1,11 +1,11 @@ -import type { FetchBranchesInitPayload } from "git/actions/fetchBranchesActions"; +import type { FetchBranchesInitPayload } from "../store/actions/fetchBranchesActions"; import fetchBranchesRequest from "git/requests/fetchBranchesRequest"; import type { FetchBranchesRequestParams, FetchBranchesResponse, } from "git/requests/fetchBranchesRequest.types"; import { gitArtifactActions } from "git/store/gitArtifactSlice"; -import type { GitArtifactPayloadAction } from "git/types"; +import type { GitArtifactPayloadAction } from "../store/types"; import { call, put } from "redux-saga/effects"; import { validateResponse } from "sagas/ErrorSagas"; diff --git a/app/client/src/git/sagas/fetchLocalProfileSaga.ts b/app/client/src/git/sagas/fetchLocalProfileSaga.ts index f0fed2306b75..a284b7e21d57 100644 --- a/app/client/src/git/sagas/fetchLocalProfileSaga.ts +++ b/app/client/src/git/sagas/fetchLocalProfileSaga.ts @@ -1,7 +1,7 @@ import fetchLocalProfileRequest from "git/requests/fetchLocalProfileRequest"; import type { FetchLocalProfileResponse } from "git/requests/fetchLocalProfileRequest.types"; import { gitArtifactActions } from "git/store/gitArtifactSlice"; -import type { GitArtifactPayloadAction } from "git/types"; +import type { GitArtifactPayloadAction } from "../store/types"; import { call, put } from "redux-saga/effects"; import { validateResponse } from "sagas/ErrorSagas"; diff --git a/app/client/src/git/sagas/index.ts b/app/client/src/git/sagas/index.ts index 50517d875efd..076853a8612a 100644 --- a/app/client/src/git/sagas/index.ts +++ b/app/client/src/git/sagas/index.ts @@ -12,8 +12,11 @@ import updateGlobalProfileSaga from "./updateGlobalProfileSaga"; export function* gitSagas() { yield all([ takeLatest(gitArtifactActions.connectInit.type, connectSaga), - takeLatest(gitArtifactActions.commitInit.type, commitSaga), + + // branches takeLatest(gitArtifactActions.fetchBranchesInit.type, fetchBranchesSaga), + + takeLatest(gitArtifactActions.commitInit.type, commitSaga), takeLatest( gitArtifactActions.fetchLocalProfileInit.type, fetchLocalProfileSaga, diff --git a/app/client/src/git/sagas/updateGlobalProfileSaga.ts b/app/client/src/git/sagas/updateGlobalProfileSaga.ts index 24d22cc144c9..967d6228a0a0 100644 --- a/app/client/src/git/sagas/updateGlobalProfileSaga.ts +++ b/app/client/src/git/sagas/updateGlobalProfileSaga.ts @@ -1,6 +1,6 @@ import type { PayloadAction } from "@reduxjs/toolkit"; import { call, put } from "redux-saga/effects"; -import type { UpdateGlobalProfileInitPayload } from "../actions/updateGlobalProfileActions"; +import type { UpdateGlobalProfileInitPayload } from "../store/actions/updateGlobalProfileActions"; import updateGlobalProfileRequest from "../requests/updateGlobalProfileRequest"; import type { UpdateGlobalProfileRequestParams, diff --git a/app/client/src/git/sagas/updateLocalProfileSaga.ts b/app/client/src/git/sagas/updateLocalProfileSaga.ts index 21923693a322..d2a9c0243a15 100644 --- a/app/client/src/git/sagas/updateLocalProfileSaga.ts +++ b/app/client/src/git/sagas/updateLocalProfileSaga.ts @@ -1,11 +1,11 @@ -import type { UpdateLocalProfileInitPayload } from "git/actions/updateLocalProfileActions"; +import type { UpdateLocalProfileInitPayload } from "../store/actions/updateLocalProfileActions"; import updateLocalProfileRequest from "git/requests/updateLocalProfileRequest"; import type { UpdateLocalProfileRequestParams, UpdateLocalProfileResponse, } from "git/requests/updateLocalProfileRequest.types"; -import { gitArtifactActions } from "git/store/gitArtifactSlice"; -import type { GitArtifactPayloadAction } from "git/types"; +import { gitArtifactActions } from "../store/gitArtifactSlice"; +import type { GitArtifactPayloadAction } from "../store/types"; import { call, put } from "redux-saga/effects"; import { validateResponse } from "sagas/ErrorSagas"; diff --git a/app/client/src/git/store/actions/checkoutBranchActions.ts b/app/client/src/git/store/actions/checkoutBranchActions.ts new file mode 100644 index 000000000000..c46edf84baff --- /dev/null +++ b/app/client/src/git/store/actions/checkoutBranchActions.ts @@ -0,0 +1,32 @@ +import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction"; +import type { GitAsyncErrorPayload } from "../types"; +import type { CheckoutBranchRequestParams } from "git/requests/checkoutBranchRequest.types"; + +export interface CheckoutBranchInitPayload + extends CheckoutBranchRequestParams {} + +export const checkoutBranchInitAction = + createSingleArtifactAction((state) => { + state.apiResponses.checkoutBranch.loading = true; + state.apiResponses.checkoutBranch.error = null; + + return state; + }); + +export const checkoutBranchSuccessAction = createSingleArtifactAction( + (state) => { + state.apiResponses.checkoutBranch.loading = false; + + return state; + }, +); + +export const checkoutBranchErrorAction = + createSingleArtifactAction((state, action) => { + const { error } = action.payload; + + state.apiResponses.checkoutBranch.loading = false; + state.apiResponses.checkoutBranch.error = error; + + return state; + }); diff --git a/app/client/src/git/actions/commitActions.ts b/app/client/src/git/store/actions/commitActions.ts similarity index 90% rename from app/client/src/git/actions/commitActions.ts rename to app/client/src/git/store/actions/commitActions.ts index eeb80a18984c..24c5b17267f4 100644 --- a/app/client/src/git/actions/commitActions.ts +++ b/app/client/src/git/store/actions/commitActions.ts @@ -1,4 +1,4 @@ -import { createSingleArtifactAction } from "./helpers/createSingleArtifactAction"; +import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction"; import type { GitAsyncErrorPayload } from "../types"; import type { CommitRequestParams } from "git/requests/commitRequest.types"; diff --git a/app/client/src/git/actions/connectActions.ts b/app/client/src/git/store/actions/connectActions.ts similarity index 91% rename from app/client/src/git/actions/connectActions.ts rename to app/client/src/git/store/actions/connectActions.ts index 5a45872ed2a1..7fed98500000 100644 --- a/app/client/src/git/actions/connectActions.ts +++ b/app/client/src/git/store/actions/connectActions.ts @@ -1,4 +1,4 @@ -import { createSingleArtifactAction } from "./helpers/createSingleArtifactAction"; +import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction"; import type { GitAsyncErrorPayload } from "../types"; import type { ConnectRequestParams } from "git/requests/connectRequest.types"; diff --git a/app/client/src/git/store/actions/createBranchActions.ts b/app/client/src/git/store/actions/createBranchActions.ts new file mode 100644 index 000000000000..be0e08445de2 --- /dev/null +++ b/app/client/src/git/store/actions/createBranchActions.ts @@ -0,0 +1,29 @@ +import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction"; +import type { GitAsyncErrorPayload } from "../types"; +import type { CreateBranchRequestParams } from "git/requests/createBranchRequest.types"; + +export interface CreateBranchInitPayload extends CreateBranchRequestParams {} + +export const createBranchInitAction = + createSingleArtifactAction((state) => { + state.apiResponses.createBranch.loading = true; + state.apiResponses.createBranch.error = null; + + return state; + }); + +export const createBranchSuccessAction = createSingleArtifactAction((state) => { + state.apiResponses.createBranch.loading = false; + + return state; +}); + +export const createBranchErrorAction = + createSingleArtifactAction((state, action) => { + const { error } = action.payload; + + state.apiResponses.createBranch.loading = false; + state.apiResponses.createBranch.error = error; + + return state; + }); diff --git a/app/client/src/git/store/actions/deleteBranchActions.ts b/app/client/src/git/store/actions/deleteBranchActions.ts new file mode 100644 index 000000000000..09296625f0d6 --- /dev/null +++ b/app/client/src/git/store/actions/deleteBranchActions.ts @@ -0,0 +1,29 @@ +import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction"; +import type { GitAsyncErrorPayload } from "../types"; +import type { DeleteBranchRequestParams } from "../../requests/deleteBranchRequest.types"; + +export interface DeleteBranchInitPayload extends DeleteBranchRequestParams {} + +export const deleteBranchInitAction = + createSingleArtifactAction((state) => { + state.apiResponses.deleteBranch.loading = true; + state.apiResponses.deleteBranch.error = null; + + return state; + }); + +export const deleteBranchSuccessAction = createSingleArtifactAction((state) => { + state.apiResponses.deleteBranch.loading = false; + + return state; +}); + +export const deleteBranchErrorAction = + createSingleArtifactAction((state, action) => { + const { error } = action.payload; + + state.apiResponses.deleteBranch.loading = false; + state.apiResponses.deleteBranch.error = error; + + return state; + }); diff --git a/app/client/src/git/actions/discardActions.ts b/app/client/src/git/store/actions/discardActions.ts similarity index 88% rename from app/client/src/git/actions/discardActions.ts rename to app/client/src/git/store/actions/discardActions.ts index a0863c79e638..c12b236f06ef 100644 --- a/app/client/src/git/actions/discardActions.ts +++ b/app/client/src/git/store/actions/discardActions.ts @@ -1,4 +1,4 @@ -import { createSingleArtifactAction } from "./helpers/createSingleArtifactAction"; +import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction"; import type { GitArtifactErrorPayloadAction } from "../types"; export const discardInitAction = createSingleArtifactAction((state) => { diff --git a/app/client/src/git/actions/disconnectActions.ts b/app/client/src/git/store/actions/disconnectActions.ts similarity index 89% rename from app/client/src/git/actions/disconnectActions.ts rename to app/client/src/git/store/actions/disconnectActions.ts index f911eefec631..dba26c0de629 100644 --- a/app/client/src/git/actions/disconnectActions.ts +++ b/app/client/src/git/store/actions/disconnectActions.ts @@ -1,4 +1,4 @@ -import { createSingleArtifactAction } from "./helpers/createSingleArtifactAction"; +import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction"; import type { GitArtifactErrorPayloadAction } from "../types"; export const disconnectInitAction = createSingleArtifactAction((state) => { diff --git a/app/client/src/git/actions/fetchAutocommitProgressActions.ts b/app/client/src/git/store/actions/fetchAutocommitProgressActions.ts similarity index 92% rename from app/client/src/git/actions/fetchAutocommitProgressActions.ts rename to app/client/src/git/store/actions/fetchAutocommitProgressActions.ts index caa054318f5f..2736b0c72cc4 100644 --- a/app/client/src/git/actions/fetchAutocommitProgressActions.ts +++ b/app/client/src/git/store/actions/fetchAutocommitProgressActions.ts @@ -3,7 +3,7 @@ import type { GitArtifactErrorPayloadAction, GitAutocommitProgress, } from "../types"; -import { createSingleArtifactAction } from "./helpers/createSingleArtifactAction"; +import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction"; export const fetchAutocommitProgressInitAction = createSingleArtifactAction( (state) => { diff --git a/app/client/src/git/actions/fetchBranchesActions.ts b/app/client/src/git/store/actions/fetchBranchesActions.ts similarity index 88% rename from app/client/src/git/actions/fetchBranchesActions.ts rename to app/client/src/git/store/actions/fetchBranchesActions.ts index 30f71574dc52..e83ce0325e59 100644 --- a/app/client/src/git/actions/fetchBranchesActions.ts +++ b/app/client/src/git/store/actions/fetchBranchesActions.ts @@ -1,9 +1,9 @@ import type { FetchBranchesRequestParams, FetchBranchesResponseData, -} from "../requests/fetchBranchesRequest.types"; +} from "../../requests/fetchBranchesRequest.types"; import type { GitAsyncErrorPayload, GitAsyncSuccessPayload } from "../types"; -import { createSingleArtifactAction } from "./helpers/createSingleArtifactAction"; +import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction"; export interface FetchBranchesInitPayload extends FetchBranchesRequestParams {} diff --git a/app/client/src/git/actions/fetchGlobalProfileActions.ts b/app/client/src/git/store/actions/fetchGlobalProfileActions.ts similarity index 100% rename from app/client/src/git/actions/fetchGlobalProfileActions.ts rename to app/client/src/git/store/actions/fetchGlobalProfileActions.ts diff --git a/app/client/src/git/actions/fetchLocalProfileActions.ts b/app/client/src/git/store/actions/fetchLocalProfileActions.ts similarity index 92% rename from app/client/src/git/actions/fetchLocalProfileActions.ts rename to app/client/src/git/store/actions/fetchLocalProfileActions.ts index c4775f4ab7f5..3559a2814c35 100644 --- a/app/client/src/git/actions/fetchLocalProfileActions.ts +++ b/app/client/src/git/store/actions/fetchLocalProfileActions.ts @@ -3,7 +3,7 @@ import type { GitArtifactErrorPayloadAction, GitAsyncSuccessPayload, } from "../types"; -import { createSingleArtifactAction } from "./helpers/createSingleArtifactAction"; +import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction"; export const fetchLocalProfileInitAction = createSingleArtifactAction( (state) => { diff --git a/app/client/src/git/store/actions/fetchMergeStatusActions.ts b/app/client/src/git/store/actions/fetchMergeStatusActions.ts new file mode 100644 index 000000000000..71ae8fccf107 --- /dev/null +++ b/app/client/src/git/store/actions/fetchMergeStatusActions.ts @@ -0,0 +1,31 @@ +import type { GitAsyncSuccessPayload, GitAsyncErrorPayload } from "../types"; +import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction"; +import type { FetchMergeStatusResponseData } from "git/requests/fetchMergeStatusRequest.types"; + +export const fetchMergeStatusInitAction = createSingleArtifactAction( + (state) => { + state.apiResponses.mergeStatus.loading = true; + state.apiResponses.mergeStatus.error = null; + + return state; + }, +); + +export const fetchMergeStatusSuccessAction = createSingleArtifactAction< + GitAsyncSuccessPayload +>((state, action) => { + state.apiResponses.mergeStatus.loading = false; + state.apiResponses.mergeStatus.value = action.payload.responseData; + + return state; +}); + +export const fetchMergeStatusErrorAction = + createSingleArtifactAction((state, action) => { + const { error } = action.payload; + + state.apiResponses.mergeStatus.loading = false; + state.apiResponses.mergeStatus.error = error; + + return state; + }); diff --git a/app/client/src/git/actions/fetchMetadataActions.ts b/app/client/src/git/store/actions/fetchMetadataActions.ts similarity index 91% rename from app/client/src/git/actions/fetchMetadataActions.ts rename to app/client/src/git/store/actions/fetchMetadataActions.ts index a11914c8200a..f4d1fd9da2cb 100644 --- a/app/client/src/git/actions/fetchMetadataActions.ts +++ b/app/client/src/git/store/actions/fetchMetadataActions.ts @@ -3,7 +3,7 @@ import type { GitArtifactErrorPayloadAction, GitMetadata, } from "../types"; -import { createSingleArtifactAction } from "./helpers/createSingleArtifactAction"; +import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction"; export const fetchMetadataInitAction = createSingleArtifactAction((state) => { state.apiResponses.metadata.loading = true; diff --git a/app/client/src/git/actions/fetchProtectedBranchesActions.ts b/app/client/src/git/store/actions/fetchProtectedBranchesActions.ts similarity index 92% rename from app/client/src/git/actions/fetchProtectedBranchesActions.ts rename to app/client/src/git/store/actions/fetchProtectedBranchesActions.ts index 32026a1ed285..223952a3962b 100644 --- a/app/client/src/git/actions/fetchProtectedBranchesActions.ts +++ b/app/client/src/git/store/actions/fetchProtectedBranchesActions.ts @@ -3,7 +3,7 @@ import type { GitArtifactErrorPayloadAction, GitProtectedBranches, } from "../types"; -import { createSingleArtifactAction } from "./helpers/createSingleArtifactAction"; +import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction"; export const fetchProtectedBranchesInitAction = createSingleArtifactAction( (state) => { diff --git a/app/client/src/git/actions/fetchSSHKeyActions.ts b/app/client/src/git/store/actions/fetchSSHKeyActions.ts similarity index 90% rename from app/client/src/git/actions/fetchSSHKeyActions.ts rename to app/client/src/git/store/actions/fetchSSHKeyActions.ts index 516adf758f44..b802160ce0af 100644 --- a/app/client/src/git/actions/fetchSSHKeyActions.ts +++ b/app/client/src/git/store/actions/fetchSSHKeyActions.ts @@ -3,7 +3,7 @@ import type { GitArtifactErrorPayloadAction, GitSSHKey, } from "../types"; -import { createSingleArtifactAction } from "./helpers/createSingleArtifactAction"; +import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction"; export const fetchSSHKeyInitAction = createSingleArtifactAction((state) => { state.apiResponses.sshKey.loading = true; diff --git a/app/client/src/git/store/actions/fetchStatusActions.ts b/app/client/src/git/store/actions/fetchStatusActions.ts new file mode 100644 index 000000000000..1121e368d012 --- /dev/null +++ b/app/client/src/git/store/actions/fetchStatusActions.ts @@ -0,0 +1,35 @@ +import type { + FetchStatusRequestParams, + FetchStatusResponseData, +} from "git/requests/fetchStatusRequest.types"; +import type { GitAsyncErrorPayload, GitAsyncSuccessPayload } from "../types"; +import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction"; + +export interface FetchStatusInitPayload extends FetchStatusRequestParams {} + +export const fetchStatusInitAction = + createSingleArtifactAction((state) => { + state.apiResponses.status.loading = true; + state.apiResponses.status.error = null; + + return state; + }); + +export const fetchStatusSuccessAction = createSingleArtifactAction< + GitAsyncSuccessPayload +>((state, action) => { + state.apiResponses.status.loading = false; + state.apiResponses.status.value = action.payload.responseData; + + return state; +}); + +export const fetchStatusErrorAction = + createSingleArtifactAction((state, action) => { + const { error } = action.payload; + + state.apiResponses.status.loading = false; + state.apiResponses.status.error = error; + + return state; + }); diff --git a/app/client/src/git/actions/generateSSHKey.ts b/app/client/src/git/store/actions/generateSSHKey.ts similarity index 89% rename from app/client/src/git/actions/generateSSHKey.ts rename to app/client/src/git/store/actions/generateSSHKey.ts index c2a82f94e8f3..52698c65a8e1 100644 --- a/app/client/src/git/actions/generateSSHKey.ts +++ b/app/client/src/git/store/actions/generateSSHKey.ts @@ -1,4 +1,4 @@ -import { createSingleArtifactAction } from "./helpers/createSingleArtifactAction"; +import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction"; import type { GitArtifactErrorPayloadAction } from "../types"; export const generateSSHKeyInitAction = createSingleArtifactAction((state) => { diff --git a/app/client/src/git/actions/mergeActions.ts b/app/client/src/git/store/actions/mergeActions.ts similarity index 88% rename from app/client/src/git/actions/mergeActions.ts rename to app/client/src/git/store/actions/mergeActions.ts index dab2d21ed4c3..5bb17a351ce6 100644 --- a/app/client/src/git/actions/mergeActions.ts +++ b/app/client/src/git/store/actions/mergeActions.ts @@ -1,4 +1,4 @@ -import { createSingleArtifactAction } from "./helpers/createSingleArtifactAction"; +import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction"; import type { GitArtifactErrorPayloadAction } from "../types"; export const mergeInitAction = createSingleArtifactAction((state) => { diff --git a/app/client/src/git/actions/mountActions.ts b/app/client/src/git/store/actions/mountActions.ts similarity index 88% rename from app/client/src/git/actions/mountActions.ts rename to app/client/src/git/store/actions/mountActions.ts index 556aae44a483..07a08ee2b566 100644 --- a/app/client/src/git/actions/mountActions.ts +++ b/app/client/src/git/store/actions/mountActions.ts @@ -1,6 +1,6 @@ import type { PayloadAction } from "@reduxjs/toolkit"; import type { GitArtifactBasePayload, GitArtifactReduxState } from "../types"; -import { gitSingleArtifactInitialState } from "./helpers/singleArtifactInitialState"; +import { gitSingleArtifactInitialState } from "../helpers/gitSingleArtifactInitialState"; // ! This might be removed later diff --git a/app/client/src/git/actions/pullActions.ts b/app/client/src/git/store/actions/pullActions.ts similarity index 88% rename from app/client/src/git/actions/pullActions.ts rename to app/client/src/git/store/actions/pullActions.ts index 04f2dfcd31fe..5acb7dc30f98 100644 --- a/app/client/src/git/actions/pullActions.ts +++ b/app/client/src/git/store/actions/pullActions.ts @@ -1,4 +1,4 @@ -import { createSingleArtifactAction } from "./helpers/createSingleArtifactAction"; +import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction"; import type { GitArtifactErrorPayloadAction } from "../types"; export const pullInitAction = createSingleArtifactAction((state) => { diff --git a/app/client/src/git/actions/repoLimitErrorModalActions.ts b/app/client/src/git/store/actions/repoLimitErrorModalActions.ts similarity index 79% rename from app/client/src/git/actions/repoLimitErrorModalActions.ts rename to app/client/src/git/store/actions/repoLimitErrorModalActions.ts index 96061395e854..d79b29d61d15 100644 --- a/app/client/src/git/actions/repoLimitErrorModalActions.ts +++ b/app/client/src/git/store/actions/repoLimitErrorModalActions.ts @@ -1,4 +1,4 @@ -import { createSingleArtifactAction } from "./helpers/createSingleArtifactAction"; +import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction"; interface ToggleRepoLimitModalActionPayload { open: boolean; diff --git a/app/client/src/git/actions/toggleAutocommitActions.ts b/app/client/src/git/store/actions/toggleAutocommitActions.ts similarity index 90% rename from app/client/src/git/actions/toggleAutocommitActions.ts rename to app/client/src/git/store/actions/toggleAutocommitActions.ts index 129011c50143..96721698196f 100644 --- a/app/client/src/git/actions/toggleAutocommitActions.ts +++ b/app/client/src/git/store/actions/toggleAutocommitActions.ts @@ -1,4 +1,4 @@ -import { createSingleArtifactAction } from "./helpers/createSingleArtifactAction"; +import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction"; import type { GitArtifactErrorPayloadAction } from "../types"; export const toggleAutocommitInitAction = createSingleArtifactAction( diff --git a/app/client/src/git/actions/triggerAutocommitActions.ts b/app/client/src/git/store/actions/triggerAutocommitActions.ts similarity index 90% rename from app/client/src/git/actions/triggerAutocommitActions.ts rename to app/client/src/git/store/actions/triggerAutocommitActions.ts index 1ea785bdaeb7..414de9ed68c8 100644 --- a/app/client/src/git/actions/triggerAutocommitActions.ts +++ b/app/client/src/git/store/actions/triggerAutocommitActions.ts @@ -1,4 +1,4 @@ -import { createSingleArtifactAction } from "./helpers/createSingleArtifactAction"; +import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction"; import type { GitArtifactErrorPayloadAction } from "../types"; export const triggerAutocommitInitAction = createSingleArtifactAction( diff --git a/app/client/src/git/store/actions/uiActions.ts b/app/client/src/git/store/actions/uiActions.ts new file mode 100644 index 000000000000..fe77a5684e51 --- /dev/null +++ b/app/client/src/git/store/actions/uiActions.ts @@ -0,0 +1,71 @@ +import type { GitOpsTab, GitSettingsTab } from "git/constants/enums"; +import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction"; + +interface ToggleRepoLimitModalPayload { + open: boolean; +} + +export const toggleRepoLimitErrorModalAction = + createSingleArtifactAction((state, action) => { + const { open } = action.payload; + + state.ui.repoLimitErrorModal.open = open; + + return state; + }); + +interface BranchListPopupPayload { + open: boolean; +} + +export const toggleGitBranchListPopupAction = + createSingleArtifactAction((state, action) => { + const { open } = action.payload; + + state.ui.branchListPopup.open = open; + + return state; + }); + +export interface ToggleGitOpsModalPayload { + open: boolean; + tab: keyof typeof GitOpsTab; +} + +export const toggleGitOpsModalAction = + createSingleArtifactAction((state, action) => { + const { open, tab } = action.payload; + + state.ui.opsModal.open = open; + state.ui.opsModal.tab = tab; + + return state; + }); + +export interface ToggleGitSettingsModalPayload { + open: boolean; + tab: keyof typeof GitSettingsTab; +} + +export const toggleGitSettingsModalAction = + createSingleArtifactAction((state, action) => { + const { open, tab } = action.payload; + + state.ui.settingsModal.open = open; + state.ui.settingsModal.tab = tab; + + return state; + }); + +export interface ToggleGitConnectModalPayload { + open: boolean; +} + +export const toggleGitConnectModalAction = + createSingleArtifactAction((state, action) => { + const { open } = action.payload; + + state.ui.connectModal.open = open; + + return state; + }); diff --git a/app/client/src/git/actions/updateGlobalProfileActions.ts b/app/client/src/git/store/actions/updateGlobalProfileActions.ts similarity index 100% rename from app/client/src/git/actions/updateGlobalProfileActions.ts rename to app/client/src/git/store/actions/updateGlobalProfileActions.ts diff --git a/app/client/src/git/actions/updateLocalProfileActions.ts b/app/client/src/git/store/actions/updateLocalProfileActions.ts similarity index 92% rename from app/client/src/git/actions/updateLocalProfileActions.ts rename to app/client/src/git/store/actions/updateLocalProfileActions.ts index 839175016d2a..717bb388cb9b 100644 --- a/app/client/src/git/actions/updateLocalProfileActions.ts +++ b/app/client/src/git/store/actions/updateLocalProfileActions.ts @@ -1,4 +1,4 @@ -import { createSingleArtifactAction } from "./helpers/createSingleArtifactAction"; +import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction"; import type { GitAsyncErrorPayload } from "../types"; import type { UpdateLocalProfileRequestParams } from "git/requests/updateLocalProfileRequest.types"; diff --git a/app/client/src/git/actions/updateProtectedBranchesActions.ts b/app/client/src/git/store/actions/updateProtectedBranchesActions.ts similarity index 90% rename from app/client/src/git/actions/updateProtectedBranchesActions.ts rename to app/client/src/git/store/actions/updateProtectedBranchesActions.ts index d20fb52cd591..8a90e5889d8d 100644 --- a/app/client/src/git/actions/updateProtectedBranchesActions.ts +++ b/app/client/src/git/store/actions/updateProtectedBranchesActions.ts @@ -1,4 +1,4 @@ -import { createSingleArtifactAction } from "./helpers/createSingleArtifactAction"; +import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction"; import type { GitArtifactErrorPayloadAction } from "../types"; export const updateProtectedBranchesInitAction = createSingleArtifactAction( diff --git a/app/client/src/git/store/gitArtifactSlice.ts b/app/client/src/git/store/gitArtifactSlice.ts index 343661634d36..da29a9f54b4a 100644 --- a/app/client/src/git/store/gitArtifactSlice.ts +++ b/app/client/src/git/store/gitArtifactSlice.ts @@ -1,82 +1,151 @@ /* eslint-disable padding-line-between-statements */ import { createSlice } from "@reduxjs/toolkit"; -import type { GitArtifactReduxState } from "../types"; -import { mountAction, unmountAction } from "../actions/mountActions"; +import type { GitArtifactReduxState } from "./types"; +import { mountAction, unmountAction } from "./actions/mountActions"; import { connectErrorAction, connectInitAction, connectSuccessAction, -} from "../actions/connectActions"; +} from "./actions/connectActions"; import { fetchMetadataErrorAction, fetchMetadataInitAction, fetchMetadataSuccessAction, -} from "../actions/fetchMetadataActions"; +} from "./actions/fetchMetadataActions"; import { fetchBranchesErrorAction, fetchBranchesInitAction, fetchBranchesSuccessAction, -} from "../actions/fetchBranchesActions"; +} from "./actions/fetchBranchesActions"; import { fetchStatusErrorAction, fetchStatusInitAction, fetchStatusSuccessAction, -} from "../actions/fetchStatusActions"; +} from "./actions/fetchStatusActions"; import { commitErrorAction, commitInitAction, commitSuccessAction, -} from "../actions/commitActions"; +} from "./actions/commitActions"; import { pullErrorAction, pullInitAction, pullSuccessAction, -} from "../actions/pullActions"; -import { toggleRepoLimitErrorModalAction } from "../actions/repoLimitErrorModalActions"; +} from "./actions/pullActions"; import { fetchLocalProfileErrorAction, fetchLocalProfileInitAction, fetchLocalProfileSuccessAction, -} from "git/actions/fetchLocalProfileActions"; +} from "./actions/fetchLocalProfileActions"; import { updateLocalProfileErrorAction, updateLocalProfileInitAction, updateLocalProfileSuccessAction, -} from "git/actions/updateLocalProfileActions"; +} from "./actions/updateLocalProfileActions"; +import { + createBranchErrorAction, + createBranchInitAction, + createBranchSuccessAction, +} from "./actions/createBranchActions"; +import { + deleteBranchErrorAction, + deleteBranchInitAction, + deleteBranchSuccessAction, +} from "./actions/deleteBranchActions"; +import { + toggleGitBranchListPopupAction, + toggleGitConnectModalAction, + toggleGitOpsModalAction, + toggleGitSettingsModalAction, + toggleRepoLimitErrorModalAction, +} from "./actions/uiActions"; +import { + checkoutBranchErrorAction, + checkoutBranchInitAction, + checkoutBranchSuccessAction, +} from "./actions/checkoutBranchActions"; +import { + discardErrorAction, + discardInitAction, + discardSuccessAction, +} from "./actions/discardActions"; +import { + fetchMergeStatusErrorAction, + fetchMergeStatusInitAction, + fetchMergeStatusSuccessAction, +} from "./actions/fetchMergeStatusActions"; +import { + mergeErrorAction, + mergeInitAction, + mergeSuccessAction, +} from "./actions/mergeActions"; const initialState: GitArtifactReduxState = {}; export const gitArtifactSlice = createSlice({ name: "git/artifact", + reducerPath: "git.artifact", initialState, reducers: { mount: mountAction, unmount: unmountAction, + + // connect connectInit: connectInitAction, connectSuccess: connectSuccessAction, connectError: connectErrorAction, - fetchMetadataInit: fetchMetadataInitAction, - fetchMetadataSuccess: fetchMetadataSuccessAction, - fetchMetadataError: fetchMetadataErrorAction, - fetchBranchesInit: fetchBranchesInitAction, - fetchBranchesSuccess: fetchBranchesSuccessAction, - fetchBranchesError: fetchBranchesErrorAction, - fetchStatusInit: fetchStatusInitAction, - fetchStatusSuccess: fetchStatusSuccessAction, - fetchStatusError: fetchStatusErrorAction, + toggleGitConnectModal: toggleGitConnectModalAction, + toggleRepoLimitErrorModal: toggleRepoLimitErrorModalAction, + + // git ops commitInit: commitInitAction, commitSuccess: commitSuccessAction, commitError: commitErrorAction, + discardInit: discardInitAction, + discardSuccess: discardSuccessAction, + discardError: discardErrorAction, + fetchStatusInit: fetchStatusInitAction, + fetchStatusSuccess: fetchStatusSuccessAction, + fetchStatusError: fetchStatusErrorAction, + fetchMergeStatusInit: fetchMergeStatusInitAction, + fetchMergeStatusSuccess: fetchMergeStatusSuccessAction, + fetchMergeStatusError: fetchMergeStatusErrorAction, + mergeInit: mergeInitAction, + mergeSuccess: mergeSuccessAction, + mergeError: mergeErrorAction, pullInit: pullInitAction, pullSuccess: pullSuccessAction, pullError: pullErrorAction, + toggleGitOpsModal: toggleGitOpsModalAction, + + // branches + fetchBranchesInit: fetchBranchesInitAction, + fetchBranchesSuccess: fetchBranchesSuccessAction, + fetchBranchesError: fetchBranchesErrorAction, + createBranchInit: createBranchInitAction, + createBranchSuccess: createBranchSuccessAction, + createBranchError: createBranchErrorAction, + deleteBranchInit: deleteBranchInitAction, + deleteBranchSuccess: deleteBranchSuccessAction, + deleteBranchError: deleteBranchErrorAction, + checkoutBranchInit: checkoutBranchInitAction, + checkoutBranchSuccess: checkoutBranchSuccessAction, + checkoutBranchError: checkoutBranchErrorAction, + toggleGitBranchListPopup: toggleGitBranchListPopupAction, + + // settings + toggleGitSettingsModal: toggleGitSettingsModalAction, + + // metadata + fetchMetadataInit: fetchMetadataInitAction, + fetchMetadataSuccess: fetchMetadataSuccessAction, + fetchMetadataError: fetchMetadataErrorAction, fetchLocalProfileInit: fetchLocalProfileInitAction, fetchLocalProfileSuccess: fetchLocalProfileSuccessAction, fetchLocalProfileError: fetchLocalProfileErrorAction, updateLocalProfileInit: updateLocalProfileInitAction, updateLocalProfileSuccess: updateLocalProfileSuccessAction, updateLocalProfileError: updateLocalProfileErrorAction, - toggleRepoLimitErrorModal: toggleRepoLimitErrorModalAction, }, }); diff --git a/app/client/src/git/store/gitConfigSlice.ts b/app/client/src/git/store/gitConfigSlice.ts index 14cf0971b643..a07a45d9730b 100644 --- a/app/client/src/git/store/gitConfigSlice.ts +++ b/app/client/src/git/store/gitConfigSlice.ts @@ -3,29 +3,17 @@ import { fetchGlobalProfileErrorAction, fetchGlobalProfileInitAction, fetchGlobalProfileSuccessAction, -} from "git/actions/fetchGlobalProfileActions"; +} from "./actions/fetchGlobalProfileActions"; import { updateGlobalProfileErrorAction, updateGlobalProfileInitAction, updateGlobalProfileSuccessAction, -} from "git/actions/updateGlobalProfileActions"; -import type { GitConfigReduxState } from "git/types"; - -const initialState: GitConfigReduxState = { - globalProfile: { - value: null, - loading: false, - error: null, - }, - updateGlobalProfile: { - loading: false, - error: null, - }, -}; +} from "./actions/updateGlobalProfileActions"; +import { gitConfigInitialState } from "./helpers/gitConfigInitialState"; export const gitConfigSlice = createSlice({ name: "git/config", - initialState, + initialState: gitConfigInitialState, reducers: { fetchGlobalProfileInit: fetchGlobalProfileInitAction, fetchGlobalProfileSuccess: fetchGlobalProfileSuccessAction, diff --git a/app/client/src/git/actions/helpers/createSingleArtifactAction.ts b/app/client/src/git/store/helpers/createSingleArtifactAction.ts similarity index 89% rename from app/client/src/git/actions/helpers/createSingleArtifactAction.ts rename to app/client/src/git/store/helpers/createSingleArtifactAction.ts index f983f1a27190..2e0642959be8 100644 --- a/app/client/src/git/actions/helpers/createSingleArtifactAction.ts +++ b/app/client/src/git/store/helpers/createSingleArtifactAction.ts @@ -3,8 +3,8 @@ import type { GitArtifactPayloadAction, GitArtifactReduxState, GitSingleArtifactReduxState, -} from "../../types"; -import { gitSingleArtifactInitialState } from "./singleArtifactInitialState"; +} from "../types"; +import { gitSingleArtifactInitialState } from "./gitSingleArtifactInitialState"; type SingleArtifactStateCb = ( singleArtifactState: GitSingleArtifactReduxState, diff --git a/app/client/src/git/store/helpers/gitConfigInitialState.ts b/app/client/src/git/store/helpers/gitConfigInitialState.ts new file mode 100644 index 000000000000..7017399dfb16 --- /dev/null +++ b/app/client/src/git/store/helpers/gitConfigInitialState.ts @@ -0,0 +1,13 @@ +import type { GitConfigReduxState } from "../types"; + +export const gitConfigInitialState: GitConfigReduxState = { + globalProfile: { + value: null, + loading: false, + error: null, + }, + updateGlobalProfile: { + loading: false, + error: null, + }, +}; diff --git a/app/client/src/git/actions/helpers/singleArtifactInitialState.ts b/app/client/src/git/store/helpers/gitSingleArtifactInitialState.ts similarity index 98% rename from app/client/src/git/actions/helpers/singleArtifactInitialState.ts rename to app/client/src/git/store/helpers/gitSingleArtifactInitialState.ts index d4155ffe3fd6..a194106914a3 100644 --- a/app/client/src/git/actions/helpers/singleArtifactInitialState.ts +++ b/app/client/src/git/store/helpers/gitSingleArtifactInitialState.ts @@ -8,7 +8,7 @@ import type { GitSingleArtifactAPIResponsesReduxState, GitSingleArtifactUIReduxState, GitSingleArtifactReduxState, -} from "../../types"; +} from "../types"; const gitSingleArtifactInitialUIState: GitSingleArtifactUIReduxState = { connectModal: { @@ -19,7 +19,7 @@ const gitSingleArtifactInitialUIState: GitSingleArtifactUIReduxState = { open: false, step: GitImportStep.Provider, }, - branchList: { + branchListPopup: { open: false, }, opsModal: { diff --git a/app/client/src/git/store/selectors/gitSingleArtifactSelectors.ts b/app/client/src/git/store/selectors/gitSingleArtifactSelectors.ts new file mode 100644 index 000000000000..e67bcdc99b4a --- /dev/null +++ b/app/client/src/git/store/selectors/gitSingleArtifactSelectors.ts @@ -0,0 +1,64 @@ +import type { GitArtifactType } from "git/constants/enums"; +import type { GitRootState } from "../types"; + +interface GitArtifactDef { + artifactType: keyof typeof GitArtifactType; + baseArtifactId: string; +} + +export const selectSingleArtifact = ( + state: GitRootState, + artifactDef: GitArtifactDef, +) => { + return state.git.artifacts[artifactDef.artifactType]?.[ + artifactDef.baseArtifactId + ]; +}; + +// git ops +export const selectCommit = ( + state: GitRootState, + artifactDef: GitArtifactDef, +) => selectSingleArtifact(state, artifactDef)?.apiResponses?.commit; + +export const selectDiscard = ( + state: GitRootState, + artifactDef: GitArtifactDef, +) => selectSingleArtifact(state, artifactDef)?.apiResponses?.discard; + +export const selectStatus = ( + state: GitRootState, + artifactDef: GitArtifactDef, +) => selectSingleArtifact(state, artifactDef)?.apiResponses?.status; + +export const selectMerge = (state: GitRootState, artifactDef: GitArtifactDef) => + selectSingleArtifact(state, artifactDef)?.apiResponses?.merge; + +export const selectMergeStatus = ( + state: GitRootState, + artifactDef: GitArtifactDef, +) => selectSingleArtifact(state, artifactDef)?.apiResponses?.mergeStatus; + +export const selectPull = (state: GitRootState, artifactDef: GitArtifactDef) => + selectSingleArtifact(state, artifactDef)?.apiResponses?.pull; + +// git branches +export const selectBranches = ( + state: GitRootState, + artifactDef: GitArtifactDef, +) => selectSingleArtifact(state, artifactDef)?.apiResponses?.branches; + +export const selectCreateBranch = ( + state: GitRootState, + artifactDef: GitArtifactDef, +) => selectSingleArtifact(state, artifactDef)?.apiResponses?.createBranch; + +export const selectDeleteBranch = ( + state: GitRootState, + artifactDef: GitArtifactDef, +) => selectSingleArtifact(state, artifactDef)?.apiResponses?.deleteBranch; + +export const selectCheckoutBranch = ( + state: GitRootState, + artifactDef: GitArtifactDef, +) => selectSingleArtifact(state, artifactDef)?.apiResponses.checkoutBranch; diff --git a/app/client/src/git/types.ts b/app/client/src/git/store/types.ts similarity index 78% rename from app/client/src/git/types.ts rename to app/client/src/git/store/types.ts index 78d9c69e8ab0..695572840d93 100644 --- a/app/client/src/git/types.ts +++ b/app/client/src/git/store/types.ts @@ -5,27 +5,23 @@ import type { GitImportStep, GitOpsTab, GitSettingsTab, -} from "./constants/enums"; -import type { FetchGlobalProfileResponseData } from "./requests/fetchGlobalProfileRequest.types"; -import type { FetchBranchesResponseData } from "./requests/fetchBranchesRequest.types"; -import type { FetchLocalProfileResponseData } from "./requests/fetchLocalProfileRequest.types"; +} from "../constants/enums"; +import type { FetchGlobalProfileResponseData } from "../requests/fetchGlobalProfileRequest.types"; +import type { FetchBranchesResponseData } from "../requests/fetchBranchesRequest.types"; +import type { FetchLocalProfileResponseData } from "../requests/fetchLocalProfileRequest.types"; +import type { FetchStatusResponseData } from "git/requests/fetchStatusRequest.types"; +import type { FetchMergeStatusResponseData } from "git/requests/fetchMergeStatusRequest.types"; // These will be updated when contracts are finalized export type GitMetadata = Record; -export type GitStatus = Record; - -export type GitMergeStatus = Record; - -export type GitLocalProfile = Record; - export type GitProtectedBranches = Record; export type GitAutocommitProgress = Record; export type GitSSHKey = Record; -interface AsyncState { +export interface AsyncState { value: T | null; loading: boolean; error: string | null; @@ -38,11 +34,11 @@ interface AsyncStateWithoutValue { export interface GitSingleArtifactAPIResponsesReduxState { metadata: AsyncState; connect: AsyncStateWithoutValue; - status: AsyncState; + status: AsyncState; commit: AsyncStateWithoutValue; pull: AsyncStateWithoutValue; discard: AsyncStateWithoutValue; - mergeStatus: AsyncState; + mergeStatus: AsyncState; merge: AsyncStateWithoutValue; branches: AsyncState; checkoutBranch: AsyncStateWithoutValue; @@ -69,7 +65,7 @@ export interface GitSingleArtifactUIReduxState { open: boolean; step: keyof typeof GitImportStep; }; - branchList: { + branchListPopup: { open: boolean; }; opsModal: { @@ -98,6 +94,13 @@ export interface GitConfigReduxState { updateGlobalProfile: AsyncStateWithoutValue; } +export interface GitRootState { + git: { + artifacts: GitArtifactReduxState; + config: GitConfigReduxState; + }; +} + export interface GitArtifactBasePayload { artifactType: keyof typeof GitArtifactType; baseArtifactId: string;