,
+ ) => ({
+ ...state,
+ isModalOpen: true,
+ entity: action.payload,
+ targetApplications: [],
+ targetPages: [],
+ }),
+ [ReduxActionTypes.CLOSE_COPY_ENTITY_TO_APP_MODAL]: (
+ state: CopyEntityToAppReduxState,
+ ) => ({
+ ...state,
+ isModalOpen: false,
+ entity: null,
+ }),
+ [ReduxActionTypes.FETCH_COPY_TARGET_PAGES_INIT]: (
+ state: CopyEntityToAppReduxState,
+ ) => ({
+ ...state,
+ isFetchingPages: true,
+ targetPages: [],
+ }),
+ [ReduxActionTypes.FETCH_COPY_TARGET_PAGES_SUCCESS]: (
+ state: CopyEntityToAppReduxState,
+ action: ReduxAction<{ pages: ApplicationPagePayload[] }>,
+ ) => ({
+ ...state,
+ isFetchingPages: false,
+ targetPages: action.payload.pages,
+ }),
+ [ReduxActionErrorTypes.FETCH_COPY_TARGET_PAGES_ERROR]: (
+ state: CopyEntityToAppReduxState,
+ ) => ({
+ ...state,
+ isFetchingPages: false,
+ targetPages: [],
+ }),
+ [ReduxActionTypes.FETCH_COPY_TARGET_APPLICATIONS_INIT]: (
+ state: CopyEntityToAppReduxState,
+ ) => ({
+ ...state,
+ isFetchingApplications: true,
+ targetApplications: [],
+ }),
+ [ReduxActionTypes.FETCH_COPY_TARGET_APPLICATIONS_SUCCESS]: (
+ state: CopyEntityToAppReduxState,
+ action: ReduxAction<{ applications: ApplicationPayload[] }>,
+ ) => ({
+ ...state,
+ isFetchingApplications: false,
+ targetApplications: action.payload.applications,
+ }),
+ [ReduxActionErrorTypes.FETCH_COPY_TARGET_APPLICATIONS_ERROR]: (
+ state: CopyEntityToAppReduxState,
+ ) => ({
+ ...state,
+ isFetchingApplications: false,
+ targetApplications: [],
+ }),
+ [ReduxActionTypes.COPY_ACTION_TO_APP_INIT]: (
+ state: CopyEntityToAppReduxState,
+ ) => ({
+ ...state,
+ isCopying: true,
+ }),
+ [ReduxActionTypes.COPY_JS_ACTION_TO_APP_INIT]: (
+ state: CopyEntityToAppReduxState,
+ ) => ({
+ ...state,
+ isCopying: true,
+ }),
+ [ReduxActionTypes.COPY_ACTION_TO_APP_SUCCESS]: (
+ state: CopyEntityToAppReduxState,
+ ) => ({
+ ...state,
+ isCopying: false,
+ }),
+ [ReduxActionTypes.COPY_JS_ACTION_TO_APP_SUCCESS]: (
+ state: CopyEntityToAppReduxState,
+ ) => ({
+ ...state,
+ isCopying: false,
+ }),
+ [ReduxActionErrorTypes.COPY_ACTION_TO_APP_ERROR]: (
+ state: CopyEntityToAppReduxState,
+ ) => ({
+ ...state,
+ isCopying: false,
+ }),
+ [ReduxActionErrorTypes.COPY_JS_ACTION_TO_APP_ERROR]: (
+ state: CopyEntityToAppReduxState,
+ ) => ({
+ ...state,
+ isCopying: false,
+ }),
+});
+
+export default copyEntityToAppReducer;
diff --git a/app/client/src/selectors/copyToAppSelectors.ts b/app/client/src/selectors/copyToAppSelectors.ts
new file mode 100644
index 000000000000..8d46eec5f4e2
--- /dev/null
+++ b/app/client/src/selectors/copyToAppSelectors.ts
@@ -0,0 +1,30 @@
+import type { DefaultRootState } from "react-redux";
+import type { ApplicationPayload } from "entities/Application";
+import type { ApplicationPagePayload } from "ee/api/ApplicationApi";
+import type { CopyToAppModalEntity } from "pages/Editor/Explorer/CopyToApp/types";
+
+export const getIsCopyToAppModalOpen = (state: DefaultRootState): boolean =>
+ state.ui.copyEntityToApp.isModalOpen;
+
+export const getCopyToAppModalEntity = (
+ state: DefaultRootState,
+): CopyToAppModalEntity | null => state.ui.copyEntityToApp.entity;
+
+export const getCopyTargetApplications = (
+ state: DefaultRootState,
+): ApplicationPayload[] => state.ui.copyEntityToApp.targetApplications;
+
+export const getIsFetchingCopyTargetApplications = (
+ state: DefaultRootState,
+): boolean => state.ui.copyEntityToApp.isFetchingApplications;
+
+export const getCopyTargetPages = (
+ state: DefaultRootState,
+): ApplicationPagePayload[] => state.ui.copyEntityToApp.targetPages;
+
+export const getIsFetchingCopyTargetPages = (
+ state: DefaultRootState,
+): boolean => state.ui.copyEntityToApp.isFetchingPages;
+
+export const getIsCopyingEntityToApp = (state: DefaultRootState): boolean =>
+ state.ui.copyEntityToApp.isCopying;
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/actioncollections/importable/ActionCollectionImportableServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/actioncollections/importable/ActionCollectionImportableServiceCEImpl.java
index 3c551841c6c0..a64cd8984e7f 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/actioncollections/importable/ActionCollectionImportableServiceCEImpl.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/actioncollections/importable/ActionCollectionImportableServiceCEImpl.java
@@ -31,6 +31,7 @@
import java.util.Set;
import static com.appsmith.external.helpers.AppsmithBeanUtils.copyNestedNonNullProperties;
+import static com.appsmith.server.helpers.ImportExportUtils.generateUniqueNameForImport;
@Slf4j
@Service
@@ -362,15 +363,9 @@ private void updateActionCollectionNameBeforeMerge(
mappedImportableResourcesDTO.getRefactoringNameReference().keySet();
for (ActionCollection actionCollection : importedNewActionCollectionList) {
- String
- oldNameActionCollection =
- actionCollection.getUnpublishedCollection().getName(),
- newNameActionCollection =
- actionCollection.getUnpublishedCollection().getName();
- int i = 1;
- while (refactoringNameSet.contains(newNameActionCollection)) {
- newNameActionCollection = oldNameActionCollection + i++;
- }
+ String oldNameActionCollection =
+ actionCollection.getUnpublishedCollection().getName();
+ String newNameActionCollection = generateUniqueNameForImport(oldNameActionCollection, refactoringNameSet);
String oldId = actionCollection.getId().split("_")[1];
actionCollection.setId(newNameActionCollection + "_" + oldId);
actionCollection.getUnpublishedCollection().setName(newNameActionCollection);
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ImportExportUtils.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ImportExportUtils.java
index 82ba3f57d294..8ed86110d45f 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ImportExportUtils.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ImportExportUtils.java
@@ -14,12 +14,63 @@
import java.time.Instant;
import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import static com.appsmith.external.helpers.AppsmithBeanUtils.copyNestedNonNullProperties;
@Slf4j
public class ImportExportUtils {
+ private static final Pattern TRAILING_NUMBER_PATTERN = Pattern.compile("^(.*?)(\\d+)$");
+
+ /**
+ * Generates a unique name for an entity being imported into a context that already contains
+ * {@code existingNames}, following Appsmith's entity-naming convention.
+ *
+ * When the incoming name already ends in a number it increments from there
+ * (e.g. {@code JSObject1 -> JSObject2}); otherwise it appends an incrementing suffix
+ * (e.g. {@code Query -> Query1}). If the name does not clash it is returned unchanged.
+ *
+ * @param name the incoming entity name
+ * @param existingNames the names already present in the destination context
+ * @return a name not present in {@code existingNames}
+ */
+ public static String generateUniqueNameForImport(String name, Set existingNames) {
+ if (name == null || existingNames == null || !existingNames.contains(name)) {
+ return name;
+ }
+
+ Matcher matcher = TRAILING_NUMBER_PATTERN.matcher(name);
+ // Default to appending a fresh suffix to the whole name. This is also the fallback
+ // when the trailing number cannot be incremented safely (too large to parse, or at
+ // Long.MAX_VALUE where suffix++ would overflow into a negative value).
+ String base = name;
+ long suffix = 0;
+
+ if (matcher.matches()) {
+ try {
+ long parsedSuffix = Long.parseLong(matcher.group(2));
+ if (parsedSuffix < Long.MAX_VALUE) {
+ base = matcher.group(1);
+ suffix = parsedSuffix;
+ }
+ } catch (NumberFormatException e) {
+ // Suffix too large to parse — keep the whole-name fallback above.
+ }
+ }
+
+ String candidate;
+
+ do {
+ suffix++;
+ candidate = base + suffix;
+ } while (existingNames.contains(candidate));
+
+ return candidate;
+ }
+
/**
* Method to provide non-cryptic and user-friendly error message with actionable input for Import-Export flows
*
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/partial/PartialImportServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/partial/PartialImportServiceCEImpl.java
index 1ebdd5095364..ff6b4d8ac07f 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/partial/PartialImportServiceCEImpl.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/partial/PartialImportServiceCEImpl.java
@@ -211,8 +211,12 @@ private Mono importResourceInPage(
importingMetaDTO.setRefName(page.getRefName());
Layout layout =
page.getUnpublishedPage().getLayouts().get(0);
+ // isFQN=false so the collision set includes action-collection (JS object)
+ // and widget names, not just action names. These are plain entity names
+ // (not dotted FQNs), so an imported JS object named "JSObject1" is correctly
+ // detected against an existing "JSObject1" and renamed rather than duplicated.
return refactoringService.getAllExistingEntitiesMono(
- page.getId(), CreatorContextType.PAGE, layout.getId(), true);
+ page.getId(), CreatorContextType.PAGE, layout.getId(), false);
})
.flatMap(nameSet -> {
// Fetch name of the existing resources in the page to avoid name clashing
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/newactions/importable/NewActionImportableServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/newactions/importable/NewActionImportableServiceCEImpl.java
index cfa2aea15de7..7284c0feaf73 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/newactions/importable/NewActionImportableServiceCEImpl.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/newactions/importable/NewActionImportableServiceCEImpl.java
@@ -42,6 +42,7 @@
import java.util.Set;
import static com.appsmith.external.helpers.AppsmithBeanUtils.copyNestedNonNullProperties;
+import static com.appsmith.server.helpers.ImportExportUtils.generateUniqueNameForImport;
import static com.appsmith.server.helpers.ImportExportUtils.sanitizeDatasourceInActionDTO;
import static java.lang.Boolean.TRUE;
@@ -527,12 +528,8 @@ private void updateActionNameBeforeMerge(
mappedImportableResourcesDTO.getRefactoringNameReference().keySet();
for (NewAction newAction : importedNewActionList) {
- String oldNameAction = newAction.getUnpublishedAction().getName(),
- newNameAction = newAction.getUnpublishedAction().getName();
- int i = 1;
- while (refactoringNames.contains(newNameAction)) {
- newNameAction = oldNameAction + i++;
- }
+ String oldNameAction = newAction.getUnpublishedAction().getName();
+ String newNameAction = generateUniqueNameForImport(oldNameAction, refactoringNames);
String oldId = newAction.getId().split("_")[1];
newAction.setId(newNameAction + "_" + oldId);
diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/ImportExportUtilsTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/ImportExportUtilsTest.java
index fac258f850fe..d08826c1674b 100644
--- a/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/ImportExportUtilsTest.java
+++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/ImportExportUtilsTest.java
@@ -84,4 +84,68 @@ public void isDatasourceUpdatedSinceLastCommit() {
// should return false if last commit date is null
Assertions.assertFalse(ImportExportUtils.isDatasourceUpdatedSinceLastCommit(map, actionDTO, null));
}
+
+ @Test
+ void generateUniqueNameForImport_noClash_returnsNameUnchanged() {
+ Assertions.assertEquals(
+ "JSObject1", ImportExportUtils.generateUniqueNameForImport("JSObject1", Set.of("Query1")));
+ }
+
+ @Test
+ void generateUniqueNameForImport_numberedNameClashes_incrementsFromExistingNumber() {
+ // JSObject1 already exists -> JSObject2 (not JSObject11)
+ Assertions.assertEquals(
+ "JSObject2", ImportExportUtils.generateUniqueNameForImport("JSObject1", Set.of("JSObject1")));
+ // JSObject1 and JSObject2 exist -> JSObject3
+ Assertions.assertEquals(
+ "JSObject3",
+ ImportExportUtils.generateUniqueNameForImport("JSObject1", Set.of("JSObject1", "JSObject2")));
+ }
+
+ @Test
+ void generateUniqueNameForImport_unnumberedNameClashes_appendsNumber() {
+ Assertions.assertEquals("Query1", ImportExportUtils.generateUniqueNameForImport("Query", Set.of("Query")));
+ Assertions.assertEquals(
+ "Query2", ImportExportUtils.generateUniqueNameForImport("Query", Set.of("Query", "Query1")));
+ }
+
+ @Test
+ void generateUniqueNameForImport_nullName_returnsNull() {
+ Assertions.assertNull(ImportExportUtils.generateUniqueNameForImport(null, Set.of("Query")));
+ }
+
+ @Test
+ void generateUniqueNameForImport_gapInSequence_picksFirstFreeAboveOwnSuffix() {
+ // JSObject1 clashes; JSObject2 free even though JSObject3 exists -> JSObject2
+ Assertions.assertEquals(
+ "JSObject2",
+ ImportExportUtils.generateUniqueNameForImport("JSObject1", Set.of("JSObject1", "JSObject3")));
+ }
+
+ @Test
+ void generateUniqueNameForImport_internalDigitOnly_appendsNumber() {
+ // "get4Items" has no TRAILING digit -> treated as base, append 1
+ Assertions.assertEquals(
+ "get4Items1", ImportExportUtils.generateUniqueNameForImport("get4Items", Set.of("get4Items")));
+ }
+
+ @Test
+ void generateUniqueNameForImport_allDigitName_incrementsNumber() {
+ Assertions.assertEquals("124", ImportExportUtils.generateUniqueNameForImport("123", Set.of("123")));
+ }
+
+ @Test
+ void generateUniqueNameForImport_oversizedNumericSuffix_fallsBackToAppendingOne() {
+ // 20-digit suffix overflows long -> NumberFormatException guard -> base=whole name, append 1
+ String huge = "Q" + "9".repeat(20);
+ Assertions.assertEquals(huge + "1", ImportExportUtils.generateUniqueNameForImport(huge, Set.of(huge)));
+ }
+
+ @Test
+ void generateUniqueNameForImport_maxLongSuffix_fallsBackToAppendingOne() {
+ // Suffix == Long.MAX_VALUE parses fine but suffix++ would overflow to a negative value;
+ // it must fall back to appending 1 to the whole name rather than producing "Q-9223372036854775808".
+ String maxLong = "Q" + Long.MAX_VALUE;
+ Assertions.assertEquals(maxLong + "1", ImportExportUtils.generateUniqueNameForImport(maxLong, Set.of(maxLong)));
+ }
}
diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/PartialImportServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/PartialImportServiceTest.java
index 6aa2cbfd78e2..1e55a55a1350 100644
--- a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/PartialImportServiceTest.java
+++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/PartialImportServiceTest.java
@@ -61,6 +61,7 @@
import java.util.Map;
import java.util.Optional;
import java.util.Set;
+import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
@@ -414,28 +415,29 @@ public void testPartialImport_nameClashInAction_successWithNoNameDuplicates() {
// Verify that the application has the imported resource
assertThat(application.getPages()).hasSize(1);
+ // Assert the exact, DISTINCT names so a duplicate (e.g. two "utils") is caught:
+ // re-importing must rename the JS object rather than duplicating it.
assertThat(actionCollectionList).hasSize(2);
- Set nameList = Set.of("utils", "utils1");
- actionCollectionList.forEach(collection -> {
- assertThat(nameList.contains(
- collection.getUnpublishedCollection().getName()))
- .isTrue();
- });
+ Set collectionNames = actionCollectionList.stream()
+ .map(collection ->
+ collection.getUnpublishedCollection().getName())
+ .collect(Collectors.toSet());
+ assertThat(collectionNames).containsExactlyInAnyOrder("utils", "utils1");
+
assertThat(actionList).hasSize(8);
- Set actionNames = Set.of(
- "DeleteQuery",
- "UpdateQuery",
- "SelectQuery",
- "InsertQuery",
- "DeleteQuery1",
- "UpdateQuery1",
- "SelectQuery1",
- "InsertQuery1");
- actionList.forEach(action -> {
- assertThat(actionNames.contains(
- action.getUnpublishedAction().getName()))
- .isTrue();
- });
+ Set actionNames = actionList.stream()
+ .map(action -> action.getUnpublishedAction().getName())
+ .collect(Collectors.toSet());
+ assertThat(actionNames)
+ .containsExactlyInAnyOrder(
+ "DeleteQuery",
+ "UpdateQuery",
+ "SelectQuery",
+ "InsertQuery",
+ "DeleteQuery1",
+ "UpdateQuery1",
+ "SelectQuery1",
+ "InsertQuery1");
})
.verifyComplete();
}