Skip to content

fix: generate unique widgetIds when cloning page#41762

Open
amelia-c0n wants to merge 6 commits intoreleasefrom
fix/clone-widgetid-duplicates
Open

fix: generate unique widgetIds when cloning page#41762
amelia-c0n wants to merge 6 commits intoreleasefrom
fix/clone-widgetid-duplicates

Conversation

@amelia-c0n
Copy link
Copy Markdown
Contributor

@amelia-c0n amelia-c0n commented Apr 24, 2026

Description

Problem

The Clone Page feature copied the source page's DSL verbatim, so every widget in the cloned page kept the exact same widgetId as its counterpart on the source page. This produced duplicate widget ids at the application level, causing some small bugs.

Fix

Regenerate all widget IDs in the cloned page's DSL as part of the server-side clone flow, while preserving internal references so the cloned page remains structurally identical to the source.

Changes

  • New helper DslUtils.regenerateWidgetIds(JSONObject) (app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/DslUtils.java)

Deep-copies the source DSL (source is never mutated).
Phase 1 — walks the widget tree via children, building an oldId → newId map for every widget.
Phase 2 — id-blind rewrite: recursively visits every map entry and list element and replaces any string equal to an old id with its new id, so references anywhere in the DSL stay consistent (including non-children locations like Tabs' tabsObj and List V2's mainCanvasId).
Leaves the root MainContainer (widgetId: "0") untouched — it is a well-known fixed id that children reference.

  • New helper DslUtils.generateWidgetId()

Produces a 10-char lowercase alphanumeric id using RandomStringUtils.secure() (SecureRandom-backed), matching the format and id space of the client's generateReactKey (nanoid) so cloned widgets are indistinguishable from client-created ones.

  • ApplicationPageServiceCEImpl.clonePageGivenApplicationId

Calls DslUtils.regenerateWidgetIds(layout.getDsl()) when building each new Layout for the cloned page. Single-line change at the clone site; no other flows touched.

Tests

  • Unit tests (DslUtilsTest.java) — 8 new tests covering:
    generateWidgetId format (length, alphabet) and distinctness across 1000 calls.
    regenerateWidgetIds for null/empty DSL, MainContainer preservation, deep reference rewriting (nested parentId chains + non-children id references), source non-mutation, no duplicate ids across a 20-widget tree, and empty-children tree.
  • Integration test (PageServiceTest.clonePage_whenPageCloned_defaultIdsRetained) — added assertions that the cloned MainContainer's widgetId and its child widget's widgetId both differ from the source, while widget names are preserved (critical for bindings to continue to work).

Scope and safety

  • Affects only the clone flow. No migration; existing apps with pre-existing duplicate widget ids are not touched and continue to work as they do today.
  • No client changes. The client already treats widget ids as opaque per-page identifiers.
  • Names are preserved, so mustache bindings (e.g. {{Button1.text}}) on the cloned page keep working against the cloned widgets.
  • No impact on git, import/export, fork, or published DSL — git uses gitSyncId + widgetId (gitSyncId is per-page); import/fork preserve ids as-is.
  • Uniqueness guarantee matches the client generator

How to verify

  1. On the source page, add a Container with a child widget and a Text widget bound to something like {{Container1.widgetId}}. Note the displayed id.
  2. Clone the page.
  3. Open the cloned page:
  • The Text widget should display a different 10-char id than on the source page.
  • Widget names, structure, and parent-child relationships are identical to the source.
  1. The source page is unchanged — its Text widget still shows the original id.
  2. Bindings on the cloned page evaluate correctly, and the page renders without errors.

Fixes https://github.com/appsmithorg/appsmith-ee/issues/8933

Automation

/ok-to-test tags=""

🔍 Cypress test results

Warning

Tests have not run on the HEAD 30118ce yet


Thu, 30 Apr 2026 07:12:44 UTC

Communication

Should the DevRel and Marketing teams inform users about this change?

  • Yes
  • [ x ] No

Summary by CodeRabbit

  • New Features

    • Utility added to regenerate widget IDs within a layout so internal references remain consistent.
  • Bug Fixes

    • Prevented widget ID collisions when cloning pages by regenerating IDs in cloned layouts while preserving structure and root container.
  • Tests

    • Expanded test coverage for ID generation, uniqueness, regeneration behavior, non-destructive cloning, and internal reference preservation.

@amelia-c0n amelia-c0n requested a review from subrata71 April 24, 2026 13:21
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 24, 2026

Walkthrough

Adds widget-id regeneration: new secure 10-character id generator and a DSL-wide regenerateWidgetIds(JSONObject) that deep-copies a layout JSON, remaps non-root widget ids, and rewrites id references (keys ending with Id) so internal relationships remain consistent.

Changes

Cohort / File(s) Summary
DSL Utilities
app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/DslUtils.java
New generateWidgetId() and regenerateWidgetIds(JSONObject) added; JSON deep-copy helper; builds old→new id map (excluding root "0"), regenerates ids, and rewrites string references for keys ending with Id.
Page Cloning
app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ApplicationPageServiceCEImpl.java
Calls DslUtils.regenerateWidgetIds() during page clone to ensure cloned layout DSLs use regenerated widget ids.
Tests
app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/DslUtilsTest.java, app/server/appsmith-server/src/test/java/com/appsmith/server/services/PageServiceTest.java
Extensive tests added/updated: id format and uniqueness, null/empty handling, root preservation and edge-case when root has empty children, parent/id cross-reference rewriting, immutability of source DSL, and updated clone assertions handling deserialized types.

Sequence Diagram

sequenceDiagram
    participant Client as Client
    participant PageService as ApplicationPageService
    participant DslUtils as DslUtils
    participant JsonObj as JSONObject (DSL)
    participant DB as Database

    Client->>PageService: clonePage(sourcePageId)
    PageService->>DB: fetch source page layouts
    DB-->>PageService: source layouts

    loop for each layout
        PageService->>JsonObj: parse layout DSL
        PageService->>DslUtils: regenerateWidgetIds(dsl)
        activate DslUtils
        DslUtils->>JsonObj: deep-copy DSL
        DslUtils->>DslUtils: collect widgetIds (exclude "0") and generate new IDs
        DslUtils->>JsonObj: recursively rewrite id references (keys ending with "Id")
        DslUtils-->>PageService: regenerated DSL
        deactivate DslUtils
        PageService->>PageService: set regenerated DSL on clone
    end

    PageService->>DB: save cloned page with new widget IDs
    DB-->>PageService: success
    PageService-->>Client: return cloned page
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

Ten chars spun, from old to new,
Roots stay steady, children too,
IDs refreshed, each link aligned,
Cloned pages wake with ties defined,
A quiet fix, tidy and true. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.53% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: generating unique widget IDs during page cloning to fix duplicate ID issues.
Linked Issues check ✅ Passed The PR fully addresses the linked issue #8933 by implementing widget ID regeneration during page cloning to eliminate duplicate IDs while preserving internal consistency.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the clone flow: widget ID regeneration utilities, integration into the clone service, and tests validating the feature.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering problem, solution, implementation details, testing strategy, scope, and verification steps.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/clone-widgetid-duplicates

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
app/server/appsmith-server/src/test/java/com/appsmith/server/services/PageServiceTest.java (1)

944-953: Optional: tighten the clone assertions to also check id format.

Currently the test only ensures the cloned ids are != "firstWidget"/!= "Table1". A stricter check (e.g., matches("[0-9a-z]{10}")) would catch regressions where clone leaves the id untouched after a future refactor but still happens to differ by some unrelated mutation. Unit tests in DslUtilsTest already cover format, so this is purely belt-and-braces.

♻️ Suggested tightening
-                    assertThat(clonedDsl.get("widgetId")).isNotEqualTo("firstWidget");
+                    assertThat(clonedDsl.get("widgetId"))
+                            .isNotEqualTo("firstWidget")
+                            .asString()
+                            .matches("[0-9a-z]{10}");
@@
-                    assertThat(clonedChild.get("widgetId")).isNotEqualTo("Table1");
+                    assertThat(clonedChild.get("widgetId"))
+                            .isNotEqualTo("Table1")
+                            .asString()
+                            .matches("[0-9a-z]{10}");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/server/appsmith-server/src/test/java/com/appsmith/server/services/PageServiceTest.java`
around lines 944 - 953, Tighten the clone assertions in PageServiceTest by
validating the regenerated widgetId formats (not just inequality): after
retrieving unpublishedPage.getLayouts().get(0).getDsl() as clonedDsl and its
child clonedChild, assert that clonedDsl.get("widgetId") and
clonedChild.get("widgetId") match the expected id pattern (e.g., a regex like
"[0-9a-z]{10}" used elsewhere in DslUtilsTest) rather than merely checking !=
"firstWidget"/"Table1", so the test ensures both uniqueness and correct id
format on clone.
app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/DslUtils.java (1)

124-132: Minor: graceful-fallback returns the source reference, not a copy.

If JSONValue.parse(...) produces something other than a JSONObject, deepCopy returns the input directly. regenerateWidgetIds would then mutate the caller's DSL via rewriteWidgetIdReferences. In practice this path is unreachable for valid DSL (so this is a nitpick), but you could make the fallback safer by returning sourceDsl at the caller site (i.e., bail out before rewriteWidgetIdReferences) or by cloning the map manually.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/DslUtils.java`
around lines 124 - 132, The fallback in deepCopy currently returns the original
JSONObject reference which allows callers like regenerateWidgetIds ->
rewriteWidgetIdReferences to mutate the caller's DSL; change the behavior so
deepCopy never returns the original reference: when parsed is not a JSONObject,
create and return a new JSONObject clone (e.g., construct a new JSONObject from
the original content or its string form) instead of returning source, or
alternatively have regenerateWidgetIds bail out early when deepCopy fails;
update deepCopy (and call sites such as
regenerateWidgetIds/rewriteWidgetIdReferences if you choose the bail-out
approach) so the caller always works with a copy, not the original reference.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/DslUtils.java`:
- Around line 74-91: collectWidgetIds only recurses into FieldName.CHILDREN and
therefore misses widgets under list templates (FieldName.LIST_WIDGET_TEMPLATE),
causing IDs there not to be regenerated; update collectWidgetIds to also
traverse the LIST_WIDGET_TEMPLATE field (and any List/Map nested structures
similar to rewriteWidgetIdReferences) by checking for
FieldName.LIST_WIDGET_TEMPLATE (and/or iterating all Map/List entries) and
recursing into JSONObject entries so template widgets get their IDs added to
oldToNewWidgetIdMap (use the same guards as existing code: skip empty IDs,
MAIN_CONTAINER_WIDGET_ID, and already-mapped IDs, and call generateWidgetId()
when adding).

---

Nitpick comments:
In
`@app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/DslUtils.java`:
- Around line 124-132: The fallback in deepCopy currently returns the original
JSONObject reference which allows callers like regenerateWidgetIds ->
rewriteWidgetIdReferences to mutate the caller's DSL; change the behavior so
deepCopy never returns the original reference: when parsed is not a JSONObject,
create and return a new JSONObject clone (e.g., construct a new JSONObject from
the original content or its string form) instead of returning source, or
alternatively have regenerateWidgetIds bail out early when deepCopy fails;
update deepCopy (and call sites such as
regenerateWidgetIds/rewriteWidgetIdReferences if you choose the bail-out
approach) so the caller always works with a copy, not the original reference.

In
`@app/server/appsmith-server/src/test/java/com/appsmith/server/services/PageServiceTest.java`:
- Around line 944-953: Tighten the clone assertions in PageServiceTest by
validating the regenerated widgetId formats (not just inequality): after
retrieving unpublishedPage.getLayouts().get(0).getDsl() as clonedDsl and its
child clonedChild, assert that clonedDsl.get("widgetId") and
clonedChild.get("widgetId") match the expected id pattern (e.g., a regex like
"[0-9a-z]{10}" used elsewhere in DslUtilsTest) rather than merely checking !=
"firstWidget"/"Table1", so the test ensures both uniqueness and correct id
format on clone.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a4e29f1e-3414-4961-9b16-464e91b1adc1

📥 Commits

Reviewing files that changed from the base of the PR and between 99d6918 and 1fea8cd.

📒 Files selected for processing (4)
  • app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/DslUtils.java
  • app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ApplicationPageServiceCEImpl.java
  • app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/DslUtilsTest.java
  • app/server/appsmith-server/src/test/java/com/appsmith/server/services/PageServiceTest.java

Comment on lines +74 to +91
private static void collectWidgetIds(JSONObject widget, Map<String, String> oldToNewWidgetIdMap) {
Object widgetIdValue = widget.get(FieldName.WIDGET_ID);
if (widgetIdValue instanceof String widgetId
&& !widgetId.isEmpty()
&& !MAIN_CONTAINER_WIDGET_ID.equals(widgetId)
&& !oldToNewWidgetIdMap.containsKey(widgetId)) {
oldToNewWidgetIdMap.put(widgetId, generateWidgetId());
}

Object children = widget.get(FieldName.CHILDREN);
if (children instanceof List<?> childrenList) {
for (Object child : childrenList) {
if (child instanceof JSONObject childObject) {
collectWidgetIds(childObject, oldToNewWidgetIdMap);
}
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Hunt for widget DSL shapes where widget nodes live outside "children".
rg -nP --type=ts -C2 '\btemplate\s*:\s*\{' -g '!**/node_modules/**' -g '**/widgets/**'
rg -nP --type=ts -C2 '"widgetId"\s*:' -g '!**/node_modules/**' -g '**/widgets/**/*.json'
rg -nP --type=ts -C2 '\b(tabsObj|template|mainCanvas)\b' -g '!**/node_modules/**' -g '**/widgets/ListWidget*/**'

Repository: appsmithorg/appsmith

Length of output: 50376


🏁 Script executed:

cat -n app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/DslUtils.java | head -120

Repository: appsmithorg/appsmith

Length of output: 6048


🏁 Script executed:

# Look at the test JSON files to see if they contain "template" field with widgets
grep -l "template" app/server/appsmith-server/src/test/resources/com/appsmith/server/widgets/refactors/*.json | head -3

Repository: appsmithorg/appsmith

Length of output: 434


🏁 Script executed:

# Check if test cases use or validate the collectWidgetIds function
rg "collectWidgetIds" app/server/appsmith-server/src/test/java --type=java -A 5

Repository: appsmithorg/appsmith

Length of output: 46


🏁 Script executed:

# Examine one of the test JSON files for template structure
head -300 app/server/appsmith-server/src/test/resources/com/appsmith/server/widgets/refactors/refactorDslWithOnlyWidgets.json | tail -150

Repository: appsmithorg/appsmith

Length of output: 5052


🏁 Script executed:

# Search for tests of DslUtils in test directory
find app/server/appsmith-server/src/test -name "*DslUtils*" -type f

Repository: appsmithorg/appsmith

Length of output: 151


🏁 Script executed:

# Look for tests that verify regenerateWidgetIds
rg -l "regenerateWidgetIds" app/server/appsmith-server/src/test --type=java

Repository: appsmithorg/appsmith

Length of output: 151


🏁 Script executed:

cat app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/DslUtilsTest.java | head -250

Repository: appsmithorg/appsmith

Length of output: 11395


🏁 Script executed:

# Search for "template" in server-side test resources/code
rg -i "template" app/server/appsmith-server/src/test --type=java | head -20

Repository: appsmithorg/appsmith

Length of output: 3662


🏁 Script executed:

# Check if ListWidget uses template on server-side
rg -i "template" app/server/appsmith-server/src/main --type=java | head -20

Repository: appsmithorg/appsmith

Length of output: 3570


🏁 Script executed:

# Find the FieldName class and check for LIST_WIDGET_TEMPLATE
rg -A 2 "LIST_WIDGET_TEMPLATE" app/server/appsmith-server/src/main --type=java

Repository: appsmithorg/appsmith

Length of output: 1000


🏁 Script executed:

# Check how template widgets are handled in regenerateWidgetIds or if there's a separate method
rg -B 5 -A 10 "LIST_WIDGET_TEMPLATE" app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/DslUtils.java

Repository: appsmithorg/appsmith

Length of output: 46


🏁 Script executed:

# Check if regenerateWidgetIds is called with template handling elsewhere
rg "regenerateWidgetIds" app/server/appsmith-server/src/main --type=java -B 3 -A 3

Repository: appsmithorg/appsmith

Length of output: 2164


🏁 Script executed:

# Check what calls regenerateWidgetIds to understand the flow
rg -B 5 -A 8 "regenerateWidgetIds" app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ApplicationPageServiceCEImpl.java

Repository: appsmithorg/appsmith

Length of output: 893


🏁 Script executed:

# Look for any test that validates template widget handling in DSL
rg -A 20 "regenerateWidgetIds" app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/DslUtilsTest.java

Repository: appsmithorg/appsmith

Length of output: 6416


collectWidgetIds misses nested widget trees under non-children keys — List widget template field contains widgets with IDs that won't be collected.

The traversal asymmetry causes data integrity issues: collectWidgetIds only recurses through FieldName.CHILDREN and skips the template field (defined as "template" in FieldName.LIST_WIDGET_TEMPLATE). This means widgets nested under List1.template.{WidgetName} won't have their IDs collected or regenerated during page cloning. However, rewriteWidgetIdReferences traverses all nested structures (Maps and Lists), so it will still attempt to rewrite any references to those uncollected template widget IDs elsewhere in the DSL. The result is an inconsistent state where template widgets retain source IDs while references to them are updated, causing potential collisions or dangling pointers.

The test suite lacks coverage for this scenario — all DslUtilsTest cases use only the children structure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/DslUtils.java`
around lines 74 - 91, collectWidgetIds only recurses into FieldName.CHILDREN and
therefore misses widgets under list templates (FieldName.LIST_WIDGET_TEMPLATE),
causing IDs there not to be regenerated; update collectWidgetIds to also
traverse the LIST_WIDGET_TEMPLATE field (and any List/Map nested structures
similar to rewriteWidgetIdReferences) by checking for
FieldName.LIST_WIDGET_TEMPLATE (and/or iterating all Map/List entries) and
recursing into JSONObject entries so template widgets get their IDs added to
oldToNewWidgetIdMap (use the same guards as existing code: skip empty IDs,
MAIN_CONTAINER_WIDGET_ID, and already-mapped IDs, and call generateWidgetId()
when adding).

@github-actions
Copy link
Copy Markdown

Failed server tests

  • com.appsmith.server.services.PageServiceTest#clonePage_whenPageCloned_defaultIdsRetained

1 similar comment
@github-actions
Copy link
Copy Markdown

Failed server tests

  • com.appsmith.server.services.PageServiceTest#clonePage_whenPageCloned_defaultIdsRetained

@amelia-c0n amelia-c0n changed the title Fix: Clone Page generates unique widget ids fix: generate unique widgetIds when cloning page Apr 24, 2026
@github-actions github-actions Bot added the Bug Something isn't working label Apr 24, 2026
@github-actions
Copy link
Copy Markdown

Failed server tests

  • com.appsmith.server.services.PageServiceTest#clonePage_whenPageCloned_defaultIdsRetained

1 similar comment
@github-actions
Copy link
Copy Markdown

Failed server tests

  • com.appsmith.server.services.PageServiceTest#clonePage_whenPageCloned_defaultIdsRetained

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/DslUtils.java (1)

74-91: ⚠️ Potential issue | 🟠 Major

Traversal still skips non-children widget containers (e.g. List widget template, Tabs tabsObj).

collectWidgetIds only descends through FieldName.CHILDREN, so widgets nested under template (ListWidgetV2) or tabsObj (Tabs) never make it into oldToNewWidgetIdMap. However, rewriteWidgetIdReferences happily walks those subtrees and will rewrite any *Id fields it finds — producing an inconsistent state where template/tab-internal widgets keep the source widgetId while parentId/mainCanvasId references to them may get rewritten (or dangle against the parent page).

At minimum, consider mirroring rewriteWidgetIdReferences' generic traversal inside collectWidgetIds (walk any nested Map/List for widgetId fields), or explicitly recurse into FieldName.LIST_WIDGET_TEMPLATE and other known widget-bearing keys. Please also add a dedicated test covering a List widget with populated template.

🛠️ Suggested generalised traversal
     private static void collectWidgetIds(JSONObject widget, Map<String, String> oldToNewWidgetIdMap) {
         Object widgetIdValue = widget.get(FieldName.WIDGET_ID);
         if (widgetIdValue instanceof String widgetId
                 && !widgetId.isEmpty()
                 && !MAIN_CONTAINER_WIDGET_ID.equals(widgetId)
                 && !oldToNewWidgetIdMap.containsKey(widgetId)) {
             oldToNewWidgetIdMap.put(widgetId, generateWidgetId());
         }

-        Object children = widget.get(FieldName.CHILDREN);
-        if (children instanceof List<?> childrenList) {
-            for (Object child : childrenList) {
-                if (child instanceof JSONObject childObject) {
-                    collectWidgetIds(childObject, oldToNewWidgetIdMap);
-                }
-            }
-        }
+        // Recurse into every nested object/array so widget trees nested under
+        // non-"children" keys (e.g. List widget template, Tabs tabsObj) are covered.
+        for (Object value : widget.values()) {
+            collectNestedWidgetIds(value, oldToNewWidgetIdMap);
+        }
+    }
+
+    private static void collectNestedWidgetIds(Object node, Map<String, String> oldToNewWidgetIdMap) {
+        if (node instanceof JSONObject obj) {
+            collectWidgetIds(obj, oldToNewWidgetIdMap);
+        } else if (node instanceof List<?> list) {
+            for (Object element : list) {
+                collectNestedWidgetIds(element, oldToNewWidgetIdMap);
+            }
+        }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/DslUtils.java`
around lines 74 - 91, collectWidgetIds currently only recurses into
FieldName.CHILDREN, missing widgets nested under other keys (e.g. List widget
FieldName.LIST_WIDGET_TEMPLATE or Tabs FieldName.TABS_OBJ) and causing
inconsistent ID rewrites; update collectWidgetIds to mirror
rewriteWidgetIdReferences' traversal by descending into any nested
Map/JSONObject and List/JSONArray it encounters and collecting
widget.get(FieldName.WIDGET_ID) values (skipping MAIN_CONTAINER_WIDGET_ID and
empty IDs) so all embedded widgets are mapped, explicitly include known
container keys like FieldName.LIST_WIDGET_TEMPLATE and FieldName.TABS_OBJ if you
prefer explicit recursion, and add a unit test that creates a List widget with a
populated template to assert IDs in the template are included in
oldToNewWidgetIdMap.
🧹 Nitpick comments (1)
app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/DslUtils.java (1)

130-138: Optional: JSONObject.toJSONString() + JSONValue.parse() is a pricey deep-copy on large DSLs.

Works fine, but on sizeable pages this is O(N) serialization + O(N) parse + full object re-allocation. If clone-page latency ever shows up in traces, a structural copy that walks Map/List directly would be materially cheaper. Leaving as-is for now is fine.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/DslUtils.java`
around lines 130 - 138, The current deepCopy method in deepCopy(JSONObject) uses
JSONObject.toJSONString() + JSONValue.parse(), which is expensive for large
DSLs; replace this serialization round-trip with a structural recursive copy
that walks JSONObject/JSONArray (and Map/List primitives) and clones entries and
elements to new JSONObject/JSONArray objects to avoid O(N) serialize/parse and
extra allocation overhead. Update the deepCopy implementation to detect
JSONValue types and recursively copy Maps (JSONObject), Lists/JSONArray and
primitive values, returning a newly constructed JSONObject copy (or the original
fallback only on irrecoverable types), keeping the method name deepCopy and
using the same return type so callers are unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/DslUtils.java`:
- Around line 74-91: collectWidgetIds currently only recurses into
FieldName.CHILDREN, missing widgets nested under other keys (e.g. List widget
FieldName.LIST_WIDGET_TEMPLATE or Tabs FieldName.TABS_OBJ) and causing
inconsistent ID rewrites; update collectWidgetIds to mirror
rewriteWidgetIdReferences' traversal by descending into any nested
Map/JSONObject and List/JSONArray it encounters and collecting
widget.get(FieldName.WIDGET_ID) values (skipping MAIN_CONTAINER_WIDGET_ID and
empty IDs) so all embedded widgets are mapped, explicitly include known
container keys like FieldName.LIST_WIDGET_TEMPLATE and FieldName.TABS_OBJ if you
prefer explicit recursion, and add a unit test that creates a List widget with a
populated template to assert IDs in the template are included in
oldToNewWidgetIdMap.

---

Nitpick comments:
In
`@app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/DslUtils.java`:
- Around line 130-138: The current deepCopy method in deepCopy(JSONObject) uses
JSONObject.toJSONString() + JSONValue.parse(), which is expensive for large
DSLs; replace this serialization round-trip with a structural recursive copy
that walks JSONObject/JSONArray (and Map/List primitives) and clones entries and
elements to new JSONObject/JSONArray objects to avoid O(N) serialize/parse and
extra allocation overhead. Update the deepCopy implementation to detect
JSONValue types and recursively copy Maps (JSONObject), Lists/JSONArray and
primitive values, returning a newly constructed JSONObject copy (or the original
fallback only on irrecoverable types), keeping the method name deepCopy and
using the same return type so callers are unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0ad95f6d-e6d7-447c-a53f-3a95afb95d9a

📥 Commits

Reviewing files that changed from the base of the PR and between 1fea8cd and 015c955.

📒 Files selected for processing (2)
  • app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/DslUtils.java
  • app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/DslUtilsTest.java

@github-actions
Copy link
Copy Markdown

Failed server tests

  • com.appsmith.server.services.PageServiceTest#clonePage_whenPageCloned_defaultIdsRetained

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
app/server/appsmith-server/src/test/java/com/appsmith/server/services/PageServiceTest.java (1)

944-958: Consider tightening the regenerated-id assertions.

Right now the test only proves the cloned ids are not the literal sentinels "firstWidget" / "Table1". That bar is fairly low — a regression that, say, blanks out the id, returns null, or accidentally reuses the same regenerated value for parent and child would still slip past. Since the whole point of this PR is "unique widget ids in the documented client format", it would be worth asserting:

  • both ids are non-blank and match the 10-char lowercase-alphanumeric format produced by DslUtils.generateWidgetId(), and
  • the parent's widgetId is not equal to the child's widgetId (uniqueness within the cloned DSL).
♻️ Suggested extra assertions
                     Object clonedChildren = clonedDsl.get("children");
                     assertThat(clonedChildren).isInstanceOf(List.class);
                     // After persisting the cloned page and reading it back, nested DSL nodes
                     // come back as LinkedHashMap rather than net.minidev.json.JSONObject, so we
                     // assert against the generic Map contract instead of casting to JSONObject.
                     Object firstChild = ((List<?>) clonedChildren).get(0);
                     assertThat(firstChild).isInstanceOf(Map.class);
                     Map<?, ?> clonedChild = (Map<?, ?>) firstChild;
                     assertThat(clonedChild.get("widgetName")).isEqualTo("Table1");
                     assertThat(clonedChild.get("widgetId")).isNotEqualTo("Table1");
+                    // Regenerated ids should match the client id format (10 lowercase alphanumerics)
+                    // and be unique within the cloned DSL.
+                    assertThat((String) clonedDsl.get("widgetId")).matches("[a-z0-9]{10}");
+                    assertThat((String) clonedChild.get("widgetId")).matches("[a-z0-9]{10}");
+                    assertThat(clonedDsl.get("widgetId")).isNotEqualTo(clonedChild.get("widgetId"));

Bonus points if you also flip the source DSL's seed widgetIds (lines 815, 822) to actual 10-char lookalikes — keeps the fixture honest with the production format.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/server/appsmith-server/src/test/java/com/appsmith/server/services/PageServiceTest.java`
around lines 944 - 958, Tighten the cloned-id assertions: instead of only
asserting clonedDsl.widgetId != "firstWidget" and clonedChild.widgetId !=
"Table1", assert both cloned IDs are non-blank, match the 10-char
lowercase-alphanumeric pattern produced by DslUtils.generateWidgetId(), and
assert the parent id (clonedDsl.get("widgetId")) is not equal to the child id
(clonedChild.get("widgetId")) to guarantee uniqueness within the cloned DSL;
also consider updating the source fixture widgetId seeds to realistic 10-char
lookalikes so tests better reflect production format.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@app/server/appsmith-server/src/test/java/com/appsmith/server/services/PageServiceTest.java`:
- Around line 944-958: Tighten the cloned-id assertions: instead of only
asserting clonedDsl.widgetId != "firstWidget" and clonedChild.widgetId !=
"Table1", assert both cloned IDs are non-blank, match the 10-char
lowercase-alphanumeric pattern produced by DslUtils.generateWidgetId(), and
assert the parent id (clonedDsl.get("widgetId")) is not equal to the child id
(clonedChild.get("widgetId")) to guarantee uniqueness within the cloned DSL;
also consider updating the source fixture widgetId seeds to realistic 10-char
lookalikes so tests better reflect production format.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 85949881-b18f-44db-9abc-1baa4a641ab2

📥 Commits

Reviewing files that changed from the base of the PR and between 015c955 and 96e048e.

📒 Files selected for processing (1)
  • app/server/appsmith-server/src/test/java/com/appsmith/server/services/PageServiceTest.java

@amelia-c0n
Copy link
Copy Markdown
Contributor Author

/build-deploy-preview skip-tests=true

@github-actions
Copy link
Copy Markdown

Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/24983989304.
Workflow: On demand build Docker image and deploy preview.
skip-tests: true.
env: ``.
PR: 41762.
recreate: .
base-image-tag: .

@github-actions
Copy link
Copy Markdown

Deploy-Preview-URL: https://ce-41762.dp.appsmith.com

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant