chore: git mod - adding init saga and connecting components to ctx (#38088)

## Description
- Adds more selectors
- Adds more sagas
- Introduces init steps for git
- Improvements upon CtxAwareGitQuickActions

Fixes #37800 
Fixes #36814 


## Automation

/ok-to-test tags="@tag.Git"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/12295168481>
> Commit: ea1ab497cad677aac36f044462e623e526d421d1
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=12295168481&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.Git`
> Spec:
> <hr>Thu, 12 Dec 2024 12:06:09 UTC
<!-- end of auto-generated comment: Cypress test results  -->


## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [ ] No


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary by CodeRabbit

- **New Features**
  - Introduced a context provider for managing Git-related state.
  - Added a `GitQuickActions` component for various Git actions.
- Implemented custom hooks for managing Git branches, metadata,
operations, and settings.
- Added Redux Saga functions for fetching Git metadata and protected
branches.
  - Enhanced autocommit functionality with polling for progress.
- Introduced actions for managing the autocommit process and fetching
protected branches.
- Added new actions to initialize Git for editor context and manage
loading states.

- **Bug Fixes**
  - Improved error handling in various action creators and sagas.

- **Chores**
- Updated action creators and types for better type safety and clarity.
- Refined test cases for components to ensure accurate button selection.
- Modified request functions to utilize Axios promises directly for
better consistency.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Rudraprasad Das 2024-12-12 23:18:09 +08:00 committed by GitHub
parent 8e30a389e4
commit 0808b279fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 782 additions and 230 deletions

View File

@ -5,9 +5,13 @@ import useStatusChangeCount from "./hooks/useStatusChangeCount";
function CtxAwareGitQuickActions() {
const {
autocommitEnabled,
autocommitPolling,
discard,
discardLoading,
fetchStatusLoading,
gitConnected,
protectedMode,
pull,
pullError,
pullLoading,
@ -17,12 +21,7 @@ function CtxAwareGitQuickActions() {
toggleGitSettingsModal,
} = useGitContext();
const isGitConnected = false;
const isAutocommitEnabled = true;
const isAutocommitPolling = false;
const isConnectPermitted = true;
const isProtectedMode = false;
const connectPermitted = true;
const isPullFailing = !!pullError;
const isStatusClean = status?.isClean ?? false;
const statusBehindCount = status?.behindCount ?? 0;
@ -31,13 +30,13 @@ function CtxAwareGitQuickActions() {
return (
<GitQuickActions
discard={discard}
isAutocommitEnabled={isAutocommitEnabled}
isAutocommitPolling={isAutocommitPolling}
isConnectPermitted={isConnectPermitted}
isAutocommitEnabled={autocommitEnabled}
isAutocommitPolling={autocommitPolling}
isConnectPermitted={connectPermitted}
isDiscardLoading={discardLoading}
isFetchStatusLoading={fetchStatusLoading}
isGitConnected={isGitConnected}
isProtectedMode={isProtectedMode}
isGitConnected={gitConnected}
isProtectedMode={protectedMode}
isPullFailing={isPullFailing}
isPullLoading={pullLoading}
isStatusClean={isStatusClean}

View File

@ -113,18 +113,18 @@ export default function useGitBranches({
};
return {
branches: branchesState?.value ?? null,
branches: branchesState?.value,
fetchBranchesLoading: branchesState?.loading ?? false,
fetchBranchesError: branchesState?.error ?? null,
fetchBranchesError: branchesState?.error,
fetchBranches,
createBranchLoading: createBranchState?.loading ?? false,
createBranchError: createBranchState?.error ?? null,
createBranchError: createBranchState?.error,
createBranch,
deleteBranchLoading: deleteBranchState?.loading ?? false,
deleteBranchError: deleteBranchState?.error ?? null,
deleteBranchError: deleteBranchState?.error,
deleteBranch,
checkoutBranchLoading: checkoutBranchState?.loading ?? false,
checkoutBranchError: checkoutBranchState?.error ?? null,
checkoutBranchError: checkoutBranchState?.error,
checkoutBranch,
toggleGitBranchListPopup,
};

View File

@ -8,6 +8,8 @@ import useGitOps from "./useGitOps";
import useGitBranches from "./useGitBranches";
import useGitSettings from "./useGitSettings";
import { useMemo } from "react";
import type { UseGitMetadataReturnValue } from "./useGitMetadata";
import useGitMetadata from "./useGitMetadata";
interface UseGitContextValueParams {
artifactType: keyof typeof GitArtifactType;
@ -15,7 +17,8 @@ interface UseGitContextValueParams {
}
export interface GitContextValue
extends UseGitConnectReturnValue,
extends UseGitMetadataReturnValue,
UseGitConnectReturnValue,
UseGitOpsReturnValue,
UseGitSettingsReturnValue,
UseGitBranchesReturnValue {}
@ -28,12 +31,14 @@ export default function useGitContextValue({
() => ({ artifactType, baseArtifactId }),
[artifactType, baseArtifactId],
);
const useGitMetadataReturnValue = useGitMetadata(basePayload);
const useGitConnectReturnValue = useGitConnect(basePayload);
const useGitOpsReturnValue = useGitOps(basePayload);
const useGitBranchesReturnValue = useGitBranches(basePayload);
const useGitSettingsReturnValue = useGitSettings(basePayload);
return {
...useGitMetadataReturnValue,
...useGitOpsReturnValue,
...useGitBranchesReturnValue,
...useGitConnectReturnValue,

View File

@ -0,0 +1,45 @@
import type { GitArtifactType } from "git/constants/enums";
import type { FetchGitMetadataResponseData } from "git/requests/fetchGitMetadataRequest.types";
import {
selectGitConnected,
selectGitMetadata,
} from "git/store/selectors/gitSingleArtifactSelectors";
import type { GitRootState } from "git/store/types";
import { useMemo } from "react";
import { useSelector } from "react-redux";
interface UseGitMetadataParams {
artifactType: keyof typeof GitArtifactType;
baseArtifactId: string;
}
export interface UseGitMetadataReturnValue {
gitMetadata: FetchGitMetadataResponseData | null;
fetchGitMetadataLoading: boolean;
fetchGitMetadataError: string | null;
gitConnected: boolean;
}
export default function useGitMetadata({
artifactType,
baseArtifactId,
}: UseGitMetadataParams): UseGitMetadataReturnValue {
const basePayload = useMemo(
() => ({ artifactType, baseArtifactId }),
[artifactType, baseArtifactId],
);
const gitMetadataState = useSelector((state: GitRootState) =>
selectGitMetadata(state, basePayload),
);
const gitConnected = useSelector((state: GitRootState) =>
selectGitConnected(state, basePayload),
);
return {
gitMetadata: gitMetadataState.value,
fetchGitMetadataLoading: gitMetadataState.loading ?? false,
fetchGitMetadataError: gitMetadataState.error,
gitConnected: gitConnected ?? false,
};
}

View File

@ -133,24 +133,24 @@ export default function useGitOps({
return {
commitLoading: commitState?.loading ?? false,
commitError: commitState?.error ?? null,
commitError: commitState?.error,
commit,
discardLoading: discardState?.loading ?? false,
discardError: discardState?.error ?? null,
discardError: discardState?.error,
discard,
status: statusState?.value ?? null,
status: statusState?.value,
fetchStatusLoading: statusState?.loading ?? false,
fetchStatusError: statusState?.error ?? null,
fetchStatusError: statusState?.error,
fetchStatus,
mergeLoading: mergeState?.loading ?? false,
mergeError: mergeState?.error ?? null,
mergeError: mergeState?.error,
merge,
mergeStatus: mergeStatusState?.value ?? null,
mergeStatus: mergeStatusState?.value,
fetchMergeStatusLoading: mergeStatusState?.loading ?? false,
fetchMergeStatusError: mergeStatusState?.error ?? null,
fetchMergeStatusError: mergeStatusState?.error,
fetchMergeStatus,
pullLoading: pullState?.loading ?? false,
pullError: pullState?.error ?? null,
pullError: pullState?.error,
pull,
toggleGitOpsModal,
};

View File

@ -1,7 +1,15 @@
import type { GitArtifactType, GitSettingsTab } from "git/constants/enums";
import type { FetchProtectedBranchesResponseData } from "git/requests/fetchProtectedBranchesRequest.types";
import { gitArtifactActions } from "git/store/gitArtifactSlice";
import {
selectAutocommitEnabled,
selectAutocommitPolling,
selectProtectedBranches,
selectProtectedMode,
} from "git/store/selectors/gitSingleArtifactSelectors";
import type { GitRootState } from "git/store/types";
import { useMemo } from "react";
import { useDispatch } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
interface UseGitSettingsParams {
artifactType: keyof typeof GitArtifactType;
@ -9,6 +17,13 @@ interface UseGitSettingsParams {
}
export interface UseGitSettingsReturnValue {
autocommitEnabled: boolean;
autocommitPolling: boolean;
protectedBranches: FetchProtectedBranchesResponseData | null;
fetchProtectedBranchesLoading: boolean;
fetchProtectedBranchesError: string | null;
fetchProtectedBranches: () => void;
protectedMode: boolean;
toggleGitSettingsModal: (
open: boolean,
tab: keyof typeof GitSettingsTab,
@ -25,6 +40,33 @@ export default function useGitSettings({
[artifactType, baseArtifactId],
);
// autocommit
const autocommitEnabled = useSelector((state: GitRootState) =>
selectAutocommitEnabled(state, basePayload),
);
const autocommitPolling = useSelector((state: GitRootState) =>
selectAutocommitPolling(state, basePayload),
);
// branch protection
const protectedBranchesState = useSelector((state: GitRootState) =>
selectProtectedBranches(state, basePayload),
);
const fetchProtectedBranches = () => {
dispatch(
gitArtifactActions.fetchProtectedBranchesInit({
...basePayload,
}),
);
};
const protectedMode = useSelector((state: GitRootState) =>
selectProtectedMode(state, basePayload),
);
// ui
const toggleGitSettingsModal = (
open: boolean,
tab: keyof typeof GitSettingsTab,
@ -39,6 +81,13 @@ export default function useGitSettings({
};
return {
autocommitEnabled: autocommitEnabled ?? false,
autocommitPolling: autocommitPolling ?? false,
protectedBranches: protectedBranchesState.value,
fetchProtectedBranchesLoading: protectedBranchesState.loading ?? false,
fetchProtectedBranchesError: protectedBranchesState.error,
fetchProtectedBranches,
protectedMode: protectedMode ?? false,
toggleGitSettingsModal,
};
}

View File

@ -1,4 +1,4 @@
import React, { createContext, useContext, useEffect } from "react";
import React, { createContext, useContext } from "react";
import type { GitArtifactType } from "git/constants/enums";
import type { GitContextValue } from "./hooks/useGitContextValue";
import useGitContextValue from "./hooks/useGitContextValue";
@ -15,24 +15,18 @@ interface GitContextProviderProps {
artifactType: keyof typeof GitArtifactType;
baseArtifactId: string;
children: React.ReactNode;
// extra
// connectPermitted?: boolean;
}
export default function GitContextProvider({
artifactType,
baseArtifactId,
children,
// connectPermitted = true,
}: GitContextProviderProps) {
const contextValue = useGitContextValue({ artifactType, baseArtifactId });
const { fetchBranches } = contextValue;
useEffect(
function gitInitEffect() {
fetchBranches();
},
[fetchBranches],
);
return (
<GitContext.Provider value={contextValue}>{children}</GitContext.Provider>
);

View File

@ -50,7 +50,7 @@ describe("QuickActionButton", () => {
<QuickActionButton {...defaultProps} />
</ThemeProvider>,
);
const btn = container.getElementsByClassName("t--test-btn")[0];
const btn = container.querySelectorAll(".t--test-btn button")[0];
fireEvent.click(btn);
expect(defaultProps.onClick).toHaveBeenCalledTimes(1);

View File

@ -19,12 +19,11 @@ const SpinnerContainer = styled.div`
padding: 0 10px;
`;
const QuickActionButtonContainer = styled.button<{ disabled?: boolean }>`
const QuickActionButtonContainer = styled.div<{ disabled?: boolean }>`
margin: 0 ${(props) => props.theme.spaces[1]}px;
display: block;
position: relative;
overflow: visible;
cursor: ${({ disabled = false }) => (disabled ? "not-allowed" : "pointer")};
opacity: ${({ disabled = false }) => (disabled ? 0.6 : 1)};
`;
@ -57,11 +56,7 @@ function QuickActionButton({
const content = capitalizeFirstLetter(tooltipText);
return (
<QuickActionButtonContainer
className={className}
disabled={disabled}
onClick={onClick}
>
<QuickActionButtonContainer className={className} disabled={disabled}>
{loading ? (
<SpinnerContainer className="t--loader-quick-git-action">
<SpinnerLoader size="md" />
@ -73,6 +68,7 @@ function QuickActionButton({
isDisabled={disabled}
isIconButton
kind="tertiary"
onClick={onClick}
size="md"
startIcon={icon}
/>

View File

@ -110,8 +110,8 @@ describe("QuickActions Component", () => {
<QuickActions {...props} />
</ThemeProvider>,
);
const commitButton = container.getElementsByClassName(
"t--bottom-bar-commit",
const commitButton = container.querySelectorAll(
".t--bottom-bar-commit button",
)[0];
fireEvent.click(commitButton);
@ -145,8 +145,9 @@ describe("QuickActions Component", () => {
<QuickActions {...props} />
</ThemeProvider>,
);
const pullButton =
container.getElementsByClassName("t--bottom-bar-pull")[0];
const pullButton = container.querySelectorAll(
".t--bottom-bar-pull button",
)[0];
fireEvent.click(pullButton);
expect(AnalyticsUtil.logEvent).toHaveBeenCalledWith("GS_PULL_GIT_CLICK", {
@ -165,8 +166,8 @@ describe("QuickActions Component", () => {
<QuickActions {...props} />
</ThemeProvider>,
);
const mergeButton = container.getElementsByClassName(
"t--bottom-bar-merge",
const mergeButton = container.querySelectorAll(
".t--bottom-bar-merge button",
)[0];
fireEvent.click(mergeButton);
@ -190,8 +191,8 @@ describe("QuickActions Component", () => {
<QuickActions {...props} />
</ThemeProvider>,
);
const settingsButton = container.getElementsByClassName(
"t--bottom-git-settings",
const settingsButton = container.querySelectorAll(
".t--bottom-git-settings button",
)[0];
fireEvent.click(settingsButton);
@ -216,8 +217,8 @@ describe("QuickActions Component", () => {
<QuickActions {...props} />
</ThemeProvider>,
);
const commitButton = container.getElementsByClassName(
"t--bottom-bar-commit",
const commitButton = container.querySelectorAll(
".t--bottom-bar-commit button",
)[0];
expect(commitButton).toBeDisabled();
@ -298,8 +299,9 @@ describe("QuickActions Component", () => {
<QuickActions {...props} />
</ThemeProvider>,
);
const pullButton =
container.getElementsByClassName("t--bottom-bar-pull")[0];
const pullButton = container.querySelectorAll(
".t--bottom-bar-pull button",
)[0];
expect(pullButton).toBeDisabled();
});

View File

@ -95,6 +95,7 @@ function GitQuickActions({
if (isProtectedMode) {
discard();
} else {
// ! case: why is triggeredFromBottomBar this needed?
// pull({ triggeredFromBottomBar: true });
pull();
}

View File

@ -1,4 +1,4 @@
import type { AxiosResponse } from "axios";
import type { AxiosPromise } from "axios";
import type {
CheckoutBranchRequestParams,
CheckoutBranchResponse,
@ -9,7 +9,7 @@ import Api from "api/Api";
export default async function checkoutBranchRequest(
branchedApplicationId: string,
params: CheckoutBranchRequestParams,
): Promise<AxiosResponse<CheckoutBranchResponse>> {
): AxiosPromise<CheckoutBranchResponse> {
return Api.get(
`${GIT_BASE_URL}/checkout-branch/app/${branchedApplicationId}`,
params,

View File

@ -4,12 +4,12 @@ import type {
CommitResponse,
} from "./commitRequest.types";
import { GIT_BASE_URL } from "./constants";
import type { AxiosResponse } from "axios";
import type { AxiosPromise } from "axios";
export default async function commitRequest(
branchedApplicationId: string,
params: CommitRequestParams,
): Promise<AxiosResponse<CommitResponse>> {
): AxiosPromise<CommitResponse> {
return Api.post(
`${GIT_BASE_URL}/commit/app/${branchedApplicationId}`,
params,

View File

@ -1,4 +1,4 @@
import type { AxiosResponse } from "axios";
import type { AxiosPromise } from "axios";
import type {
CreateBranchRequestParams,
CreateBranchResponse,
@ -9,7 +9,7 @@ import Api from "api/Api";
export default async function createBranchRequest(
branchedApplicationId: string,
params: CreateBranchRequestParams,
): Promise<AxiosResponse<CreateBranchResponse>> {
): AxiosPromise<CreateBranchResponse> {
return Api.post(
`${GIT_BASE_URL}/create-branch/app/${branchedApplicationId}`,
params,

View File

@ -1,4 +1,4 @@
import type { AxiosResponse } from "axios";
import type { AxiosPromise } from "axios";
import type {
DeleteBranchRequestParams,
DeleteBranchResponse,
@ -9,6 +9,6 @@ import Api from "api/Api";
export default async function deleteBranchRequest(
baseApplicationId: string,
params: DeleteBranchRequestParams,
): Promise<AxiosResponse<DeleteBranchResponse>> {
): AxiosPromise<DeleteBranchResponse> {
return Api.delete(`${GIT_BASE_URL}/branch/app/${baseApplicationId}`, params);
}

View File

@ -1,9 +1,9 @@
import Api from "api/Api";
import { GIT_BASE_URL } from "./constants";
import type { AxiosResponse } from "axios";
import type { AxiosPromise } from "axios";
export default async function discardRequest(
branchedApplicationId: string,
): Promise<AxiosResponse<void>> {
): AxiosPromise<void> {
return Api.put(`${GIT_BASE_URL}/discard/app/${branchedApplicationId}`);
}

View File

@ -1,10 +1,10 @@
import type { AxiosResponse } from "axios";
import type { AxiosPromise } from "axios";
import { GIT_BASE_URL } from "./constants";
import type { DisconnectResponse } from "./disconnectRequest.types";
import Api from "api/Api";
export default async function disconnectRequest(
baseApplicationId: string,
): Promise<AxiosResponse<DisconnectResponse>> {
): AxiosPromise<DisconnectResponse> {
return Api.post(`${GIT_BASE_URL}/disconnect/app/${baseApplicationId}`);
}

View File

@ -1,11 +1,11 @@
import Api from "api/Api";
import { GIT_BASE_URL } from "./constants";
import type { AxiosResponse } from "axios";
import type { AxiosPromise } from "axios";
import type { FetchAutocommitProgressResponse } from "./fetchAutocommitProgressRequest.types";
export default async function fetchAutocommitProgressRequest(
baseApplicationId: string,
): Promise<AxiosResponse<FetchAutocommitProgressResponse>> {
): AxiosPromise<FetchAutocommitProgressResponse> {
return Api.get(
`${GIT_BASE_URL}/auto-commit/progress/app/${baseApplicationId}`,
);

View File

@ -1,7 +1,11 @@
import type { ApiResponse } from "api/types";
import type { AutocommitStatus } from "../constants/enums";
export interface FetchAutocommitProgressResponse {
export interface FetchAutocommitProgressResponseData {
autoCommitResponse: AutocommitStatus;
progress: number;
branchName: string;
}
export type FetchAutocommitProgressResponse =
ApiResponse<FetchAutocommitProgressResponseData>;

View File

@ -8,7 +8,7 @@ import type { AxiosPromise } from "axios";
export default async function fetchBranchesRequest(
branchedApplicationId: string,
params: FetchBranchesRequestParams,
params: FetchBranchesRequestParams = { pruneBranches: true },
): AxiosPromise<FetchBranchesResponse> {
const queryParams = {} as FetchBranchesRequestParams;

View File

@ -1,7 +1,7 @@
import type { ApiResponse } from "api/ApiResponses";
export interface FetchBranchesRequestParams {
pruneBranches: boolean;
pruneBranches?: boolean;
}
interface SingleBranch {

View File

@ -1,10 +1,10 @@
import Api from "api/Api";
import { GIT_BASE_URL } from "./constants";
import type { AxiosResponse } from "axios";
import type { AxiosPromise } from "axios";
import type { FetchGitMetadataResponse } from "./fetchGitMetadataRequest.types";
export default async function fetchGitMetadataRequest(
baseApplicationId: string,
): Promise<AxiosResponse<FetchGitMetadataResponse>> {
): AxiosPromise<FetchGitMetadataResponse> {
return Api.get(`${GIT_BASE_URL}/metadata/app/${baseApplicationId}`);
}

View File

@ -1,4 +1,6 @@
export interface FetchGitMetadataResponse {
import type { ApiResponse } from "api/types";
export interface FetchGitMetadataResponseData {
branchName: string;
defaultBranchName: string;
remoteUrl: string;
@ -13,3 +15,6 @@ export interface FetchGitMetadataResponse {
};
isAutoDeploymentEnabled?: boolean;
}
export type FetchGitMetadataResponse =
ApiResponse<FetchGitMetadataResponseData>;

View File

@ -1,4 +1,4 @@
import type { AxiosResponse } from "axios";
import type { AxiosPromise } from "axios";
import type {
FetchMergeStatusRequestParams,
FetchMergeStatusResponse,
@ -9,7 +9,7 @@ import { GIT_BASE_URL } from "./constants";
export default async function fetchMergeStatusRequest(
branchedApplicationId: string,
params: FetchMergeStatusRequestParams,
): Promise<AxiosResponse<FetchMergeStatusResponse>> {
): AxiosPromise<FetchMergeStatusResponse> {
return Api.post(
`${GIT_BASE_URL}/merge/status/app/${branchedApplicationId}`,
params,

View File

@ -1,10 +1,10 @@
import Api from "api/Api";
import { GIT_BASE_URL } from "./constants";
import type { AxiosResponse } from "axios";
import type { FetchProtectedBranches } from "./fetchProtectedBranchesRequest.types";
import type { AxiosPromise } from "axios";
import type { FetchProtectedBranchesResponse } from "./fetchProtectedBranchesRequest.types";
export default async function fetchProtectedBranchesRequest(
baseApplicationId: string,
): Promise<AxiosResponse<FetchProtectedBranches>> {
): AxiosPromise<FetchProtectedBranchesResponse> {
return Api.get(`${GIT_BASE_URL}/branch/app/${baseApplicationId}/protected`);
}

View File

@ -1 +1,6 @@
export type FetchProtectedBranches = string[];
import type { ApiResponse } from "api/types";
export type FetchProtectedBranchesResponseData = string[];
export type FetchProtectedBranchesResponse =
ApiResponse<FetchProtectedBranchesResponseData>;

View File

@ -1,10 +1,10 @@
import type { AxiosResponse } from "axios";
import type { AxiosPromise } from "axios";
import type { FetchSSHKeyResponse } from "./fetchSSHKeyRequest.types";
import Api from "api/Api";
import { APPLICATION_BASE_URL } from "./constants";
export default async function fetchSSHKeyRequest(
baseApplicationId: string,
): Promise<AxiosResponse<FetchSSHKeyResponse>> {
): AxiosPromise<FetchSSHKeyResponse> {
return Api.get(`${APPLICATION_BASE_URL}/ssh-keypair/${baseApplicationId}`);
}

View File

@ -4,11 +4,11 @@ import type {
FetchStatusResponse,
} from "./fetchStatusRequest.types";
import { GIT_BASE_URL } from "./constants";
import type { AxiosResponse } from "axios";
import type { AxiosPromise } from "axios";
export default async function fetchStatusRequest(
branchedApplicationId: string,
params: FetchStatusRequestParams,
): Promise<AxiosResponse<FetchStatusResponse>> {
params: FetchStatusRequestParams = { compareRemote: true },
): AxiosPromise<FetchStatusResponse> {
return Api.get(`${GIT_BASE_URL}/status/app/${branchedApplicationId}`, params);
}

View File

@ -1,7 +1,7 @@
import type { ApiResponse } from "api/types";
export interface FetchStatusRequestParams {
compareRemote: boolean;
compareRemote?: boolean;
}
export interface FetchStatusResponseData {
added: string[];

View File

@ -1,4 +1,4 @@
import type { AxiosResponse } from "axios";
import type { AxiosPromise } from "axios";
import type {
GenerateSSHKeyRequestParams,
GenerateSSHKeyResponse,
@ -9,7 +9,7 @@ import Api from "api/Api";
export default async function generateSSHKeyRequest(
baseApplicationId: string,
params: GenerateSSHKeyRequestParams,
): Promise<AxiosResponse<GenerateSSHKeyResponse>> {
): AxiosPromise<GenerateSSHKeyResponse> {
const url = params.isImporting
? `${GIT_BASE_URL}/import/keys?keyType=${params.keyType}`
: `${APPLICATION_BASE_URL}/ssh-keypair/${baseApplicationId}?keyType=${params.keyType}`;

View File

@ -4,11 +4,11 @@ import type {
ImportGitRequestParams,
ImportGitResponse,
} from "./importGitRequest.types";
import type { AxiosResponse } from "axios";
import type { AxiosPromise } from "axios";
export default async function importGitRequest(
workspaceId: string,
params: ImportGitRequestParams,
): Promise<AxiosResponse<ImportGitResponse>> {
): AxiosPromise<ImportGitResponse> {
return Api.post(`${GIT_BASE_URL}/import/${workspaceId}`, params);
}

View File

@ -1,11 +1,11 @@
import Api from "api/Api";
import type { MergeRequestParams, MergeResponse } from "./mergeRequest.types";
import { GIT_BASE_URL } from "./constants";
import type { AxiosResponse } from "axios";
import type { AxiosPromise } from "axios";
export default async function mergeRequest(
branchedApplicationId: string,
params: MergeRequestParams,
): Promise<AxiosResponse<MergeResponse>> {
): AxiosPromise<MergeResponse> {
return Api.post(`${GIT_BASE_URL}/merge/app/${branchedApplicationId}`, params);
}

View File

@ -1,10 +1,10 @@
import Api from "api/Api";
import { GIT_BASE_URL } from "./constants";
import type { AxiosResponse } from "axios";
import type { AxiosPromise } from "axios";
import type { PullRequestResponse } from "./pullRequest.types";
export default async function pullRequest(
branchedApplicationId: string,
): Promise<AxiosResponse<PullRequestResponse>> {
): AxiosPromise<PullRequestResponse> {
return Api.get(`${GIT_BASE_URL}/pull/app/${branchedApplicationId}`);
}

View File

@ -1,11 +1,11 @@
import Api from "api/Api";
import { GIT_BASE_URL } from "./constants";
import type { AxiosResponse } from "axios";
import type { AxiosPromise } from "axios";
import type { ToggleAutocommitResponse } from "./toggleAutocommitRequest.types";
export default async function toggleAutocommitRequest(
baseApplicationId: string,
): Promise<AxiosResponse<ToggleAutocommitResponse>> {
): AxiosPromise<ToggleAutocommitResponse> {
return Api.patch(
`${GIT_BASE_URL}/auto-commit/toggle/app/${baseApplicationId}`,
);

View File

@ -1,10 +1,10 @@
import Api from "api/Api";
import { GIT_BASE_URL } from "./constants";
import type { AxiosResponse } from "axios";
import type { AxiosPromise } from "axios";
import type { TriggerAutocommitResponse } from "./triggerAutocommitRequest.types";
export default async function triggerAutocommitRequest(
branchedApplicationId: string,
): Promise<AxiosResponse<TriggerAutocommitResponse>> {
): AxiosPromise<TriggerAutocommitResponse> {
return Api.post(`${GIT_BASE_URL}/auto-commit/app/${branchedApplicationId}`);
}

View File

@ -1,7 +1,11 @@
import type { ApiResponse } from "api/types";
import type { AutocommitStatus } from "../constants/enums";
export interface TriggerAutocommitResponse {
export interface TriggerAutocommitResponseData {
autoCommitResponse: AutocommitStatus;
progress: number;
branchName: string;
}
export type TriggerAutocommitResponse =
ApiResponse<TriggerAutocommitResponseData>;

View File

@ -1,4 +1,4 @@
import type { AxiosResponse } from "axios";
import type { AxiosPromise } from "axios";
import type {
UpdateGlobalProfileRequestParams,
UpdateGlobalProfileResponse,
@ -8,6 +8,6 @@ import { GIT_BASE_URL } from "./constants";
export default async function updateGlobalProfileRequest(
params: UpdateGlobalProfileRequestParams,
): Promise<AxiosResponse<UpdateGlobalProfileResponse>> {
): AxiosPromise<UpdateGlobalProfileResponse> {
return Api.post(`${GIT_BASE_URL}/profile/default`, params);
}

View File

@ -4,12 +4,12 @@ import type {
UpdateProtectedBranchesRequestParams,
UpdateProtectedBranchesResponse,
} from "./updateProtectedBranchesRequest.types";
import type { AxiosResponse } from "axios";
import type { AxiosPromise } from "axios";
export default async function updateProtectedBranchesRequest(
baseApplicationId: string,
params: UpdateProtectedBranchesRequestParams,
): Promise<AxiosResponse<UpdateProtectedBranchesResponse>> {
): AxiosPromise<UpdateProtectedBranchesResponse> {
return Api.post(
`${GIT_BASE_URL}/branch/app/${baseApplicationId}/protected`,
params,

View File

@ -0,0 +1,35 @@
import fetchGitMetadataRequest from "git/requests/fetchGitMetadataRequest";
import type { FetchGitMetadataResponse } from "git/requests/fetchGitMetadataRequest.types";
import { gitArtifactActions } from "git/store/gitArtifactSlice";
import type { GitArtifactPayloadAction } from "git/store/types";
import { call, put } from "redux-saga/effects";
import { validateResponse } from "sagas/ErrorSagas";
export default function* fetchGitMetadataSaga(
action: GitArtifactPayloadAction,
) {
const { artifactType, baseArtifactId } = action.payload;
const basePayload = { artifactType, baseArtifactId };
let response: FetchGitMetadataResponse | undefined;
try {
response = yield call(fetchGitMetadataRequest, baseArtifactId);
const isValidResponse: boolean = yield validateResponse(response, false);
if (response && isValidResponse) {
yield put(
gitArtifactActions.fetchGitMetadataSuccess({
...basePayload,
responseData: response.data,
}),
);
}
} catch (error) {
yield put(
gitArtifactActions.fetchGitMetadataError({
...basePayload,
error: error as string,
}),
);
}
}

View File

@ -0,0 +1,36 @@
import fetchProtectedBranchesRequest from "git/requests/fetchProtectedBranchesRequest";
import type { FetchProtectedBranchesResponse } from "git/requests/fetchProtectedBranchesRequest.types";
import { gitArtifactActions } from "git/store/gitArtifactSlice";
import type { GitArtifactPayloadAction } from "git/store/types";
import { call, put } from "redux-saga/effects";
import { validateResponse } from "sagas/ErrorSagas";
export default function* fetchProtectedBranchesSaga(
action: GitArtifactPayloadAction,
) {
const { artifactType, baseArtifactId } = action.payload;
const basePayload = { artifactType, baseArtifactId };
let response: FetchProtectedBranchesResponse | undefined;
try {
response = yield call(fetchProtectedBranchesRequest, baseArtifactId);
const isValidResponse: boolean = yield validateResponse(response);
if (response && isValidResponse) {
yield put(
gitArtifactActions.fetchProtectedBranchesSuccess({
...basePayload,
responseData: response.data,
}),
);
}
} catch (error) {
yield put(
gitArtifactActions.fetchProtectedBranchesError({
...basePayload,
error: error as string,
}),
);
}
}

View File

@ -0,0 +1,43 @@
import fetchStatusRequest from "git/requests/fetchStatusRequest";
import type { FetchStatusResponse } from "git/requests/fetchStatusRequest.types";
import type { FetchStatusInitPayload } from "git/store/actions/fetchStatusActions";
import { gitArtifactActions } from "git/store/gitArtifactSlice";
import type { GitArtifactPayloadAction } from "git/store/types";
import { call, put } from "redux-saga/effects";
import { validateResponse } from "sagas/ErrorSagas";
export default function* fetchStatusSaga(
action: GitArtifactPayloadAction<FetchStatusInitPayload>,
) {
const { artifactType, baseArtifactId } = action.payload;
const basePayload = { artifactType, baseArtifactId };
let response: FetchStatusResponse | undefined;
try {
response = yield call(fetchStatusRequest, baseArtifactId);
const isValidResponse: boolean = yield validateResponse(response);
if (response && isValidResponse) {
yield put(
gitArtifactActions.fetchStatusSuccess({
...basePayload,
responseData: response.data,
}),
);
}
} catch (error) {
yield put(
gitArtifactActions.fetchStatusError({
...basePayload,
error: error as string,
}),
);
// ! case: BETTER ERROR HANDLING
// if ((error as Error)?.message?.includes("Auth fail")) {
// payload.error = new Error(createMessage(ERROR_GIT_AUTH_FAIL));
// } else if ((error as Error)?.message?.includes("Invalid remote: origin")) {
// payload.error = new Error(createMessage(ERROR_GIT_INVALID_REMOTE));
// }
}
}

View File

@ -1,37 +1,99 @@
import { gitArtifactActions } from "git/store/gitArtifactSlice";
import { all, takeLatest } from "redux-saga/effects";
import {
actionChannel,
call,
fork,
take,
takeLatest,
} from "redux-saga/effects";
import type { TakeableChannel } from "redux-saga";
import type { PayloadAction } from "@reduxjs/toolkit";
import { objectKeys } from "@appsmith/utils";
import { gitConfigActions } from "../store/gitConfigSlice";
import { gitArtifactActions } from "../store/gitArtifactSlice";
import connectSaga from "./connectSaga";
import commitSaga from "./commitSaga";
import { gitConfigActions } from "git/store/gitConfigSlice";
import fetchGlobalProfileSaga from "./fetchGlobalProfileSaga";
import fetchBranchesSaga from "./fetchBranchesSaga";
import fetchLocalProfileSaga from "./fetchLocalProfileSaga";
import updateLocalProfileSaga from "./updateLocalProfileSaga";
import updateGlobalProfileSaga from "./updateGlobalProfileSaga";
import initGitForEditorSaga from "./initGitSaga";
import fetchGitMetadataSaga from "./fetchGitMetadataSaga";
import triggerAutocommitSaga from "./triggerAutocommitSaga";
import fetchStatusSaga from "./fetchStatusSaga";
import fetchProtectedBranchesSaga from "./fetchProtectedBranchesSaga";
export function* gitSagas() {
yield all([
takeLatest(gitArtifactActions.connectInit.type, connectSaga),
const gitRequestBlockingActions: Record<
string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(action: PayloadAction<any>) => Generator<any>
> = {
// init
[gitArtifactActions.fetchGitMetadataInit.type]: fetchGitMetadataSaga,
// branches
takeLatest(gitArtifactActions.fetchBranchesInit.type, fetchBranchesSaga),
// connect
[gitArtifactActions.connectInit.type]: connectSaga,
takeLatest(gitArtifactActions.commitInit.type, commitSaga),
takeLatest(
gitArtifactActions.fetchLocalProfileInit.type,
fetchLocalProfileSaga,
),
takeLatest(
gitArtifactActions.updateLocalProfileInit.type,
updateLocalProfileSaga,
),
takeLatest(
gitConfigActions.fetchGlobalProfileInit.type,
fetchGlobalProfileSaga,
),
takeLatest(
gitConfigActions.updateGlobalProfileInit.type,
updateGlobalProfileSaga,
),
]);
// ops
[gitArtifactActions.commitInit.type]: commitSaga,
[gitArtifactActions.fetchStatusInit.type]: fetchStatusSaga,
// branches
[gitArtifactActions.fetchBranchesInit.type]: fetchBranchesSaga,
// settings
[gitArtifactActions.fetchLocalProfileInit.type]: fetchLocalProfileSaga,
[gitArtifactActions.updateLocalProfileInit.type]: updateLocalProfileSaga,
[gitConfigActions.fetchGlobalProfileInit.type]: fetchGlobalProfileSaga,
[gitConfigActions.updateGlobalProfileInit.type]: updateGlobalProfileSaga,
// autocommit
[gitArtifactActions.triggerAutocommitInit.type]: triggerAutocommitSaga,
};
const gitRequestNonBlockingActions: Record<
string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(action: PayloadAction<any>) => Generator<any>
> = {
// init
[gitArtifactActions.initGitForEditor.type]: initGitForEditorSaga,
// settings
[gitArtifactActions.fetchProtectedBranchesInit.type]:
fetchProtectedBranchesSaga,
};
/**
* All git actions on the server are behind a lock,
* that means that only one action can be performed at once.
*
* To follow the same principle, we will queue all actions from the client
* as well and only perform one action at a time.
*
* This will ensure that client is not running parallel requests to the server for git
* */
function* watchGitBlockingRequests() {
const gitActionChannel: TakeableChannel<unknown> = yield actionChannel(
objectKeys(gitRequestBlockingActions),
);
while (true) {
const action: PayloadAction<unknown> = yield take(gitActionChannel);
yield call(gitRequestBlockingActions[action.type], action);
}
}
function* watchGitNonBlockingRequests() {
const keys = objectKeys(gitRequestNonBlockingActions);
for (const actionType of keys) {
yield takeLatest(actionType, gitRequestNonBlockingActions[actionType]);
}
}
export default function* gitSagas() {
yield fork(watchGitNonBlockingRequests);
yield fork(watchGitBlockingRequests);
}

View File

@ -0,0 +1,30 @@
import { GitArtifactType } from "git/constants/enums";
import type { InitGitForEditorPayload } from "git/store/actions/initGitActions";
import { gitArtifactActions } from "git/store/gitArtifactSlice";
import type { GitArtifactPayloadAction } from "git/store/types";
import { put, take } from "redux-saga/effects";
export default function* initGitForEditorSaga(
action: GitArtifactPayloadAction<InitGitForEditorPayload>,
) {
const { artifact, artifactType, baseArtifactId } = action.payload;
const basePayload = { artifactType, baseArtifactId };
yield put(gitArtifactActions.mount(basePayload));
if (artifactType === GitArtifactType.Application) {
if (!!artifact.gitApplicationMetadata) {
yield put(gitArtifactActions.fetchGitMetadataInit(basePayload));
yield take(gitArtifactActions.fetchGitMetadataSuccess.type);
yield put(
gitArtifactActions.triggerAutocommitInit({
...basePayload,
artifactId: artifact.id,
}),
);
yield put(gitArtifactActions.fetchBranchesInit(basePayload));
yield put(gitArtifactActions.fetchProtectedBranchesInit(basePayload));
yield put(gitArtifactActions.fetchStatusInit(basePayload));
}
}
}

View File

@ -0,0 +1,131 @@
import { triggerAutocommitSuccessAction } from "actions/gitSyncActions";
import { AutocommitStatus, type GitArtifactType } from "git/constants/enums";
import fetchAutocommitProgressRequest from "git/requests/fetchAutocommitProgressRequest";
import type {
FetchAutocommitProgressResponse,
FetchAutocommitProgressResponseData,
} from "git/requests/fetchAutocommitProgressRequest.types";
import triggerAutocommitRequest from "git/requests/triggerAutocommitRequest";
import type {
TriggerAutocommitResponse,
TriggerAutocommitResponseData,
} from "git/requests/triggerAutocommitRequest.types";
import type { TriggerAutocommitInitPayload } from "git/store/actions/triggerAutocommitActions";
import { gitArtifactActions } from "git/store/gitArtifactSlice";
import { selectAutocommitEnabled } from "git/store/selectors/gitSingleArtifactSelectors";
import type { GitArtifactPayloadAction } from "git/store/types";
import {
call,
cancel,
delay,
fork,
put,
select,
take,
} from "redux-saga/effects";
import type { Task } from "redux-saga";
import { validateResponse } from "sagas/ErrorSagas";
const AUTOCOMMIT_POLL_DELAY = 1000;
const AUTOCOMMIT_WHITELISTED_STATES = [
AutocommitStatus.PUBLISHED,
AutocommitStatus.IN_PROGRESS,
AutocommitStatus.LOCKED,
];
interface PollAutocommitProgressParams {
artifactType: keyof typeof GitArtifactType;
baseArtifactId: string;
artifactId: string;
}
function isAutocommitHappening(
responseData:
| TriggerAutocommitResponseData
| FetchAutocommitProgressResponseData
| undefined,
): boolean {
return (
!!responseData &&
AUTOCOMMIT_WHITELISTED_STATES.includes(responseData.autoCommitResponse)
);
}
function* pollAutocommitProgressSaga(params: PollAutocommitProgressParams) {
const { artifactId, artifactType, baseArtifactId } = params;
const basePayload = { artifactType, baseArtifactId };
let triggerResponse: TriggerAutocommitResponse | undefined;
try {
triggerResponse = yield call(triggerAutocommitRequest, artifactId);
const isValidResponse: boolean = yield validateResponse(triggerResponse);
if (triggerResponse && isValidResponse) {
yield put(gitArtifactActions.triggerAutocommitSuccess(basePayload));
}
} catch (error) {
yield put(
gitArtifactActions.triggerAutocommitError({
...basePayload,
error: error as string,
}),
);
}
try {
if (isAutocommitHappening(triggerResponse?.data)) {
yield put(gitArtifactActions.pollAutocommitProgressStart(basePayload));
while (true) {
yield put(gitArtifactActions.fetchAutocommitProgressInit(basePayload));
const progressResponse: FetchAutocommitProgressResponse = yield call(
fetchAutocommitProgressRequest,
baseArtifactId,
);
const isValidResponse: boolean =
yield validateResponse(progressResponse);
if (isValidResponse && !isAutocommitHappening(progressResponse?.data)) {
yield put(gitArtifactActions.pollAutocommitProgressStop(basePayload));
}
if (!isValidResponse) {
yield put(gitArtifactActions.pollAutocommitProgressStop(basePayload));
}
yield delay(AUTOCOMMIT_POLL_DELAY);
}
} else {
yield put(gitArtifactActions.pollAutocommitProgressStop(basePayload));
}
} catch (error) {
yield put(gitArtifactActions.pollAutocommitProgressStop(basePayload));
yield put(
gitArtifactActions.fetchAutocommitProgressError({
...basePayload,
error: error as string,
}),
);
}
}
export default function* triggerAutocommitSaga(
action: GitArtifactPayloadAction<TriggerAutocommitInitPayload>,
) {
const { artifactId, artifactType, baseArtifactId } = action.payload;
const basePayload = { artifactType, baseArtifactId };
const isAutocommitEnabled: boolean = yield select(
selectAutocommitEnabled,
basePayload,
);
if (isAutocommitEnabled) {
const params = { artifactType, baseArtifactId, artifactId };
const pollTask: Task = yield fork(pollAutocommitProgressSaga, params);
yield take(gitArtifactActions.pollAutocommitProgressStop.type);
yield cancel(pollTask);
} else {
yield put(triggerAutocommitSuccessAction());
}
}

View File

@ -1,8 +1,4 @@
import type {
GitArtifactPayloadAction,
GitArtifactErrorPayloadAction,
GitAutocommitProgress,
} from "../types";
import type { GitAsyncErrorPayload } from "../types";
import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction";
export const fetchAutocommitProgressInitAction = createSingleArtifactAction(
@ -15,27 +11,19 @@ export const fetchAutocommitProgressInitAction = createSingleArtifactAction(
);
export const fetchAutocommitProgressSuccessAction = createSingleArtifactAction(
(
state,
action: GitArtifactPayloadAction<{
autocommitProgress: GitAutocommitProgress;
}>,
) => {
(state) => {
state.apiResponses.autocommitProgress.loading = false;
state.apiResponses.autocommitProgress.value =
action.payload.autocommitProgress;
return state;
},
);
export const fetchAutocommitProgressErrorAction = createSingleArtifactAction(
(state, action: GitArtifactErrorPayloadAction) => {
export const fetchAutocommitProgressErrorAction =
createSingleArtifactAction<GitAsyncErrorPayload>((state, action) => {
const { error } = action.payload;
state.apiResponses.autocommitProgress.loading = false;
state.apiResponses.autocommitProgress.error = error;
return state;
},
);
});

View File

@ -0,0 +1,31 @@
import type { GitAsyncErrorPayload, GitAsyncSuccessPayload } from "../types";
import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction";
import type { FetchGitMetadataResponseData } from "git/requests/fetchGitMetadataRequest.types";
export const fetchGitMetadataInitAction = createSingleArtifactAction(
(state) => {
state.apiResponses.metadata.loading = true;
state.apiResponses.metadata.error = null;
return state;
},
);
export const fetchGitMetadataSuccessAction = createSingleArtifactAction<
GitAsyncSuccessPayload<FetchGitMetadataResponseData>
>((state, action) => {
state.apiResponses.metadata.loading = false;
state.apiResponses.metadata.value = action.payload.responseData;
return state;
});
export const fetchGitMetadataErrorAction =
createSingleArtifactAction<GitAsyncErrorPayload>((state, action) => {
const { error } = action.payload;
state.apiResponses.metadata.loading = false;
state.apiResponses.metadata.error = error;
return state;
});

View File

@ -1,33 +0,0 @@
import type {
GitArtifactPayloadAction,
GitArtifactErrorPayloadAction,
GitMetadata,
} from "../types";
import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction";
export const fetchMetadataInitAction = createSingleArtifactAction((state) => {
state.apiResponses.metadata.loading = true;
state.apiResponses.metadata.error = null;
return state;
});
export const fetchMetadataSuccessAction = createSingleArtifactAction(
(state, action: GitArtifactPayloadAction<{ metadata: GitMetadata }>) => {
state.apiResponses.metadata.loading = false;
state.apiResponses.metadata.value = action.payload.metadata;
return state;
},
);
export const fetchMetadataErrorAction = createSingleArtifactAction(
(state, action: GitArtifactErrorPayloadAction) => {
const { error } = action.payload;
state.apiResponses.metadata.loading = false;
state.apiResponses.metadata.error = error;
return state;
},
);

View File

@ -1,9 +1,6 @@
import type {
GitArtifactPayloadAction,
GitArtifactErrorPayloadAction,
GitProtectedBranches,
} from "../types";
import type { GitAsyncSuccessPayload, GitAsyncErrorPayload } from "../types";
import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction";
import type { FetchProtectedBranchesResponseData } from "git/requests/fetchProtectedBranchesRequest.types";
export const fetchProtectedBranchesInitAction = createSingleArtifactAction(
(state) => {
@ -14,28 +11,21 @@ export const fetchProtectedBranchesInitAction = createSingleArtifactAction(
},
);
export const fetchProtectedBranchesSuccessAction = createSingleArtifactAction(
(
state,
action: GitArtifactPayloadAction<{
protectedBranches: GitProtectedBranches;
}>,
) => {
state.apiResponses.protectedBranches.loading = false;
state.apiResponses.protectedBranches.value =
action.payload.protectedBranches;
export const fetchProtectedBranchesSuccessAction = createSingleArtifactAction<
GitAsyncSuccessPayload<FetchProtectedBranchesResponseData>
>((state, action) => {
state.apiResponses.protectedBranches.loading = false;
state.apiResponses.protectedBranches.value = action.payload.responseData;
return state;
},
);
return state;
});
export const fetchProtectedBranchesErrorAction = createSingleArtifactAction(
(state, action: GitArtifactErrorPayloadAction) => {
export const fetchProtectedBranchesErrorAction =
createSingleArtifactAction<GitAsyncErrorPayload>((state, action) => {
const { error } = action.payload;
state.apiResponses.protectedBranches.loading = false;
state.apiResponses.protectedBranches.error = error;
return state;
},
);
});

View File

@ -0,0 +1,15 @@
import type { FetchGitMetadataResponseData } from "git/requests/fetchGitMetadataRequest.types";
import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction";
export interface InitGitForEditorPayload {
artifact: {
id: string;
baseId: string;
gitApplicationMetadata?: Partial<FetchGitMetadataResponseData>;
};
}
export const initGitForEditorAction =
createSingleArtifactAction<InitGitForEditorPayload>((state) => {
return state;
});

View File

@ -1,14 +1,17 @@
import { createSingleArtifactAction } from "../helpers/createSingleArtifactAction";
import type { GitArtifactErrorPayloadAction } from "../types";
import type { GitAsyncErrorPayload } from "../types";
export const triggerAutocommitInitAction = createSingleArtifactAction(
(state) => {
export interface TriggerAutocommitInitPayload {
artifactId: string;
}
export const triggerAutocommitInitAction =
createSingleArtifactAction<TriggerAutocommitInitPayload>((state) => {
state.apiResponses.triggerAutocommit.loading = true;
state.apiResponses.triggerAutocommit.error = null;
return state;
},
);
});
export const triggerAutocommitSuccessAction = createSingleArtifactAction(
(state) => {
@ -18,13 +21,28 @@ export const triggerAutocommitSuccessAction = createSingleArtifactAction(
},
);
export const triggerAutocommitErrorAction = createSingleArtifactAction(
(state, action: GitArtifactErrorPayloadAction) => {
export const triggerAutocommitErrorAction =
createSingleArtifactAction<GitAsyncErrorPayload>((state, action) => {
const { error } = action.payload;
state.apiResponses.triggerAutocommit.loading = false;
state.apiResponses.triggerAutocommit.error = error;
return state;
});
export const pollAutocommitProgressStartAction = createSingleArtifactAction(
(state) => {
state.ui.autocommitPolling = true;
return state;
},
);
export const pollAutocommitProgressStopAction = createSingleArtifactAction(
(state) => {
state.ui.autocommitPolling = false;
return state;
},
);

View File

@ -8,10 +8,10 @@ import {
connectSuccessAction,
} from "./actions/connectActions";
import {
fetchMetadataErrorAction,
fetchMetadataInitAction,
fetchMetadataSuccessAction,
} from "./actions/fetchMetadataActions";
fetchGitMetadataErrorAction,
fetchGitMetadataInitAction,
fetchGitMetadataSuccessAction,
} from "./actions/fetchGitMetadataActions";
import {
fetchBranchesErrorAction,
fetchBranchesInitAction,
@ -79,6 +79,34 @@ import {
mergeInitAction,
mergeSuccessAction,
} from "./actions/mergeActions";
import {
pollAutocommitProgressStopAction,
pollAutocommitProgressStartAction,
triggerAutocommitErrorAction,
triggerAutocommitInitAction,
triggerAutocommitSuccessAction,
} from "./actions/triggerAutocommitActions";
import {
toggleAutocommitErrorAction,
toggleAutocommitInitAction,
toggleAutocommitSuccessAction,
} from "./actions/toggleAutocommitActions";
import {
fetchProtectedBranchesErrorAction,
fetchProtectedBranchesInitAction,
fetchProtectedBranchesSuccessAction,
} from "./actions/fetchProtectedBranchesActions";
import {
updateProtectedBranchesErrorAction,
updateProtectedBranchesInitAction,
updateProtectedBranchesSuccessAction,
} from "./actions/updateProtectedBranchesActions";
import { initGitForEditorAction } from "./actions/initGitActions";
import {
fetchAutocommitProgressErrorAction,
fetchAutocommitProgressInitAction,
fetchAutocommitProgressSuccessAction,
} from "./actions/fetchAutocommitProgressActions";
const initialState: GitArtifactReduxState = {};
@ -87,8 +115,13 @@ export const gitArtifactSlice = createSlice({
reducerPath: "git.artifact",
initialState,
reducers: {
// init
initGitForEditor: initGitForEditorAction,
mount: mountAction,
unmount: unmountAction,
fetchGitMetadataInit: fetchGitMetadataInitAction,
fetchGitMetadataSuccess: fetchGitMetadataSuccessAction,
fetchGitMetadataError: fetchGitMetadataErrorAction,
// connect
connectInit: connectInitAction,
@ -135,17 +168,31 @@ export const gitArtifactSlice = createSlice({
// settings
toggleGitSettingsModal: toggleGitSettingsModalAction,
// metadata
fetchMetadataInit: fetchMetadataInitAction,
fetchMetadataSuccess: fetchMetadataSuccessAction,
fetchMetadataError: fetchMetadataErrorAction,
fetchLocalProfileInit: fetchLocalProfileInitAction,
fetchLocalProfileSuccess: fetchLocalProfileSuccessAction,
fetchLocalProfileError: fetchLocalProfileErrorAction,
updateLocalProfileInit: updateLocalProfileInitAction,
updateLocalProfileSuccess: updateLocalProfileSuccessAction,
updateLocalProfileError: updateLocalProfileErrorAction,
fetchProtectedBranchesInit: fetchProtectedBranchesInitAction,
fetchProtectedBranchesSuccess: fetchProtectedBranchesSuccessAction,
fetchProtectedBranchesError: fetchProtectedBranchesErrorAction,
updateProtectedBranchesInit: updateProtectedBranchesInitAction,
updateProtectedBranchesSuccess: updateProtectedBranchesSuccessAction,
updateProtectedBranchesError: updateProtectedBranchesErrorAction,
// autocommit
toggleAutocommitInit: toggleAutocommitInitAction,
toggleAutocommitSuccess: toggleAutocommitSuccessAction,
toggleAutocommitError: toggleAutocommitErrorAction,
triggerAutocommitInit: triggerAutocommitInitAction,
triggerAutocommitSuccess: triggerAutocommitSuccessAction,
triggerAutocommitError: triggerAutocommitErrorAction,
fetchAutocommitProgressInit: fetchAutocommitProgressInitAction,
fetchAutocommitProgressSuccess: fetchAutocommitProgressSuccessAction,
fetchAutocommitProgressError: fetchAutocommitProgressErrorAction,
pollAutocommitProgressStart: pollAutocommitProgressStartAction,
pollAutocommitProgressStop: pollAutocommitProgressStopAction,
},
});

View File

@ -33,6 +33,8 @@ const gitSingleArtifactInitialUIState: GitSingleArtifactUIReduxState = {
repoLimitErrorModal: {
open: false,
},
autocommitModalOpen: false,
autocommitPolling: false,
};
const gitSingleArtifactInitialAPIResponses: GitSingleArtifactAPIResponsesReduxState =
@ -112,7 +114,6 @@ const gitSingleArtifactInitialAPIResponses: GitSingleArtifactAPIResponsesReduxSt
error: null,
},
autocommitProgress: {
value: null,
loading: false,
error: null,
},

View File

@ -15,6 +15,17 @@ export const selectSingleArtifact = (
];
};
// metadata
export const selectGitMetadata = (
state: GitRootState,
artifactDef: GitArtifactDef,
) => selectSingleArtifact(state, artifactDef)?.apiResponses.metadata;
export const selectGitConnected = (
state: GitRootState,
artifactDef: GitArtifactDef,
) => !!selectGitMetadata(state, artifactDef).value;
// git ops
export const selectCommit = (
state: GitRootState,
@ -43,6 +54,16 @@ export const selectPull = (state: GitRootState, artifactDef: GitArtifactDef) =>
selectSingleArtifact(state, artifactDef)?.apiResponses?.pull;
// git branches
export const selectCurrentBranch = (
state: GitRootState,
artifactDef: GitArtifactDef,
) => {
const gitMetadataState = selectGitMetadata(state, artifactDef).value;
return gitMetadataState?.branchName;
};
export const selectBranches = (
state: GitRootState,
artifactDef: GitArtifactDef,
@ -62,3 +83,34 @@ export const selectCheckoutBranch = (
state: GitRootState,
artifactDef: GitArtifactDef,
) => selectSingleArtifact(state, artifactDef)?.apiResponses.checkoutBranch;
// autocommit
export const selectAutocommitEnabled = (
state: GitRootState,
artifactDef: GitArtifactDef,
) => {
const gitMetadata = selectGitMetadata(state, artifactDef).value;
return gitMetadata?.autoCommitConfig?.enabled;
};
export const selectAutocommitPolling = (
state: GitRootState,
artifactDef: GitArtifactDef,
) => selectSingleArtifact(state, artifactDef)?.ui.autocommitPolling;
// protected branches
export const selectProtectedBranches = (
state: GitRootState,
artifactDef: GitArtifactDef,
) => selectSingleArtifact(state, artifactDef)?.apiResponses.protectedBranches;
export const selectProtectedMode = (
state: GitRootState,
artifactDef: GitArtifactDef,
) => {
const currentBranch = selectCurrentBranch(state, artifactDef);
const protectedBranches = selectProtectedBranches(state, artifactDef).value;
return protectedBranches?.includes(currentBranch ?? "");
};

View File

@ -11,13 +11,8 @@ import type { FetchBranchesResponseData } from "../requests/fetchBranchesRequest
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<string, unknown>;
export type GitProtectedBranches = Record<string, unknown>;
export type GitAutocommitProgress = Record<string, unknown>;
import type { FetchGitMetadataResponseData } from "git/requests/fetchGitMetadataRequest.types";
import type { FetchProtectedBranchesResponseData } from "git/requests/fetchProtectedBranchesRequest.types";
export type GitSSHKey = Record<string, unknown>;
@ -32,7 +27,7 @@ interface AsyncStateWithoutValue {
error: string | null;
}
export interface GitSingleArtifactAPIResponsesReduxState {
metadata: AsyncState<GitMetadata>;
metadata: AsyncState<FetchGitMetadataResponseData>;
connect: AsyncStateWithoutValue;
status: AsyncState<FetchStatusResponseData>;
commit: AsyncStateWithoutValue;
@ -47,9 +42,9 @@ export interface GitSingleArtifactAPIResponsesReduxState {
localProfile: AsyncState<FetchLocalProfileResponseData>;
updateLocalProfile: AsyncStateWithoutValue;
disconnect: AsyncStateWithoutValue;
protectedBranches: AsyncState<GitProtectedBranches>;
protectedBranches: AsyncState<FetchProtectedBranchesResponseData>;
updateProtectedBranches: AsyncStateWithoutValue;
autocommitProgress: AsyncState<GitAutocommitProgress>;
autocommitProgress: AsyncStateWithoutValue;
toggleAutocommit: AsyncStateWithoutValue;
triggerAutocommit: AsyncStateWithoutValue;
sshKey: AsyncState<GitSSHKey>;
@ -79,6 +74,8 @@ export interface GitSingleArtifactUIReduxState {
repoLimitErrorModal: {
open: boolean;
};
autocommitPolling: boolean;
autocommitModalOpen: boolean;
}
export interface GitSingleArtifactReduxState {
ui: GitSingleArtifactUIReduxState;