diff --git a/app/client/src/ce/pages/Applications/index.tsx b/app/client/src/ce/pages/Applications/index.tsx
index 85b1174f9e56..742ee2703a69 100644
--- a/app/client/src/ce/pages/Applications/index.tsx
+++ b/app/client/src/ce/pages/Applications/index.tsx
@@ -135,6 +135,7 @@ import { useGitModEnabled } from "pages/Editor/gitSync/hooks/modHooks";
import {
GitRepoLimitErrorModal as NewGitRepoLimitErrorModal,
GitImportModal as NewGitImportModal,
+ GitImportOverrideModal,
} from "git";
import OldRepoLimitExceededErrorModal from "pages/Editor/gitSync/RepoLimitExceededErrorModal";
@@ -145,6 +146,7 @@ function GitModals() {
<>
+
>
) : (
<>
diff --git a/app/client/src/git/ce/constants/messages.tsx b/app/client/src/git/ce/constants/messages.tsx
index 200e109bb360..24d183fb7581 100644
--- a/app/client/src/git/ce/constants/messages.tsx
+++ b/app/client/src/git/ce/constants/messages.tsx
@@ -4,6 +4,14 @@ export const IMPORT_GIT = {
WAIT_TEXT: "Please wait while we import via Git..",
};
+export const IMPORT_OVERRIDE_MODAL = {
+ TITLE: "Override existing {{artifactType}}?",
+ DESCRIPTION:
+ "{{newArtifactName}} already exists in this workspace as {{oldArtifactName}}. Do you want to override it with the imported {{artifactType}}?",
+ CANCEL_BTN: "Cancel",
+ OVERRIDE_BTN: "Override",
+};
+
export const CONNECT_GIT = {
MODAL_TITLE: "Configure Git",
CHOOSE_PROVIDER_CTA: "Configure Git",
diff --git a/app/client/src/git/components/ImportModal/index.tsx b/app/client/src/git/components/ImportModal/index.tsx
index 2dd82b71bbad..31932d55d925 100644
--- a/app/client/src/git/components/ImportModal/index.tsx
+++ b/app/client/src/git/components/ImportModal/index.tsx
@@ -11,6 +11,7 @@ function ImportModal() {
gitImportError,
isGitImportLoading,
isImportModalOpen,
+ resetGitImport,
toggleImportModal,
} = useImport();
const {
@@ -24,11 +25,16 @@ function ImportModal() {
const onSubmit = useCallback(
(params: GitImportRequestParams) => {
- gitImport(params);
+ gitImport({ ...params, override: false });
},
[gitImport],
);
+ const resetConnectState = useCallback(() => {
+ resetGlobalSSHKey();
+ resetGitImport();
+ }, [resetGitImport, resetGlobalSSHKey]);
+
return (
diff --git a/app/client/src/git/components/ImportOverrideModal/ImportOverrideModalView.tsx b/app/client/src/git/components/ImportOverrideModal/ImportOverrideModalView.tsx
new file mode 100644
index 000000000000..ae5c56014ad6
--- /dev/null
+++ b/app/client/src/git/components/ImportOverrideModal/ImportOverrideModalView.tsx
@@ -0,0 +1,86 @@
+import {
+ Button,
+ Modal,
+ ModalBody,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ Text,
+} from "@appsmith/ads";
+import { IMPORT_OVERRIDE_MODAL } from "git/ee/constants/messages";
+import useMessage from "git/hooks/useMessage";
+import noop from "lodash/noop";
+import React, { useCallback } from "react";
+import styled from "styled-components";
+
+const StyledModalContent = styled(ModalContent)`
+ width: 640px;
+`;
+
+interface ImportOverrideModalViewProps {
+ artifactType?: string;
+ isImportLoading: boolean;
+ isOpen: boolean;
+ newArtifactName: string | null;
+ oldArtifactName: string | null;
+ onImport: () => void;
+ onOpenChange: (open: boolean) => void;
+}
+
+function ImportOverrideModalView({
+ artifactType = "artifact",
+ isImportLoading = false,
+ isOpen = false,
+ newArtifactName = null,
+ oldArtifactName = null,
+ onImport = noop,
+ onOpenChange = noop,
+}: ImportOverrideModalViewProps) {
+ const modalTitle = useMessage(IMPORT_OVERRIDE_MODAL.TITLE, { artifactType });
+ const modalDescription = useMessage(IMPORT_OVERRIDE_MODAL.DESCRIPTION, {
+ newArtifactName: newArtifactName ?? "",
+ oldArtifactName: oldArtifactName ?? "",
+ artifactType,
+ });
+ const ctaBtnText = useMessage(IMPORT_OVERRIDE_MODAL.OVERRIDE_BTN, {
+ artifactType,
+ });
+
+ const handleCancel = useCallback(() => {
+ onOpenChange(false);
+ }, [onOpenChange]);
+
+ const handleImport = useCallback(() => {
+ if (!isImportLoading) {
+ onImport();
+ }
+ }, [isImportLoading, onImport]);
+
+ return (
+
+
+ {modalTitle}
+
+
+ {modalDescription}
+
+
+
+
+
+
+
+
+ );
+}
+
+export default ImportOverrideModalView;
diff --git a/app/client/src/git/components/ImportOverrideModal/index.tsx b/app/client/src/git/components/ImportOverrideModal/index.tsx
new file mode 100644
index 000000000000..cee5161dbf35
--- /dev/null
+++ b/app/client/src/git/components/ImportOverrideModal/index.tsx
@@ -0,0 +1,46 @@
+import React, { useCallback } from "react";
+import ImportOverrideModalView from "./ImportOverrideModalView";
+import useImport from "git/hooks/useImport";
+
+function ImportOverrideModal() {
+ const {
+ gitImport,
+ importOverrideDetails,
+ isGitImportLoading,
+ isImportOverrideModalOpen,
+ resetGitImport,
+ resetImportOverrideDetails,
+ } = useImport();
+
+ const handleOpenChange = useCallback(
+ (open: boolean) => {
+ if (!open && !isGitImportLoading) {
+ resetImportOverrideDetails();
+ resetGitImport();
+ }
+ },
+ [isGitImportLoading, resetGitImport, resetImportOverrideDetails],
+ );
+
+ const handleImport = useCallback(() => {
+ if (importOverrideDetails) {
+ const params = { ...importOverrideDetails.params, override: true };
+
+ gitImport(params);
+ }
+ }, [gitImport, importOverrideDetails]);
+
+ return (
+
+ );
+}
+
+export default ImportOverrideModal;
diff --git a/app/client/src/git/constants/enums.ts b/app/client/src/git/constants/enums.ts
index b979b66645ed..1b2fe9084e88 100644
--- a/app/client/src/git/constants/enums.ts
+++ b/app/client/src/git/constants/enums.ts
@@ -37,4 +37,5 @@ export enum GitErrorCodes {
REPO_NOT_EMPTY = "AE-GIT-4033",
REPO_LIMIT_REACHED = "AE-GIT-4043",
PUSH_FAILED_REMOTE_COUNTERPART_IS_AHEAD = "AE-GIT-4048",
+ DUPLICATE_ARTIFACT_OVERRIDE = "AE-GIT-5004",
}
diff --git a/app/client/src/git/hooks/useImport.ts b/app/client/src/git/hooks/useImport.ts
index 62069b888d68..50d4beb26c34 100644
--- a/app/client/src/git/hooks/useImport.ts
+++ b/app/client/src/git/hooks/useImport.ts
@@ -3,9 +3,12 @@ import { useDispatch, useSelector } from "react-redux";
import {
selectGitImportState,
selectImportModalOpen,
+ selectImportOverrideDetails,
+ selectImportOverrideModalOpen,
} from "git/store/selectors/gitGlobalSelectors";
import { gitGlobalActions } from "git/store/gitGlobalSlice";
import type { GitImportRequestParams } from "git/requests/gitImportRequest.types";
+import type { SetImportOverrideDetailsPayload } from "git/store/actions/uiActions";
export default function useImport() {
const dispatch = useDispatch();
@@ -19,6 +22,10 @@ export default function useImport() {
[dispatch],
);
+ const resetGitImport = useCallback(() => {
+ dispatch(gitGlobalActions.resetGitImport());
+ }, [dispatch]);
+
const isImportModalOpen = useSelector(selectImportModalOpen);
const toggleImportModal = useCallback(
@@ -28,11 +35,31 @@ export default function useImport() {
[dispatch],
);
+ const isImportOverrideModalOpen = useSelector(selectImportOverrideModalOpen);
+
+ const importOverrideDetails = useSelector(selectImportOverrideDetails);
+
+ const setImportOverrideDetails = useCallback(
+ (details: SetImportOverrideDetailsPayload) => {
+ dispatch(gitGlobalActions.setImportOverrideDetails(details));
+ },
+ [dispatch],
+ );
+
+ const resetImportOverrideDetails = useCallback(() => {
+ dispatch(gitGlobalActions.resetImportOverrideDetails());
+ }, [dispatch]);
+
return {
isGitImportLoading: gitImportState?.loading ?? false,
gitImportError: gitImportState?.error ?? null,
gitImport,
+ resetGitImport,
isImportModalOpen: isImportModalOpen ?? false,
toggleImportModal,
+ isImportOverrideModalOpen: isImportOverrideModalOpen ?? false,
+ importOverrideDetails,
+ setImportOverrideDetails,
+ resetImportOverrideDetails,
};
}
diff --git a/app/client/src/git/hooks/useMessage.ts b/app/client/src/git/hooks/useMessage.ts
new file mode 100644
index 000000000000..73845168aff6
--- /dev/null
+++ b/app/client/src/git/hooks/useMessage.ts
@@ -0,0 +1,12 @@
+import { useMemo } from "react";
+
+export default function useMessage(msg: string, args: Record) {
+ return useMemo(() => {
+ const msgWithArgs = msg.replace(/\{\{([^}]+)\}\}/g, (match, p1) => {
+ // p1 is the key from {{key}} in the message
+ return args[p1] || match;
+ });
+
+ return msgWithArgs;
+ }, [msg, args]);
+}
diff --git a/app/client/src/git/index.ts b/app/client/src/git/index.ts
index c210c1a07596..8a4ef343cb5b 100644
--- a/app/client/src/git/index.ts
+++ b/app/client/src/git/index.ts
@@ -6,6 +6,7 @@ export { useHotKeys } from "./components/HotKeys";
export { default as GitContextProvider } from "./components/GitContextProvider";
export { default as GitModals } from "./ee/components/GitModals";
export { default as GitImportModal } from "./components/ImportModal";
+export { default as GitImportOverrideModal } from "./components/ImportOverrideModal";
export { default as GitRepoLimitErrorModal } from "./components/RepoLimitErrorModal";
export { default as GitQuickActions } from "./components/QuickActions";
export { default as GitProtectedBranchCallout } from "./components/ProtectedBranchCallout";
diff --git a/app/client/src/git/requests/gitImportRequest.ts b/app/client/src/git/requests/gitImportRequest.ts
index 3ee6dcdfcba7..ec6dd3ab3a12 100644
--- a/app/client/src/git/requests/gitImportRequest.ts
+++ b/app/client/src/git/requests/gitImportRequest.ts
@@ -17,7 +17,10 @@ async function gitImportRequestNew(
workspaceId: string,
params: GitImportRequestParams,
): AxiosPromise {
- return Api.post(`${GIT_BASE_URL}/artifacts/import`, params, { workspaceId });
+ const { override = false, ...restParams } = params;
+ const body = { override, ...restParams };
+
+ return Api.post(`${GIT_BASE_URL}/artifacts/import`, body, { workspaceId });
}
export default async function gitImportRequest(
diff --git a/app/client/src/git/requests/gitImportRequest.types.ts b/app/client/src/git/requests/gitImportRequest.types.ts
index bfa4def95548..6c2b599c6103 100644
--- a/app/client/src/git/requests/gitImportRequest.types.ts
+++ b/app/client/src/git/requests/gitImportRequest.types.ts
@@ -9,6 +9,7 @@ export interface GitImportRequestParams {
authorEmail: string;
useDefaultProfile?: boolean;
};
+ override?: boolean;
}
export interface GitImportResponseData {
diff --git a/app/client/src/git/sagas/gitImportSaga.ts b/app/client/src/git/sagas/gitImportSaga.ts
index 5f55c4772ebc..4709245d686c 100644
--- a/app/client/src/git/sagas/gitImportSaga.ts
+++ b/app/client/src/git/sagas/gitImportSaga.ts
@@ -2,13 +2,17 @@ import { call, put, select } from "redux-saga/effects";
import { validateResponse } from "sagas/ErrorSagas";
import type { PayloadAction } from "@reduxjs/toolkit";
import gitImportRequest from "git/requests/gitImportRequest";
-import type { GitImportResponse } from "git/requests/gitImportRequest.types";
+import type {
+ GitImportRequestParams,
+ GitImportResponse,
+} from "git/requests/gitImportRequest.types";
import type { GitImportInitPayload } from "git/store/actions/gitImportActions";
import { gitGlobalActions } from "git/store/gitGlobalSlice";
import { getWorkspaceIdForImport } from "ee/selectors/applicationSelectors";
import { GitErrorCodes } from "git/constants/enums";
import { selectGitApiContractsEnabled } from "git/store/selectors/gitFeatureFlagSelectors";
import handleApiErrors from "./helpers/handleApiErrors";
+import type { GitApiError } from "git/store/types";
export default function* gitImportSaga(
action: PayloadAction,
@@ -36,6 +40,7 @@ export default function* gitImportSaga(
gitGlobalActions.gitImportSuccess({ responseData: response.data }),
);
yield put(gitGlobalActions.toggleImportModal({ open: false }));
+ yield put(gitGlobalActions.resetImportOverrideDetails());
}
} catch (e) {
const error = handleApiErrors(e as Error, response);
@@ -47,6 +52,38 @@ export default function* gitImportSaga(
yield put(gitGlobalActions.toggleImportModal({ open: false }));
yield put(gitGlobalActions.toggleRepoLimitErrorModal({ open: true }));
}
+
+ if (GitErrorCodes.DUPLICATE_ARTIFACT_OVERRIDE === error.code) {
+ yield call(handleDuplicateArtifactOverride, error, params);
+ }
+ }
+ }
+}
+
+function* handleDuplicateArtifactOverride(
+ error: GitApiError,
+ params: GitImportRequestParams,
+) {
+ yield put(gitGlobalActions.toggleImportModal({ open: false }));
+
+ let artifactNames = { newArtifactName: null, oldArtifactName: null };
+
+ if (error?.message) {
+ const jsonMatch = error.message.match(/\{.*\}/);
+ const jsonStr = jsonMatch ? jsonMatch[0] : null;
+
+ if (jsonStr) {
+ try {
+ artifactNames = JSON.parse(jsonStr);
+ } catch {}
}
}
+
+ yield put(
+ gitGlobalActions.setImportOverrideDetails({
+ params,
+ oldArtifactName: artifactNames.oldArtifactName ?? "",
+ newArtifactName: artifactNames.newArtifactName ?? "",
+ }),
+ );
}
diff --git a/app/client/src/git/store/actions/gitImportActions.ts b/app/client/src/git/store/actions/gitImportActions.ts
index a823ca7b6d4b..748e921fd9c6 100644
--- a/app/client/src/git/store/actions/gitImportActions.ts
+++ b/app/client/src/git/store/actions/gitImportActions.ts
@@ -48,3 +48,10 @@ export const gitImportErrorAction = (
return state;
};
+
+export const resetGitImportAction = (state: GitGlobalReduxState) => {
+ state.gitImport.loading = false;
+ state.gitImport.error = null;
+
+ return state;
+};
diff --git a/app/client/src/git/store/actions/uiActions.ts b/app/client/src/git/store/actions/uiActions.ts
index b08267ba95cc..ebf205366e99 100644
--- a/app/client/src/git/store/actions/uiActions.ts
+++ b/app/client/src/git/store/actions/uiActions.ts
@@ -3,6 +3,7 @@ import { createArtifactAction } from "../helpers/createArtifactAction";
import type { GitGlobalReduxState } from "../types";
import type { PayloadAction } from "@reduxjs/toolkit";
import type { GitArtifactDef } from "git/types";
+import type { GitImportRequestParams } from "git/requests/gitImportRequest.types";
// connect modal
export interface ToggleConnectModalPayload {
@@ -31,6 +32,7 @@ export const toggleConnectSuccessModalAction =
return state;
});
+// import
export interface ToggleImportModalPayload {
open: boolean;
}
@@ -46,6 +48,29 @@ export const toggleImportModalAction = (
return state;
};
+export const resetImportOverrideDetailsAction = (
+ state: GitGlobalReduxState,
+) => {
+ state.importOverrideDetails = null;
+
+ return state;
+};
+
+export interface SetImportOverrideDetailsPayload {
+ params: GitImportRequestParams;
+ oldArtifactName: string;
+ newArtifactName: string;
+}
+
+export const setImportOverrideDetailsAction = (
+ state: GitGlobalReduxState,
+ action: PayloadAction,
+) => {
+ state.importOverrideDetails = { ...action.payload };
+
+ return state;
+};
+
// disconnect modal
export interface OpenDisconnectModalPayload {
targetArtifactDef: GitArtifactDef;
diff --git a/app/client/src/git/store/gitGlobalSlice.ts b/app/client/src/git/store/gitGlobalSlice.ts
index 3d38b14aa492..2dc8d0028393 100644
--- a/app/client/src/git/store/gitGlobalSlice.ts
+++ b/app/client/src/git/store/gitGlobalSlice.ts
@@ -10,11 +10,16 @@ import {
updateGlobalProfileSuccessAction,
} from "./actions/updateGlobalProfileActions";
import { gitGlobalInitialState } from "./helpers/initialState";
-import { toggleImportModalAction } from "./actions/uiActions";
+import {
+ resetImportOverrideDetailsAction,
+ setImportOverrideDetailsAction,
+ toggleImportModalAction,
+} from "./actions/uiActions";
import {
gitImportErrorAction,
gitImportInitAction,
gitImportSuccessAction,
+ resetGitImportAction,
} from "./actions/gitImportActions";
import {
fetchGlobalSSHKeyErrorAction,
@@ -41,7 +46,10 @@ export const gitGlobalSlice = createSlice({
gitImportInit: gitImportInitAction,
gitImportSuccess: gitImportSuccessAction,
gitImportError: gitImportErrorAction,
+ resetGitImport: resetGitImportAction,
toggleImportModal: toggleImportModalAction,
+ resetImportOverrideDetails: resetImportOverrideDetailsAction,
+ setImportOverrideDetails: setImportOverrideDetailsAction,
toggleRepoLimitErrorModal: toggleRepoLimitErrorModalAction,
},
});
diff --git a/app/client/src/git/store/helpers/initialState.ts b/app/client/src/git/store/helpers/initialState.ts
index 2a602efb2fab..324cace8d9bd 100644
--- a/app/client/src/git/store/helpers/initialState.ts
+++ b/app/client/src/git/store/helpers/initialState.ts
@@ -168,5 +168,6 @@ export const gitGlobalInitialState: GitGlobalReduxState = {
error: null,
},
isImportModalOpen: false,
+ importOverrideDetails: null,
repoLimitErrorModalOpen: false,
};
diff --git a/app/client/src/git/store/selectors/gitGlobalSelectors.ts b/app/client/src/git/store/selectors/gitGlobalSelectors.ts
index 57e5c5123036..84d320cd4c22 100644
--- a/app/client/src/git/store/selectors/gitGlobalSelectors.ts
+++ b/app/client/src/git/store/selectors/gitGlobalSelectors.ts
@@ -14,6 +14,12 @@ export const selectUpdateGlobalProfileState = (state: GitRootState) =>
export const selectImportModalOpen = (state: GitRootState) =>
selectGitGlobal(state).isImportModalOpen;
+export const selectImportOverrideModalOpen = (state: GitRootState) =>
+ !!selectGitGlobal(state).importOverrideDetails;
+
+export const selectImportOverrideDetails = (state: GitRootState) =>
+ selectGitGlobal(state).importOverrideDetails ?? null;
+
export const selectGitImportState = (state: GitRootState) =>
selectGitGlobal(state).gitImport;
diff --git a/app/client/src/git/store/types.ts b/app/client/src/git/store/types.ts
index da80f539a2e8..93d26e5b440d 100644
--- a/app/client/src/git/store/types.ts
+++ b/app/client/src/git/store/types.ts
@@ -20,6 +20,7 @@ import type { FetchGlobalSSHKeyResponseData } from "git/requests/fetchGlobalSSHK
import type { FetchRefsResponseData } from "git/requests/fetchRefsRequest.types";
import type { GitArtifactDef } from "git/types";
import type { PretagResponseData } from "git/requests/pretagRequest.types";
+import type { GitImportRequestParams } from "git/requests/gitImportRequest.types";
export interface GitApiError extends ApiResponseError {
errorType?: string;
@@ -98,6 +99,11 @@ export interface GitGlobalReduxState {
globalSSHKey: GitAsyncState;
// ui
isImportModalOpen: boolean;
+ importOverrideDetails: {
+ params: GitImportRequestParams;
+ oldArtifactName: string;
+ newArtifactName: string;
+ } | null;
repoLimitErrorModalOpen: boolean;
}
diff --git a/app/server/appsmith-git/src/main/java/com/appsmith/git/files/FileUtilsCEImpl.java b/app/server/appsmith-git/src/main/java/com/appsmith/git/files/FileUtilsCEImpl.java
index 9e50cac97a58..e36eea10f825 100644
--- a/app/server/appsmith-git/src/main/java/com/appsmith/git/files/FileUtilsCEImpl.java
+++ b/app/server/appsmith-git/src/main/java/com/appsmith/git/files/FileUtilsCEImpl.java
@@ -84,7 +84,7 @@
@Import({GitServiceConfig.class})
public class FileUtilsCEImpl implements FileInterface {
- private final GitServiceConfig gitServiceConfig;
+ protected final GitServiceConfig gitServiceConfig;
protected final FSGitHandler fsGitHandler;
private final GitExecutor gitExecutor;
protected final FileOperations fileOperations;
@@ -98,7 +98,7 @@ public class FileUtilsCEImpl implements FileInterface {
private static final Pattern ALLOWED_FILE_EXTENSION_PATTERN =
Pattern.compile("(.*?)\\.(md|MD|git|gitignore|github|yml|yaml)$");
- private final Scheduler scheduler = Schedulers.boundedElastic();
+ protected final Scheduler scheduler = Schedulers.boundedElastic();
private static final String CANVAS_WIDGET = "(Canvas)[0-9]*.";
@@ -1250,6 +1250,12 @@ public Mono