Skip to content

fix: prevent duplicate fieldnames on form save#107

Merged
harshtandiya merged 3 commits into
developfrom
fix/fieldnames
Apr 27, 2026
Merged

fix: prevent duplicate fieldnames on form save#107
harshtandiya merged 3 commits into
developfrom
fix/fieldnames

Conversation

@harshtandiya

@harshtandiya harshtandiya commented Apr 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add global dialog component for imperative dialogs (like vue-sonner pattern)
  • Add HTML support for rich dialog messages
  • Detect duplicate fieldnames before save and show user-friendly error with Label(fieldname) pairs

Closes #78

Test plan

  • Add two fields with the same label
  • Try to save — dialog should appear listing duplicates in bold
  • Change one label and save — should succeed

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a global dialog system to display alerts and confirmations across the application.
    • Form submission now validates fieldname uniqueness and alerts users to duplicates before saving.

Adds dialog utility with confirm/alert/show methods callable from
anywhere via TypeScript, similar to vue-sonner pattern.
Shows dialog listing conflicting Label(fieldname) pairs before save.
@coderabbitai

coderabbitai Bot commented Apr 27, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR introduces a global dialog system and adds validation to prevent duplicate fieldnames in form fields. A new dialog utility and component enable centralized dialog management, while the form save action now validates fieldname uniqueness before submission, displaying an alert if duplicates are detected.

Changes

Cohort / File(s) Summary
Global Dialog System
frontend/src/utils/dialog.ts, frontend/src/components/ui/GlobalDialog.vue, frontend/src/App.vue
Introduces a new reactive dialog state management utility and corresponding Vue component. The dialog utility exports DialogState, DialogAction, DialogOptions interfaces and a dialog object with show(), confirm(), alert(), and close() methods. GlobalDialog.vue binds frappe-ui's Dialog to shared state and renders dynamic content as HTML or plain text. App.vue integrates the component into the root template.
Duplicate Fieldname Validation
frontend/src/stores/editForm.ts
Refactors the save() action to be async and adds findDuplicateFieldnames() helper. Before submission, validates fieldname uniqueness by deriving effective fieldnames (from trimmed input or scrubbed label fallback), detects duplicates, and displays an alert dialog if found. Proceeds to submission only if validation passes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A dialog springs to life, so grand and free,
Global and shared for all fields to see,
Duplicate names? Not on our watch today,
Validation warns before data goes astray! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 'fix: prevent duplicate fieldnames on form save' directly addresses the main objective of detecting and preventing duplicate fieldnames, matching the primary change across all modified files.
Description check ✅ Passed The PR description covers the main changes (global dialog, HTML support, duplicate fieldname detection), includes a test plan, and references the linked issue #78, though it lacks the pre-commit and typecheck checklist items from the template.
Linked Issues check ✅ Passed The implementation successfully addresses issue #78 by detecting duplicate fieldnames at save time and preventing submission with user-friendly error messages listing conflicting Label(fieldname) pairs.
Out of Scope Changes check ✅ Passed The global dialog component and HTML support utilities are necessary scaffolding for the duplicate fieldname detection feature, making all changes directly in-scope and justified by the linked requirements.

✏️ 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/fieldnames

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.

@harshtandiya harshtandiya enabled auto-merge April 27, 2026 13:18
@harshtandiya harshtandiya added this pull request to the merge queue Apr 27, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
frontend/src/stores/editForm.ts (1)

94-115: LGTM on duplicate detection logic.

The first-occurrence guard (!duplicates.some(...)) correctly avoids double-pushing the original field when 3+ fields share a name, and using seen.set after the check preserves the original field for existing.label. One small defensive nit: scrubFieldname(field.label) will throw on a null/undefined label; today addField initializes label: "", but consider guarding with field.label ?? "" to stay safe against future call sites or backend-provided fields.

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

In `@frontend/src/stores/editForm.ts` around lines 94 - 115,
findDuplicateFieldnames may call scrubFieldname(field.label) which will throw if
field.label is null/undefined; update the fallback call so scrubFieldname always
receives a string by passing field.label ?? "" (i.e. change the fallback
expression used when computing name in findDuplicateFieldnames to call
scrubFieldname(field.label ?? "")); keep the rest of the duplicate logic the
same so existing.label/field.label behavior is preserved.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/components/ui/GlobalDialog.vue`:
- Around line 14-23: The GlobalDialog.vue currently uses
v-html="dialogState.message" which allows XSS when callers (e.g., editForm.ts)
pass HTML built from user-controlled values; change GlobalDialog.vue to stop
rendering raw HTML by removing v-html and instead render dialogState.message as
plain text (use the existing interpolation branch) or provide a dedicated
slot/prop (e.g., default slot or messageSlot) for callers to supply safe rich
content; alternatively, if keeping HTML mode, require and validate sanitized
HTML by calling a sanitizer (e.g., DOMPurify) inside the GlobalDialog component
before assigning to dialogState.message and update editForm.ts to either escape
label/fieldname or perform sanitization there as well so that dialogState.html
cannot be set with unsanitized user input.

In `@frontend/src/stores/editForm.ts`:
- Around line 125-132: The code builds an HTML string from untrusted values
(duplicates → d.label and d.fieldname from formResource.value.doc.fields) and
passes it to dialog.alert with html: true, enabling XSS; fix by removing HTML
output: construct a plain-text message (e.g., join duplicates.map(d =>
`${d.label} (${d.fieldname})`) with newlines or commas) and call dialog.alert
without the html: true option so the GlobalDialog.vue v-html path is not used;
if you must keep formatting, instead escape d.label and d.fieldname with a small
escapeHtml helper before wrapping in <b> and keep html: true.

In `@frontend/src/utils/dialog.ts`:
- Around line 35-89: The promise leak happens because the default action in
dialog.show has no onClick and v-model closes bypassing closeDialog, leaving
dialogState.resolve set; fix by (1) ensuring the default action created in
dialog.show includes an onClick that calls closeDialog(true) so clicking OK
resolves the promise, and (2) adding a watcher/observer on dialogState.open that
detects when it transitions to false and, if dialogState.resolve exists, calls
closeDialog(false) to resolve and clear the pending promise; reference
dialog.show, dialogState, closeDialog, confirm and alert when making these
changes.

---

Nitpick comments:
In `@frontend/src/stores/editForm.ts`:
- Around line 94-115: findDuplicateFieldnames may call
scrubFieldname(field.label) which will throw if field.label is null/undefined;
update the fallback call so scrubFieldname always receives a string by passing
field.label ?? "" (i.e. change the fallback expression used when computing name
in findDuplicateFieldnames to call scrubFieldname(field.label ?? "")); keep the
rest of the duplicate logic the same so existing.label/field.label behavior is
preserved.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: b744b485-0f52-4a60-ad23-2d55be4d1d36

📥 Commits

Reviewing files that changed from the base of the PR and between 72f63f8 and 8f6df8b.

📒 Files selected for processing (4)
  • frontend/src/App.vue
  • frontend/src/components/ui/GlobalDialog.vue
  • frontend/src/stores/editForm.ts
  • frontend/src/utils/dialog.ts

Comment on lines +14 to +23
<template #body-content>
<p
v-if="dialogState.html"
class="text-p-base text-gray-700"
v-html="dialogState.message"
></p>
<p v-else class="text-p-base text-gray-700">
{{ dialogState.message }}
</p>
</template>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

XSS via v-html when callers embed user-controlled strings.

v-html="dialogState.message" will execute any HTML/JS in the message. In this PR, editForm.ts builds the message by string-concatenating field.label and field.fieldname (both author-controlled) into <b>…</b>, so a label like <img src=x onerror=alert(1)> becomes executable script when the duplicate dialog renders. Since this is a shared global dialog, every future html: true caller inherits the same risk.

Recommended options (any one):

  • Drop html mode entirely and let callers pass structured content (e.g., a messageLines: string[] rendered as plain <li> interpolations), or expose a slot for custom content instead of raw HTML.
  • If html is kept, require callers to pass already-sanitized HTML (e.g., via DOMPurify) and document this contract; sanitize defensively here as well.

At minimum, the editForm.ts call site needs to escape label/fieldname before interpolation — see related comment there.

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

In `@frontend/src/components/ui/GlobalDialog.vue` around lines 14 - 23, The
GlobalDialog.vue currently uses v-html="dialogState.message" which allows XSS
when callers (e.g., editForm.ts) pass HTML built from user-controlled values;
change GlobalDialog.vue to stop rendering raw HTML by removing v-html and
instead render dialogState.message as plain text (use the existing interpolation
branch) or provide a dedicated slot/prop (e.g., default slot or messageSlot) for
callers to supply safe rich content; alternatively, if keeping HTML mode,
require and validate sanitized HTML by calling a sanitizer (e.g., DOMPurify)
inside the GlobalDialog component before assigning to dialogState.message and
update editForm.ts to either escape label/fieldname or perform sanitization
there as well so that dialogState.html cannot be set with unsanitized user
input.

Comment on lines +125 to 132
const fieldList = duplicates
.map((d) => `<b>${d.label} (${d.fieldname})</b>`)
.join(", ");
await dialog.alert({
title: "Duplicate Fieldnames",
message: `These fields will have duplicate fieldnames: ${fieldList}. Please change one of the labels or set a unique fieldname.`,
html: true,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Unescaped user input interpolated into HTML message — XSS.

d.label and d.fieldname come straight from formResource.value.doc.fields (form-builder authored) and are concatenated into fieldList which is then sent with html: true. Combined with v-html in GlobalDialog.vue, a label such as <img src=x onerror=alert(1)> will execute when the duplicate dialog opens.

Either drop html: true and present the duplicate list as structured plain text, or escape the interpolated values before building the HTML.

🛡️ Proposed fix (no HTML, plain message)
-      const fieldList = duplicates
-        .map((d) => `<b>${d.label} (${d.fieldname})</b>`)
-        .join(", ");
-      await dialog.alert({
-        title: "Duplicate Fieldnames",
-        message: `These fields will have duplicate fieldnames: ${fieldList}. Please change one of the labels or set a unique fieldname.`,
-        html: true,
-      });
+      const fieldList = duplicates
+        .map((d) => `${d.label} (${d.fieldname})`)
+        .join(", ");
+      await dialog.alert({
+        title: "Duplicate Fieldnames",
+        message: `These fields will have duplicate fieldnames: ${fieldList}. Please change one of the labels or set a unique fieldname.`,
+      });

If bold formatting is desired, sanitize/escape first (e.g., a small escapeHtml helper) before wrapping in <b>.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const fieldList = duplicates
.map((d) => `<b>${d.label} (${d.fieldname})</b>`)
.join(", ");
await dialog.alert({
title: "Duplicate Fieldnames",
message: `These fields will have duplicate fieldnames: ${fieldList}. Please change one of the labels or set a unique fieldname.`,
html: true,
});
const fieldList = duplicates
.map((d) => `${d.label} (${d.fieldname})`)
.join(", ");
await dialog.alert({
title: "Duplicate Fieldnames",
message: `These fields will have duplicate fieldnames: ${fieldList}. Please change one of the labels or set a unique fieldname.`,
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/stores/editForm.ts` around lines 125 - 132, The code builds an
HTML string from untrusted values (duplicates → d.label and d.fieldname from
formResource.value.doc.fields) and passes it to dialog.alert with html: true,
enabling XSS; fix by removing HTML output: construct a plain-text message (e.g.,
join duplicates.map(d => `${d.label} (${d.fieldname})`) with newlines or commas)
and call dialog.alert without the html: true option so the GlobalDialog.vue
v-html path is not used; if you must keep formatting, instead escape d.label and
d.fieldname with a small escapeHtml helper before wrapping in <b> and keep html:
true.

Comment on lines +35 to +89
function closeDialog(result: boolean = false) {
dialogState.open = false;
if (dialogState.resolve) {
dialogState.resolve(result);
dialogState.resolve = null;
}
}

export const dialog = {
show(options: DialogOptions): Promise<boolean> {
return new Promise((resolve) => {
dialogState.title = options.title;
dialogState.message = options.message;
dialogState.html = options.html || false;
dialogState.actions = options.actions || [
{ label: "OK", variant: "solid" },
];
dialogState.resolve = resolve;
dialogState.open = true;
});
},

confirm(options: Omit<DialogOptions, "actions">): Promise<boolean> {
return this.show({
...options,
actions: [
{
label: "Cancel",
variant: "outline",
onClick: () => closeDialog(false),
},
{
label: "Confirm",
variant: "solid",
onClick: () => closeDialog(true),
},
],
});
},

alert(options: Omit<DialogOptions, "actions">): Promise<boolean> {
return this.show({
...options,
actions: [
{
label: "OK",
variant: "solid",
onClick: () => closeDialog(true),
},
],
});
},

close: closeDialog,
};

Copy link
Copy Markdown

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

🌐 Web query:

frappe-ui Dialog component v-model close behavior backdrop ESC action onClick

💡 Result:

The frappe-ui Dialog component uses v-model (modelValue prop) to control its open/closed state. When the dialog closes via backdrop click, ESC key, or other standard modal close actions (built on reka-ui/Headless UI primitives), it emits 'update:modelValue' with false, syncing the v-model to false, and also emits a 'close' event. To prevent closing on backdrop click (outside click), set the disableOutsideClickToClose prop to true. This stops closure on outside clicks but does not explicitly mention affecting ESC in docs; standard behavior (from source using DialogOverlay/DialogContent) likely still closes on ESC unless further customized. onClick handlers are in options.actions array; each action receives a context with close function to manually close, or use emit 'close'. Actions can be primary or secondary. ESC and backdrop trigger standard close: modelValue -> false via update:modelValue emit. Official docs: https://ui.frappe.io/docs/components/dialog (props: modelValue, disableOutsideClickToClose; emits: update:modelValue, close). Source confirms: uses reka-ui Dialog components; isOpen setter emits update:modelValue(false) and close on close; actions use reactive onClick with close fn.

Citations:


🏁 Script executed:

# First, let's read the complete dialog.ts file to understand the context
cat -n frontend/src/utils/dialog.ts | head -100

Repository: BuildWithHussain/forms_pro

Length of output: 1893


🏁 Script executed:

# Find the Vue component that uses this dialog utility
rg -l "import.*dialog|from.*dialog" frontend/src --type vue --type ts

Repository: BuildWithHussain/forms_pro

Length of output: 98


🏁 Script executed:

# Check how the dialog is used in components - look for dialogState binding
rg "dialogState|dialog\.show|dialog\.confirm|dialog\.alert" frontend/src --type vue -A 2 -B 2

Repository: BuildWithHussain/forms_pro

Length of output: 98


Promise leaks unresolved when the dialog closes outside an explicit action.

Two concrete paths leave dialogState.resolve populated and the returned Promise<boolean> permanently pending:

  1. show() synthesizes a default OK action { label: "OK", variant: "solid" } with no onClick handler (lines 49–51). Clicking it does not invoke closeDialog, so dialog.show() consumers never see their promise settle. confirm() and alert() only work because they always supply explicit onClick handlers.

  2. Any v-model–driven close (backdrop click, ESC, close icon) flips dialogState.open to false without going through closeDialog, so dialogState.resolve is retained and will fire for the next dialog's close—resolving the stale promise with the new result.

Both can be fixed by routing all close paths through closeDialog via a watcher and giving the default action a proper handler.

🛡️ Proposed fix
-import { reactive } from "vue";
+import { reactive, watch } from "vue";
@@
-function closeDialog(result: boolean = false) {
+function closeDialog(result: boolean = false) {
   dialogState.open = false;
   if (dialogState.resolve) {
     dialogState.resolve(result);
     dialogState.resolve = null;
   }
 }
+
+// Resolve the pending promise if the dialog is dismissed via v-model
+// (backdrop click, ESC, close icon) without an explicit action handler.
+watch(
+  () => dialogState.open,
+  (open) => {
+    if (!open && dialogState.resolve) {
+      dialogState.resolve(false);
+      dialogState.resolve = null;
+    }
+  }
+);
@@
-      dialogState.actions = options.actions || [
-        { label: "OK", variant: "solid" },
-      ];
+      dialogState.actions = options.actions || [
+        { label: "OK", variant: "solid", onClick: () => closeDialog(true) },
+      ];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/utils/dialog.ts` around lines 35 - 89, The promise leak happens
because the default action in dialog.show has no onClick and v-model closes
bypassing closeDialog, leaving dialogState.resolve set; fix by (1) ensuring the
default action created in dialog.show includes an onClick that calls
closeDialog(true) so clicking OK resolves the promise, and (2) adding a
watcher/observer on dialogState.open that detects when it transitions to false
and, if dialogState.resolve exists, calls closeDialog(false) to resolve and
clear the pending promise; reference dialog.show, dialogState, closeDialog,
confirm and alert when making these changes.

Merged via the queue into develop with commit 9e7849d Apr 27, 2026
8 checks passed
@harshtandiya harshtandiya deleted the fix/fieldnames branch April 27, 2026 13:23
harshtandiya added a commit that referenced this pull request Apr 29, 2026
…114)

* fix(multiselect): reset option input and error message on startAddingOption

(cherry picked from commit 96a0f94)

* chore: better ui to remove options in multiselect (#87)

* chore: make labels w-full in builder

* chore(multiselect): better ui for removing options

- Introduced `inEditMode` prop to `RenderField.vue` to control edit state.
- Updated `FieldRenderer.vue` to pass `inEditMode` to `RenderField`.
- Enhanced `Multiselect.vue` to utilize `inEditMode` for conditional rendering and option removal functionality.

(cherry picked from commit 55c4fc0)

* chore(deps-dev): bump postcss from 8.5.8 to 8.5.10 in /frontend (#95)

Bumps [postcss](https://github.com/postcss/postcss) from 8.5.8 to 8.5.10.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](postcss/postcss@8.5.8...8.5.10)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit 7a3aa8c)

* chore(deps): bump dayjs from 1.11.19 to 1.11.20 in /frontend (#84)

Bumps [dayjs](https://github.com/iamkun/dayjs) from 1.11.19 to 1.11.20.
- [Release notes](https://github.com/iamkun/dayjs/releases)
- [Changelog](https://github.com/iamkun/dayjs/blob/dev/CHANGELOG.md)
- [Commits](iamkun/dayjs@v1.11.19...v1.11.20)

---
updated-dependencies:
- dependency-name: dayjs
  dependency-version: 1.11.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit 222c73d)

* chore(deps): bump @lottiefiles/dotlottie-vue in /frontend (#93)

Bumps [@lottiefiles/dotlottie-vue](https://github.com/LottieFiles/dotlottie-web/tree/HEAD/packages/vue) from 0.10.4 to 0.11.11.
- [Release notes](https://github.com/LottieFiles/dotlottie-web/releases)
- [Changelog](https://github.com/LottieFiles/dotlottie-web/blob/main/packages/vue/CHANGELOG.md)
- [Commits](https://github.com/LottieFiles/dotlottie-web/commits/@lottiefiles/dotlottie-vue@0.11.11/packages/vue)

---
updated-dependencies:
- dependency-name: "@lottiefiles/dotlottie-vue"
  dependency-version: 0.11.11
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit 092d2e8)

* chore(deps): bump actions/setup-node from 4 to 6 (#88)

Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](actions/setup-node@v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit 9aa693f)

* chore(deps): bump actions/checkout from 4 to 6 (#90)

Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit d147e0d)

* chore(deps): bump actions/setup-python from 5 to 6 (#91)

Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](actions/setup-python@v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit 00a219c)

* chore(deps): bump actions/upload-artifact from 4 to 7 (#89)

Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](actions/upload-artifact@v4...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit e95b934)

* ci: add merge_group trigger to enable GitHub merge queue (#97)

(cherry picked from commit cd63fa7)

* fix(e2e): auto-fill form title in FormBuilderPage.goto() to prevent MandatoryError (#96)

Forms created via e2e fixtures have title "Untitled Form" which the frontend
transforms to "" on load. When tests modify the form and save, the blank title
causes a MandatoryError. This was causing CI failures in multiselect-field.spec.ts.

Changes:
- FormBuilderPage.goto() now auto-fills a unique title and saves
- Added skipTitleFill option for forms that already have a title set
- createPublishedForm fixture now sets title alongside is_published
- Removed manual title fill from multiselect-field.spec.ts

(cherry picked from commit 26e2a74)

* chore: update gitignore to ignore semgrep folder

(cherry picked from commit a6f6b2a)

* fix: resolve semgrep findings across codebase (#98)

* fix: resolve semgrep findings across codebase

- Add nosemgrep comments for reviewed guest-whitelisted endpoints
- Fix format string injection by converting exceptions to str()
- Remove redundant db.commit() in test teardown (DDL auto-commits)
- Add explanation comment for required CSRF token commit

* ci: remove paths from pull_request trigger in typecheck workflow

- Simplified the typecheck workflow by removing specific paths from the pull_request trigger, allowing for broader checks on all changes.

* fix: use correct semgrep rule ID prefix (frappe-semgrep-rules)

(cherry picked from commit d5cde23)

* feat: add Heading 1/2/3 field types (#103)

* feat(form-field): add Heading 1/2/3 fieldtypes to doctype and backend mapping

Maps heading fieldtypes to Frappe HTML, generates h1/h2/h3 options content
for the CustomField, and skips heading fields in server-side required validation.

* feat(heading): wire up Heading 1/2/3 field types across the frontend

Adds heading layout type, Heading component with h2/h3/h4 tag rendering,
FieldRenderer branch for edit/view modes, isHeading util, and submission
display handling.

* test(heading): add backend and E2E tests for heading field types

- Unit tests for heading fields skipped in validation
- Integration tests for get_options() and to_frappe_field
- E2E tests for builder, public form rendering, and submission
- Fix missing Heading imports in FieldRenderer and SubmissionFieldValue

* fix(form-field): escape HTML in heading labels for get_options method

- Updated get_options method to use escape_html for heading labels to prevent potential HTML injection.
- Adjusted return type annotation to allow for None in addition to str.

* chore: minor styling

(cherry picked from commit 16989b1)

* refactor: redesign the form builder layout (#105)

* refactor: redesign the form builder layout

- Introduced a new FieldActions component to handle field removal and drag functionality.
- Integrated FieldActions into FormBuilderContent for improved user interaction with form fields.
- Updated styles for better visibility and interaction feedback.

* feat: enhance FieldActions component for improved drag-and-drop functionality

- Updated FieldActions to include drag state handling, allowing for better user feedback during field manipulation.
- Integrated the new FieldActions component into FormBuilderContent, enhancing the interaction experience with form fields.
- Adjusted styles for visibility based on selection and drag state.

(cherry picked from commit 4e9b9d1)

* enhance(FieldActions): add tooltips for field actions buttons

- Added tooltip text for the remove and drag buttons in the FieldActions component to improve user experience and accessibility.
- Updated button structure for better readability and maintainability.

(cherry picked from commit 66e04de)

* fix(Form): correct initial route generation string (#106)

- Updated the initial route generation method to remove the unnecessary 's/' prefix, ensuring the route is generated correctly as 'forms_pro_' followed by a random string.

(cherry picked from commit 72f63f8)

* fix: prevent duplicate fieldnames on form save (#107)

* feat(frontend): add global dialog component for imperative dialogs

Adds dialog utility with confirm/alert/show methods callable from
anywhere via TypeScript, similar to vue-sonner pattern.

* feat(dialog): add html support for rich message content

* fix(editForm): prevent save when duplicate fieldnames detected

Shows dialog listing conflicting Label(fieldname) pairs before save.

(cherry picked from commit 9e7849d)

* chore(deps): bump vue from 3.5.32 to 3.5.33 in /frontend (#110)

Bumps [vue](https://github.com/vuejs/core) from 3.5.32 to 3.5.33.
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](vuejs/core@v3.5.32...v3.5.33)

---
updated-dependencies:
- dependency-name: vue
  dependency-version: 3.5.33
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit 0ffdad3)

* chore(deps-dev): bump @vitejs/plugin-vue in /frontend (#109)

Bumps [@vitejs/plugin-vue](https://github.com/vitejs/vite-plugin-vue/tree/HEAD/packages/plugin-vue) from 6.0.5 to 6.0.6.
- [Release notes](https://github.com/vitejs/vite-plugin-vue/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-vue/blob/main/packages/plugin-vue/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-vue/commits/plugin-vue@6.0.6/packages/plugin-vue)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-vue"
  dependency-version: 6.0.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit 9035fdb)

* fix(backport): adapt cherry-picked code to v15 lucide + dual-enum types

- FieldActions.vue: use lucide-vue-next (v15 hasn't migrated to @lucide/vue)
- SubmissionFieldValue.vue: cast FormFieldTypes prop to Fieldtype where the
  Heading helper / component expect the doctype-generated enum (refactor #75
  consolidated these enums on develop; v15 still has both)

* fix(test): use v15-compatible FrappeTestCase import in test_form_field

frappe.tests.IntegrationTestCase doesn't exist on Frappe v15. Match the
import pattern used by the other v15 tests (test_roles, test_invitations).

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: prevent duplicate fieldnames when adding fields with the same label

1 participant