fix: prevent duplicate fieldnames on form save#107
Conversation
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.
📝 WalkthroughWalkthroughThis 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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 usingseen.setafter the check preserves the original field forexisting.label. One small defensive nit:scrubFieldname(field.label)will throw on anull/undefinedlabel; todayaddFieldinitializeslabel: "", but consider guarding withfield.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
📒 Files selected for processing (4)
frontend/src/App.vuefrontend/src/components/ui/GlobalDialog.vuefrontend/src/stores/editForm.tsfrontend/src/utils/dialog.ts
| <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> |
There was a problem hiding this comment.
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
htmlmode entirely and let callers pass structured content (e.g., amessageLines: string[]rendered as plain<li>interpolations), or expose a slot for custom content instead of raw HTML. - If
htmlis 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.
| 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, | ||
| }); |
There was a problem hiding this comment.
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.
| 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.
| 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, | ||
| }; |
There was a problem hiding this comment.
🧩 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:
- 1: https://ui.frappe.io/docs/components/dialog
- 2: https://cdn.jsdelivr.net/npm/frappe-ui@0.1.248/src/components/Dialog/Dialog.vue
🏁 Script executed:
# First, let's read the complete dialog.ts file to understand the context
cat -n frontend/src/utils/dialog.ts | head -100Repository: 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 tsRepository: 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 2Repository: 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:
-
show()synthesizes a default OK action{ label: "OK", variant: "solid" }with noonClickhandler (lines 49–51). Clicking it does not invokecloseDialog, sodialog.show()consumers never see their promise settle.confirm()andalert()only work because they always supply explicitonClickhandlers. -
Any v-model–driven close (backdrop click, ESC, close icon) flips
dialogState.opentofalsewithout going throughcloseDialog, sodialogState.resolveis 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.
…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>
Summary
Closes #78
Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit