feat: child table support v1#42
Conversation
- Updated form_field.json and form_field.py to include 'Table' in the list of selectable field types for enhanced functionality. - Adjusted the modified timestamp in form_field.json to reflect recent changes.
- Added a new Table component for rendering tabular data input. - Updated RenderField and FieldRenderer components to support dynamic options for Select and Link fields. - Introduced useFieldOptions composable for improved option management. - Enhanced form_fields utility to include Table as a selectable field type. - Updated auto-imports and TypeScript definitions to accommodate new functionality.
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📝 WalkthroughWalkthroughAdds a new "Table" form field: backend type declarations updated, frontend type and registry extended, a Table field component implemented, and a composable for loading select/link options integrated into field rendering. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant TableComponent as Table.vue
participant FieldRenderer
participant OptionsComposable as useFieldOptions()
participant BackendAPI as /api/v2/method/...
User->>TableComponent: click "Add Row" / edit cell
TableComponent->>FieldRenderer: render per-column FieldRenderer with model-value
FieldRenderer->>OptionsComposable: request options for field (fieldtype, options)
OptionsComposable->>BackendAPI: fetch link/select options (if Link) or return parsed options
BackendAPI-->>OptionsComposable: options data
OptionsComposable-->>FieldRenderer: resolved options
FieldRenderer-->>TableComponent: emit updated cell value
TableComponent-->>User: updated rows displayed
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 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: 7
🧹 Nitpick comments (3)
frontend/src/components/fields/Table.vue (1)
63-82: Index-based:keycauses incorrect diffing when rows are removed.Using
:key="index"means Vue reuses DOM nodes based on position after a removal — e.g., removing row 0 visually shifts every remaining row's content while keeping the component instance in place. A stable key prevents this:♻️ Proposed fix
-const rows = defineModel<Row[]>({ default: [] }); +type Row = { _id?: string; [key: string]: any }; + +function addRow() { + ... + const newRow = { _id: crypto.randomUUID(), ...columns... }; + ... +}Then in template:
- v-for="(_, index) in rows" - :key="index" + v-for="(row, index) in rows" + :key="row._id ?? index"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/fields/Table.vue` around lines 63 - 82, The template uses :key="index" in the v-for over rows which causes Vue to reuse DOM nodes incorrectly when a row is removed; change the key to a stable identifier (e.g., :key="row.id" or :key="rows[index].id") on the outer div and each FieldRenderer iteration, and ensure every row object is assigned a unique, persistent id when created (update the row-creation logic such as addRow/initialization to generate an id); keep removeRow and updateCell behavior unchanged but rely on the stable id for correct diffing.frontend/src/components/RenderField.vue (1)
22-24:useFieldOptionsruns for every field regardless of type, causing unnecessary API calls.For non-Select/non-Link fields,
getFieldOptionsstill resolves (returning""or the rawoptionsstring), triggering a reactive cycle on everyfieldtype/optionschange. Consider gating the load inside the composable — or here before instantiating it — so Link fields are the only ones that make network requests.-const fieldRef = computed(() => props.field); -const { options: loadedOptions } = useFieldOptions(fieldRef); +const fieldRef = computed(() => props.field); +const fieldtype = computed(() => props.field?.fieldtype); +const { options: loadedOptions } = useFieldOptions(fieldRef); +// NOTE: useFieldOptions should short-circuit for non-Select/Link types(The real fix belongs in
useFieldOptions— see theselectOptions.tscomment.)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/RenderField.vue` around lines 22 - 24, The current code always calls useFieldOptions(fieldRef), causing unnecessary getFieldOptions runs for non-Link/Select fields; change it to only instantiate/use useFieldOptions when the field type requires remote loading (e.g., when fieldRef.value.fieldtype === 'Link' or 'Select'). Keep fieldRef as-is, ensure loadedOptions defaults to an empty/ref value when the composable is not used, and update resolvedOptions (the computed using props.options ?? loadedOptions.value) to work with that default so non-Link/Select fields avoid network requests.frontend/src/utils/selectOptions.ts (1)
31-44: No error handling onresource.fetch()and a newcreateResourceis allocated on every call.If the API call fails (network error, permission denied), the rejection propagates uncaught through
load()into thewatchcallback, producing an unhandled promise rejection. Additionally, a freshcreateResourceinstance is created on everyload()invocation with no cleanup.♻️ Proposed fix
if (field.fieldtype === "Link") { const resource = createResource({ url: "forms_pro.api.form.get_link_field_options", makeParams: () => ({ doctype: field.options, filters: {}, page_length: 999, }), }); - await resource.fetch(); - return resource.data as string[] | SelectOption[]; + try { + await resource.fetch(); + return resource.data as string[] | SelectOption[]; + } catch { + return ""; + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/utils/selectOptions.ts` around lines 31 - 44, Wrap the call to resource.fetch() in a try/catch inside the code path that handles field.fieldtype === "Link" so any network/permission error is caught and you return a safe fallback (e.g., field.options or an empty array) instead of letting the rejection bubble into load()/watch(); also stop allocating a new createResource() on every invocation by caching the resource (e.g., a Map keyed by field.options) or by hoisting the createResource call out of the load/watch so you reuse the same resource instance for the same link field; if the resource API exposes a dispose/abort/unsubscribe method, call it when replacing/clearing the cache to avoid leaks.
🤖 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/builder/FieldRenderer.vue`:
- Around line 164-171: The <small> for fieldData.description should be
conditional like the Checkbox branch and the Table prop :doctype must never be
undefined; update FieldRenderer.vue to render the description only when
fieldData.description is truthy (use the same v-if guard as the Checkbox branch)
and pass a safe doctype to the Table component (e.g., compute a fallback or pass
null/empty string when fieldData.options is falsy) so Table does not receive
doctype=undefined; ensure references are to fieldData.description and the Table
component props (v-model, :in-edit-mode, :doctype) so you modify the correct
elements.
- Line 168: FieldRenderer.vue is silencing a type mismatch by using
v-model="modelValue as undefined"; instead give the component a proper typed
model value: update the defineModel usage in FieldRenderer (replace the untyped
defineModel/unknown modelValue) to the correct generic type (e.g.,
defineModel<Row[]>() or declare a properly typed prop/computed wrapper) so
v-model binds a typed modelValue rather than casting to undefined; ensure the
symbol names involved are FieldRenderer.vue, defineModel, modelValue and that
Table.vue's defineModel<Row[]>() type is respected by this component (or
introduce a typed intermediate like modelValueTyped: Ref<Row[]> and use
v-model="modelValueTyped").
In `@frontend/src/components/fields/Table.vue`:
- Around line 39-45: addRow currently builds a newRow from columns.value which
can be empty if columnResource hasn't loaded, producing keyless rows; update
addRow to early-return (or disable action) when columns.value is empty (e.g., if
(!columns.value || columns.value.length === 0) return) so it never appends an
empty object to rows.value, ensuring newRow creation uses actual column keys;
reference the addRow function, columns.value, rows.value and the Row type (and
FieldRenderer rendering) when applying the guard.
- Around line 27-37: The computed columns builder spreads raw doctype column
data into the column objects, which leaves Frappe-native fieldtypes (e.g.,
"Int", "Datetime") in the props and causes FieldRenderer to fail because
formFields expects forms_pro types; update the columns mapping in the columns
computed to translate each column.fieldtype using the existing
mapDoctypeFieldForForm function (from form_fields.ts) before returning the
column object so FieldRenderer receives a mapped fieldtype that matches
formFields (ensure you import mapDoctypeFieldForForm and apply it to
column.fieldtype when constructing the mapped column).
In `@frontend/src/utils/form_fields.ts`:
- Around line 148-158: TableField currently maps to the read-only ListView with
only an emptyState, which breaks form-fill rendering paths; update the
TableField definition so it either points to the interactive table component
(Table.vue) instead of ListView or provides a self-contained editable table stub
that supplies required props (columns, rows, rowKey) and data-binding handlers
used by RenderField/FieldRenderer.vue; remove placeholder emptyState text and
ensure the TableField exposes the same API as Table.vue (props/events) so direct
RenderField usage renders the editable table rather than a perpetual empty
state.
In `@frontend/src/utils/selectOptions.ts`:
- Around line 56-62: In load() inside selectOptions.ts, avoid casting non-array
results from getFieldOptions(field.value) into string[] | SelectOption[]; change
the assignment to only set options.value when Array.isArray(result) (and not
empty string/undefined), otherwise set options.value to null—i.e. use
Array.isArray(result) to guard the assignment in the load function so downstream
code that iterates options.value always receives an actual array.
- Around line 64-72: The watch call already invokes load() immediately ({
immediate: true }), so remove the redundant onMounted(() => load()) call and
drop onMounted from the imports to avoid duplicate API requests; update the code
around watch(...) and the import list (referencing watch, load, onMounted, and
field in selectOptions.ts) so only the watch with immediate remains.
---
Nitpick comments:
In `@frontend/src/components/fields/Table.vue`:
- Around line 63-82: The template uses :key="index" in the v-for over rows which
causes Vue to reuse DOM nodes incorrectly when a row is removed; change the key
to a stable identifier (e.g., :key="row.id" or :key="rows[index].id") on the
outer div and each FieldRenderer iteration, and ensure every row object is
assigned a unique, persistent id when created (update the row-creation logic
such as addRow/initialization to generate an id); keep removeRow and updateCell
behavior unchanged but rely on the stable id for correct diffing.
In `@frontend/src/components/RenderField.vue`:
- Around line 22-24: The current code always calls useFieldOptions(fieldRef),
causing unnecessary getFieldOptions runs for non-Link/Select fields; change it
to only instantiate/use useFieldOptions when the field type requires remote
loading (e.g., when fieldRef.value.fieldtype === 'Link' or 'Select'). Keep
fieldRef as-is, ensure loadedOptions defaults to an empty/ref value when the
composable is not used, and update resolvedOptions (the computed using
props.options ?? loadedOptions.value) to work with that default so
non-Link/Select fields avoid network requests.
In `@frontend/src/utils/selectOptions.ts`:
- Around line 31-44: Wrap the call to resource.fetch() in a try/catch inside the
code path that handles field.fieldtype === "Link" so any network/permission
error is caught and you return a safe fallback (e.g., field.options or an empty
array) instead of letting the rejection bubble into load()/watch(); also stop
allocating a new createResource() on every invocation by caching the resource
(e.g., a Map keyed by field.options) or by hoisting the createResource call out
of the load/watch so you reuse the same resource instance for the same link
field; if the resource API exposes a dispose/abort/unsubscribe method, call it
when replacing/clearing the cache to avoid leaks.
| <small class="text-gray-500"> | ||
| {{ fieldData.description }} | ||
| </small> | ||
| <Table | ||
| v-model="modelValue as undefined" | ||
| :in-edit-mode="inEditMode" | ||
| :doctype="fieldData.options" | ||
| /> |
There was a problem hiding this comment.
Two small issues in the Table branch.
-
Missing description guard (line 164): The
<small>forfieldData.descriptionrenders an empty element when description is absent, unlike the Checkbox branch (line 99) which hasv-if="fieldData.description". -
Undefined doctype (line 170): If the form field was saved without setting
options(the child doctype),fieldData.optionsisundefined, andTable.vuewill fire an API call fordoctype=undefined.
♻️ Proposed fix
- <small class="text-gray-500">
- {{ fieldData.description }}
- </small>
+ <small v-if="fieldData.description" class="text-gray-500">
+ {{ fieldData.description }}
+ </small>
<Table
v-model="modelValue as undefined"
:in-edit-mode="inEditMode"
- :doctype="fieldData.options"
+ :doctype="fieldData.options || ''"
/>📝 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.
| <small class="text-gray-500"> | |
| {{ fieldData.description }} | |
| </small> | |
| <Table | |
| v-model="modelValue as undefined" | |
| :in-edit-mode="inEditMode" | |
| :doctype="fieldData.options" | |
| /> | |
| <small v-if="fieldData.description" class="text-gray-500"> | |
| {{ fieldData.description }} | |
| </small> | |
| <Table | |
| v-model="modelValue as undefined" | |
| :in-edit-mode="inEditMode" | |
| :doctype="fieldData.options || ''" | |
| /> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/components/builder/FieldRenderer.vue` around lines 164 - 171,
The <small> for fieldData.description should be conditional like the Checkbox
branch and the Table prop :doctype must never be undefined; update
FieldRenderer.vue to render the description only when fieldData.description is
truthy (use the same v-if guard as the Checkbox branch) and pass a safe doctype
to the Table component (e.g., compute a fallback or pass null/empty string when
fieldData.options is falsy) so Table does not receive doctype=undefined; ensure
references are to fieldData.description and the Table component props (v-model,
:in-edit-mode, :doctype) so you modify the correct elements.
| {{ fieldData.description }} | ||
| </small> | ||
| <Table | ||
| v-model="modelValue as undefined" |
There was a problem hiding this comment.
v-model="modelValue as undefined" silences a legitimate type mismatch.
defineModel() in FieldRenderer is untyped (unknown), while Table.vue declares defineModel<Row[]>(). Cast-to-undefined is a no-op at runtime but hides the real fix: either type modelValue as Row[] in this component or use a properly typed intermediate.
♻️ Proposed fix
-const modelValue = defineModel();
+const modelValue = defineModel<Row[]>();Then in the template:
-<Table v-model="modelValue as undefined" .../>
+<Table v-model="modelValue" .../>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/components/builder/FieldRenderer.vue` at line 168,
FieldRenderer.vue is silencing a type mismatch by using v-model="modelValue as
undefined"; instead give the component a proper typed model value: update the
defineModel usage in FieldRenderer (replace the untyped defineModel/unknown
modelValue) to the correct generic type (e.g., defineModel<Row[]>() or declare a
properly typed prop/computed wrapper) so v-model binds a typed modelValue rather
than casting to undefined; ensure the symbol names involved are
FieldRenderer.vue, defineModel, modelValue and that Table.vue's
defineModel<Row[]>() type is respected by this component (or introduce a typed
intermediate like modelValueTyped: Ref<Row[]> and use
v-model="modelValueTyped").
| function addRow() { | ||
| const newRow = columns.value.reduce((acc, column) => { | ||
| acc[column.key] = null; | ||
| return acc; | ||
| }, {} as Row); | ||
| rows.value = [...(rows.value ?? []), newRow]; | ||
| } |
There was a problem hiding this comment.
addRow when columns haven't loaded yet produces empty, keyless rows.
If a user clicks "Add Row" before the columnResource finishes, columns.value is [], so newRow is {} — a row with no keys. Those rows then render zero FieldRenderer instances but still appear in the list. Guard the action:
♻️ Proposed fix
function addRow() {
+ if (!columns.value.length) return;
const newRow = columns.value.reduce((acc, column) => {
acc[column.key] = null;
return acc;
}, {} as Row);
rows.value = [...(rows.value ?? []), newRow];
}📝 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.
| function addRow() { | |
| const newRow = columns.value.reduce((acc, column) => { | |
| acc[column.key] = null; | |
| return acc; | |
| }, {} as Row); | |
| rows.value = [...(rows.value ?? []), newRow]; | |
| } | |
| function addRow() { | |
| if (!columns.value.length) return; | |
| const newRow = columns.value.reduce((acc, column) => { | |
| acc[column.key] = null; | |
| return acc; | |
| }, {} as Row); | |
| rows.value = [...(rows.value ?? []), newRow]; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/components/fields/Table.vue` around lines 39 - 45, addRow
currently builds a newRow from columns.value which can be empty if
columnResource hasn't loaded, producing keyless rows; update addRow to
early-return (or disable action) when columns.value is empty (e.g., if
(!columns.value || columns.value.length === 0) return) so it never appends an
empty object to rows.value, ensuring newRow creation uses actual column keys;
reference the addRow function, columns.value, rows.value and the Row type (and
FieldRenderer rendering) when applying the guard.
| export const TableField: FormFieldType = { | ||
| component: ListView, | ||
| props: { | ||
| options: { | ||
| emptyState: { | ||
| title: "This is a table field", | ||
| description: "Use this field to input a list of items.", | ||
| }, | ||
| }, | ||
| }, | ||
| }; |
There was a problem hiding this comment.
TableField mapping to ListView is a non-functional placeholder that breaks form-fill rendering paths.
ListView is a read-only list display component — it requires at minimum columns, rows, and rowKey to render anything useful. With only an emptyState object supplied, it will perpetually render the empty state with no data binding.
FieldRenderer.vue avoids this by short-circuiting to Table.vue in its dedicated v-else-if branch, but any call path that uses RenderField directly (e.g. standalone form-fill / submission views) with a Table-typed field will hit this mapping and produce a broken UI. The emptyState strings also look like placeholder text that shouldn't reach production.
Replace this with a proper self-contained table-input stub or point it at Table.vue to keep both rendering paths consistent:
♻️ Proposed fix
+import TableComponent from "@/components/fields/Table.vue";
-export const TableField: FormFieldType = {
- component: ListView,
- props: {
- options: {
- emptyState: {
- title: "This is a table field",
- description: "Use this field to input a list of items.",
- },
- },
- },
-};
+export const TableField: FormFieldType = {
+ component: TableComponent,
+ props: {},
+};📝 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.
| export const TableField: FormFieldType = { | |
| component: ListView, | |
| props: { | |
| options: { | |
| emptyState: { | |
| title: "This is a table field", | |
| description: "Use this field to input a list of items.", | |
| }, | |
| }, | |
| }, | |
| }; | |
| import TableComponent from "@/components/fields/Table.vue"; | |
| export const TableField: FormFieldType = { | |
| component: TableComponent, | |
| props: {}, | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/utils/form_fields.ts` around lines 148 - 158, TableField
currently maps to the read-only ListView with only an emptyState, which breaks
form-fill rendering paths; update the TableField definition so it either points
to the interactive table component (Table.vue) instead of ListView or provides a
self-contained editable table stub that supplies required props (columns, rows,
rowKey) and data-binding handlers used by RenderField/FieldRenderer.vue; remove
placeholder emptyState text and ensure the TableField exposes the same API as
Table.vue (props/events) so direct RenderField usage renders the editable table
rather than a perpetual empty state.
| const load = async () => { | ||
| const result = await getFieldOptions(field.value); | ||
| options.value = | ||
| result === "" || result === undefined | ||
| ? null | ||
| : (result as string[] | SelectOption[]); | ||
| }; |
There was a problem hiding this comment.
Non-array result (raw string) is incorrectly cast to string[] | SelectOption[].
When fieldtype is neither "Select" nor "Link" but options is non-empty (e.g. a "Data" field with options: "Email"), getFieldOptions returns the raw string "Email". The guard result === "" is false, so useFieldOptions assigns "Email" as string[] | SelectOption[]. Any downstream component that iterates options.value would receive individual characters.
Restrict the assignment to actual array results:
♻️ Proposed fix
const load = async () => {
const result = await getFieldOptions(field.value);
options.value =
- result === "" || result === undefined
+ !result || !Array.isArray(result)
? null
: (result as string[] | SelectOption[]);
};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/utils/selectOptions.ts` around lines 56 - 62, In load() inside
selectOptions.ts, avoid casting non-array results from
getFieldOptions(field.value) into string[] | SelectOption[]; change the
assignment to only set options.value when Array.isArray(result) (and not empty
string/undefined), otherwise set options.value to null—i.e. use
Array.isArray(result) to guard the assignment in the load function so downstream
code that iterates options.value always receives an actual array.
- Integrated utility function to map fieldtype for Table component, ensuring proper handling of field types. - Defaulted unmapped fieldtypes to "Data" for improved consistency in table rendering.
- Replaced ListView with Table in the TableField definition to ensure proper rendering of tabular data. - Adjusted imports in form_fields utility to reflect the change in component usage.
- Eliminated the onMounted lifecycle hook from the useFieldOptions composable to streamline the loading process of options. - Adjusted imports to reflect the removal of the unused onMounted function.
* chore: bump frappe-ui to `0.1.262` * feat: add 'Table' option to fieldtype selection - Updated form_field.json and form_field.py to include 'Table' in the list of selectable field types for enhanced functionality. - Adjusted the modified timestamp in form_field.json to reflect recent changes. * feat: implement Table field component and enhance field options handling - Added a new Table component for rendering tabular data input. - Updated RenderField and FieldRenderer components to support dynamic options for Select and Link fields. - Introduced useFieldOptions composable for improved option management. - Enhanced form_fields utility to include Table as a selectable field type. - Updated auto-imports and TypeScript definitions to accommodate new functionality. * feat: enhance Table component fieldtype mapping - Integrated utility function to map fieldtype for Table component, ensuring proper handling of field types. - Defaulted unmapped fieldtypes to "Data" for improved consistency in table rendering. * fix: update TableField component to use the correct Table component - Replaced ListView with Table in the TableField definition to ensure proper rendering of tabular data. - Adjusted imports in form_fields utility to reflect the change in component usage. * refactor: remove onMounted hook from useFieldOptions - Eliminated the onMounted lifecycle hook from the useFieldOptions composable to streamline the loading process of options. - Adjusted imports to reflect the removal of the unused onMounted function.
* fix: route handling for /form * feat: child table support v1 (#42) * chore: bump frappe-ui to `0.1.262` * feat: add 'Table' option to fieldtype selection - Updated form_field.json and form_field.py to include 'Table' in the list of selectable field types for enhanced functionality. - Adjusted the modified timestamp in form_field.json to reflect recent changes. * feat: implement Table field component and enhance field options handling - Added a new Table component for rendering tabular data input. - Updated RenderField and FieldRenderer components to support dynamic options for Select and Link fields. - Introduced useFieldOptions composable for improved option management. - Enhanced form_fields utility to include Table as a selectable field type. - Updated auto-imports and TypeScript definitions to accommodate new functionality. * feat: enhance Table component fieldtype mapping - Integrated utility function to map fieldtype for Table component, ensuring proper handling of field types. - Defaulted unmapped fieldtypes to "Data" for improved consistency in table rendering. * fix: update TableField component to use the correct Table component - Replaced ListView with Table in the TableField definition to ensure proper rendering of tabular data. - Adjusted imports in form_fields utility to reflect the change in component usage. * refactor: remove onMounted hook from useFieldOptions - Eliminated the onMounted lifecycle hook from the useFieldOptions composable to streamline the loading process of options. - Adjusted imports to reflect the removal of the unused onMounted function. * fix: child table fields fetch * refactor: update TextEditor component bindings - Changed `:model-value` to `:content` for TextEditor components across multiple files to ensure consistency in prop usage. - Added an empty declaration block in auto-imports.d.ts for improved TypeScript support. * feat: team management pages (#46) * chore: bump lucide-icons * feat: restructure home dashboard and sidebar - Introduced a new home dashboard layout with a dedicated Dashboard.vue component. - Migrated existing dashboard functionality from the previous Dashboard.vue to the new home structure. - Added sidebar items management through a new sidebarItems.ts file for better organization. - Updated TeamSwitcher component to conditionally render based on the current team. - Removed unused code and components to streamline the application. * refactor: enhance layout structure in BaseLayout and Dashboard components - Wrapped the slot in BaseLayout with a div for improved layout consistency. - Removed unnecessary padding class from the Dashboard component for a cleaner design. - Ensured consistent formatting in watch options for better readability. * fix: manageform page layout * feat: team invitations via User Invitations * feat: implement team member removal and permission toggling - Added functionality to remove members from a team and toggle their edit permissions. - Introduced `remove_member_from_team` and `toggle_can_edit_team` API endpoints. - Created `RemoveMemberDialog` component for user confirmation before removal. - Updated `TeamMemberList` to include removal actions and permission toggling. - Enhanced `FPTeam` model to manage team member permissions effectively. * feat: add team details update functionality - Implemented a new API endpoint `save` to update team fields, allowing modifications to `team_name` and `logo`. - Created `ManageTeamHeader` component for editing team name and logo upload functionality. - Integrated the new save functionality into the team store for seamless updates. - Updated `ManageTeam` page to include the new header component for enhanced team management. * refactor: clean up imports in team store - Removed unused `call` import from the team store file to streamline dependencies. * chore: better code - Added checks to prevent duplicate team member invitations and ensure team owners cannot have their permissions toggled. - Updated `FPTeam` model to prevent removal of the team owner from the team. - Modified `RemoveMemberDialog` and `TeamMemberList` components to improve member removal functionality and user experience. * fix: tests * chore: better code * fix: team avatar uploader (#47) * feat: Image uploader component with crop * feat: integrate ImageUploader for team logo management - Replaced the existing logo upload button with an ImageUploader component for enhanced functionality. - Added error handling and upload progress display within the ImageUploader. - Updated the button label dynamically based on the upload state and existing logo presence. * feat: enhance TeamSwitcher with search functionality - Added a search input to the TeamSwitcher component, allowing users to filter teams by name. - Updated the team options computation to include search query handling. - Integrated a new search icon and improved layout for better user experience. - Refactored dropdown item templates to accommodate the search input and maintain consistent styling. * refactor: update CreateTeamDialog to use ImageUploader for logo uploads - Replaced FileUploader with ImageUploader component for improved logo management. - Updated type handling for uploaded files to align with new component. - Enhanced error handling and upload progress display in the logo upload section. - Adjusted button label to reflect upload status dynamically. * refactor: update BaseLayout styling and icon usage - Modified the layout classes for improved design consistency and responsiveness. - Changed the logout button icon from a string to a component reference for better integration with the icon library. * feat: add Breadcrumbs component to Dashboard - Integrated Breadcrumbs component into the Dashboard for improved navigation. - Defined breadcrumb items to enhance user context within the application. * feat: submission list pages (#59) * feat: add submissions page and update sidebar navigation - Introduced a new "Manage Form Submissions" page for viewing form submissions. - Updated the router to include a route for submissions. - Refactored sidebar items to include navigation to the new submissions page. - Enhanced the overview page with breadcrumb navigation for better user context. * feat: submissions page v1 * feat: add Drawer component for UI enhancement - Introduced a new Drawer component to provide a sliding panel interface. - Implemented customizable properties for size, position, title, and actions. - Added keyboard accessibility with Escape key support for closing the drawer. - Included transition effects for smooth opening and closing animations. * feat: extend Drawer component with additional size options - Added "xl" size option to the Drawer component for enhanced customization. - Updated size classes for both horizontal and vertical orientations to include new "xl" dimensions. * feat: add submission details view and field value component - Introduced SubmissionDetails component to display detailed information about a specific submission. - Created SubmissionFieldValue component to render individual field values with appropriate formatting based on field type. - Enhanced SubmissionList component to include a drawer for viewing submission details upon row click. - Updated API integration to fetch submission data securely based on user permissions. * feat: enhance SubmissionFieldValue component with dynamic class handling - Added computed property to determine class names based on field type, improving layout for Switch and Checkbox types. - Updated template structure to utilize dynamic classes for better styling and responsiveness. * fix: validate doctype against linked_doctype in get_submission_response A client could pass an arbitrary doctype to access submissions from a different form. Now we fetch both linked_team_id and linked_doctype from the Form doc and reject requests where the supplied doctype doesn't match. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: throw when form not found in get_all_submissions If form_id is invalid, frappe.db.get_value returns None and calling has_permission(doc=None) has undefined behaviour. Explicitly throw DoesNotExistError before the permission check. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: show dash placeholder for null textarea value in SubmissionFieldValue Textarea was rendering blank when value is null/undefined, inconsistent with the default case which uses value ?? "–". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: remove unused isLoading ref in SubmissionList isLoading was declared and set but never bound in the template, so the loading state had no effect and was also never cleared on fetch error. Removed it entirely. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: show loading state in drawer when formData not yet available If a row is clicked before formData loads, the drawer was empty. Now shows a Loading... placeholder until linked_doctype is available. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: add aria-labelledby to Drawer dialog element Adds id="drawer-title" to the title heading and wires aria-labelledby on the dialog div so assistive tech can announce the drawer title. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: form sharing methods (#58) * fix: form sharing methods - Introduced `add_form_access` and `set_form_permission` functions to manage user permissions on forms. - Updated the frontend to utilize the new API endpoints for adding access and setting permissions. - Enhanced permission validation to ensure only authorized users can share forms. * fix: validate permission_to allowlist and add docstrings to sharing API - Validate `permission_to` against an explicit allowlist in `set_form_permission` to prevent unexpected kwargs from being forwarded to `add_docshare` - Use `int(bool(value))` for safe coercion of the permission value - Expand docstrings on `add_form_access` and `set_form_permission` with full Args/Raises sections and inline comments Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: hygine (#60) * chore: update gitignore and pre-commit config for current project structure Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: replace license.txt with LICENSE Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: add community health files and GitHub templates Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: update README with contributing and security references Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: update Python version references to 3.14 across all configs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: set ruff target-version to py313 (py314 not yet supported) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: move vulnerable dependency check to a separate workflow The vulnerable dependency check has been extracted from the linter workflow into its own dedicated workflow file. This change improves organization and allows for more focused execution of dependency checks during pull requests and manual triggers. * chore: update dependabot configuration to use npm instead of yarn * chore(deps): bump actions/cache from 4 to 5 (#61) Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](actions/cache@v4...v5) --- updated-dependencies: - dependency-name: actions/cache dependency-version: '5' 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> * chore(deps): bump actions/setup-node from 3 to 6 (#62) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3 to 6. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](actions/setup-node@v3...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> * chore(deps): bump actions/setup-python from 4 to 6 (#63) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 6. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](actions/setup-python@v4...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> * chore(deps): bump pre-commit/action from 3.0.0 to 3.0.1 (#65) Bumps [pre-commit/action](https://github.com/pre-commit/action) from 3.0.0 to 3.0.1. - [Release notes](https://github.com/pre-commit/action/releases) - [Commits](pre-commit/action@v3.0.0...v3.0.1) --- updated-dependencies: - dependency-name: pre-commit/action dependency-version: 3.0.1 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> * chore(deps): bump actions/checkout from 3 to 6 (#66) Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](actions/checkout@v3...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> * chore(deps-dev): bump jsdom from 25.0.1 to 29.0.2 in /frontend (#67) Bumps [jsdom](https://github.com/jsdom/jsdom) from 25.0.1 to 29.0.2. - [Release notes](https://github.com/jsdom/jsdom/releases) - [Commits](jsdom/jsdom@v25.0.1...v29.0.2) --- updated-dependencies: - dependency-name: jsdom dependency-version: 29.0.2 dependency-type: direct:development 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> * chore(deps): bump vue from 3.5.21 to 3.5.32 in /frontend (#68) Bumps [vue](https://github.com/vuejs/core) from 3.5.21 to 3.5.32. - [Release notes](https://github.com/vuejs/core/releases) - [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md) - [Commits](vuejs/core@v3.5.21...v3.5.32) --- updated-dependencies: - dependency-name: vue dependency-version: 3.5.32 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> * chore(deps): bump zod from 4.1.12 to 4.3.6 in /frontend (#69) Bumps [zod](https://github.com/colinhacks/zod) from 4.1.12 to 4.3.6. - [Release notes](https://github.com/colinhacks/zod/releases) - [Commits](colinhacks/zod@v4.1.12...v4.3.6) --- updated-dependencies: - dependency-name: zod dependency-version: 4.3.6 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> * chore(deps): bump socket.io-client from 4.8.1 to 4.8.3 in /frontend (#70) Bumps [socket.io-client](https://github.com/socketio/socket.io) from 4.8.1 to 4.8.3. - [Release notes](https://github.com/socketio/socket.io/releases) - [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md) - [Commits](https://github.com/socketio/socket.io/compare/socket.io-client@4.8.1...socket.io-client@4.8.3) --- updated-dependencies: - dependency-name: socket.io-client dependency-version: 4.8.3 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> * chore(deps-dev): bump postcss from 8.5.6 to 8.5.8 in /frontend (#71) Bumps [postcss](https://github.com/postcss/postcss) from 8.5.6 to 8.5.8. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](postcss/postcss@8.5.6...8.5.8) --- updated-dependencies: - dependency-name: postcss dependency-version: 8.5.8 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> * chore(deps): update frappe-ui to 0.1.272 * chore(deps): update vite to version 5.4.21 and adjust auto-imports and Vite config * refactor(tests): streamline email handling in TestTeamInvitations Removed the use of patching for the sendmail function and replaced it with a flag to mute emails during tests. This change simplifies the setup and teardown process while ensuring that email notifications are suppressed during test execution. Additionally, cleaned up the tearDown method to delete the Email Queue after tests. * refactor(tests): enhance email handling in TestTeamInvitations Reintroduced patching for the sendmail function in the TestTeamInvitations class to ensure email notifications are suppressed during tests. Updated the setUp and tearDown methods to manage the patching process effectively, ensuring compatibility with different Frappe versions. Removed the deletion of the Email Queue in tearDown as it is no longer necessary. * feat: test framework with test factory (#72) * feat(tests): add UserFactory and FPTeamFactory via frappe_factory_bot Introduces a factories package under forms_pro/tests/factories/ with: - UserFactory: generates random User docs; with_forms_pro_role trait includes the role in the initial doc dict so on_update fires once with the role already set - FPTeamFactory: generates FP Team docs with a random team name default Both factories implement __del_override__ as a safety-net cleanup guard. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(tests): use UserFactory in test_roles Replaces manual frappe.get_doc + Faker boilerplate with UserFactory.create(). Role assignment is kept as a post-insert step since the test specifically exercises the on_update hook that fires when a role is added after creation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(tests): use factories in TestTeamInvitations Replaces _create_user() and _create_team() helpers and the Faker/string tracking lists with UserFactory and FPTeamFactory. Key changes: - _user(*traits) and _team(owner) thin wrappers call the factories and track Document objects instead of email/name strings - Helper signatures updated to accept Document args throughout - UserFactory.build().email used where only a random email is needed (no DB insert required) - All 11 tests pass unchanged in behaviour Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(hooks): add required_apps for frappe_factory_bot Introduces a new required_apps list in hooks.py to include the frappe_factory_bot repository, enabling its integration into the application. This change enhances the app's capabilities by allowing the use of factory methods for testing and development. * ci: fetch frappe_factory_bot before installing forms_pro bench get-app with a local path skips required_apps resolution, so frappe_factory_bot was never fetched. bench install-app then failed with ModuleNotFoundError when Frappe tried to satisfy the required_apps entry. Explicitly get the app before installing forms_pro. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: update README with local test instructions and installation steps for frappe_factory_bot Added a new section to the README detailing how to run tests locally using frappe_factory_bot. Included installation instructions and commands for running all tests as well as specific test modules. * chore: remove del_override * refactor: tests * chore: refactor test_fp_team --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(deps): update devDependencies for Vite and Vitest (#73) Updated the versions of @vitejs/plugin-vue, @vitest/coverage-v8, vite, and vitest in package.json to their latest compatible releases. Removed deprecated dependencies from yarn.lock and updated @bcoe/v8-coverage to version 1.0.2. * feat(e2e): Playwright E2E test suite (#57) (#79) * feat(e2e): Phase 1 — Playwright foundation Install @playwright/test, add test:e2e scripts, create playwright.config.ts with Chromium project, global-setup for Frappe session auth, and a smoke spec that asserts an authenticated user can reach the forms dashboard. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(e2e): Phase 2 — data-testid attributes, POMs, and test data fixture Add data-testid to 7 Vue components (form-card, btn-new-form, field-type-*, btn-save-form, btn-publish, btn-submit-form, submission-success, btn-send-invite, input-invite-email). Restructure InviteMemberDialog to use #actions slot so the send button is directly targetable. Create Page Object Models (dashboard, form-builder, submission, team) and a Playwright fixture with API helpers for form create/publish/teardown. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(e2e): Phase 2+3 — semantic selectors and form-creation spec (5 tests) Remove all data-testid attributes from Vue components (reverted to original). Rewrite POMs with semantic selectors: getByRole/getByLabel/getByText, scoped to pre-existing data-form-builder-component attributes. Add form-creation.spec.ts covering dashboard listing, builder load, field canvas, publish, and unpublish. Tests avoid triggering Frappe validation by never saving a field with an empty label; publish/unpublish uses setValue({is_published}) which is always valid. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(e2e): Phase 3+4 — remaining specs, flake fixes, and CI workflow Specs: - form-submission.spec.ts — guest context submission + success message - submission-view.spec.ts — empty state and post-submission row visibility - team-invite.spec.ts — invite dialog open/fill/send flow Fixture: - submitForm fixture for creating a guest submission via API Reliability fixes: - smoke.spec.ts: reload on Frappe 500 under parallel startup load - TeamPage.goto(): waitForLoadState("networkidle") so Vue API calls complete before assertions run - TeamPage.openInviteDialog(): wait for email input to be visible (not just the dialog shell) before returning - playwright.config.ts: retries=1 everywhere; github reporter added CI: - .github/workflows/ui-tests.yml: full Playwright E2E workflow running against a real Frappe bench on every PR and push to develop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * ci(ui-tests): log bench serve output on startup timeout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * ci(ui-tests): fix server startup — switch to bench start, add hostname, fix action versions - Replace bench serve (silent crash) with bench start + Procfile (disable watch/schedule) - Add forms.test to /etc/hosts and set host_name config; rename site test_site → forms.test - Swap npx wait-on for a curl loop (no install overhead, same pattern as buzz CI) - Fix non-existent action versions: checkout@v6→@v4, setup-python@v6→@v5, setup-node@v6→@v4 - Add timeout-minutes: 60, fix concurrency group for push events, add workflow_dispatch - Add Playwright browser cache step and bench logs dump on failure Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * ci(ui-tests): setup E2E test data — give Admin Forms Pro role and default team `before_tests()` already added the role to Administrator, but the `on_update` hook that creates the default team checks `has_value_changed` on a freshly-fetched doc (which is always False), so no team was ever created. `_ensure_admin_has_default_team()` closes that gap explicitly. A new "Setup E2E test data" CI step calls `bench execute forms_pro.install.before_tests` before the server starts, so the test user has a team available when fixtures call `get_user_teams`. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(form-generator): replace clear_cache() with targeted doctype cache clear frappe.clear_cache() flushes Redis entirely, including session and CSRF token data. In the E2E test suite this causes all POST API calls after the first form creation to fail with CSRFTokenError, because the token captured during global-setup is invalidated server-side. Replace with frappe.clear_document_cache("DocType", name) which only evicts the specific newly-created DocType from the document cache — sufficient for the new schema to be reloaded on next access, and does not touch sessions or CSRF tokens. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * ci(ui-tests): set ignore_csrf=1 for test site Frappe's DocType.on_update() calls frappe.clear_cache() internally whenever a DocType is saved. This flushes session/CSRF data from Redis, invalidating the CSRF token captured in Playwright's storageState.json. All POST API calls after the first form creation then fail with CSRFTokenError. ignore_csrf=1 is the standard Frappe configuration for CI/test sites and skips CSRF validation entirely, which is correct here. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * ci(ui-tests): mute emails so invite test does not need an email account Sets mute_emails=1 in site config so Frappe returns a dummy outgoing account instead of throwing OutgoingEmailError when no real account is configured, allowing the team-invite E2E test to succeed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * ci(ui-tests): run E2E tests as the shared Forms Pro test user - Switch global-setup login from Administrator to FORMS_PRO_TEST_USER (test_forms_pro_user@example.com / testforms123) so E2E tests run under a least-privileged Forms Pro User, matching real-user conditions - Fix create_test_user() to strip the System User role Frappe auto-assigns on insert, leaving only the Forms Pro User role - Set a known password via update_password() so Playwright can log in - Add frontend/e2e/tsconfig.json with @types/node so process.env references in global-setup.ts type-check correctly - Install @types/node dev dependency Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: update .gitignore to include Playwright report and skills-lock.json * feat(multiselect): add Multiselect field with registry-driven builder extras (#85) * feat(form-field): add Multiselect fieldtype to doctype and backend mapping * fix(submission): serialize list values to JSON string before DocType insert * feat(multiselect): wire up Multiselect field across the frontend * docs: add add-field skill and register it in CLAUDE.md * chore: move skills to .claude/skills, gitignore .agents, document npx skills workflow * docs: add userinterface-wiki skill reference to CLAUDE.md * feat(fieldTypes): add optional builderExtras component slot to FieldTypeDefinition * refactor(fields): move Multiselect into multiselect/ subfolder, add MultiselectBuilderExtras * feat(fieldTypes): update Multiselect import path and register MultiselectBuilderExtras * refactor(builder): replace hardcoded Multiselect extras with registry-driven builderExtras * feat(multiselect): enhance option addition with error handling and unique validation * feat(multiselect): change layout to description-first (label → desc → checkboxes) * refactor(builder): wrap template in single root div so builderExtras stacks below field * test(e2e): add Playwright spec for Multiselect field builder and submission flow * test(e2e): wait for canvas render and set label before adding options * test(e2e): fix strict mode violation on No options defined locator * test(e2e): wait for Add Option button to confirm field rendered on canvas * test(e2e): ensure Add Option button is visible before adding options * fix(e2e): ensure form is saved before publishing to enable Publish button * fix(e2e): set form title and wait on Save button state Multiselect spec clicked Save while form title was blank — Frappe rejected the save with MandatoryError, Save button never hid, and the publish helper timed out. Fill a title before save so the request succeeds, and in the helper wait for the Save button to disappear (same render cycle as Publish appearing) instead of polling for Publish. * fix(ci): resolve all CI failures on backport branch - Node 18 → 20 in ci.yml and typecheck.yml (@vitejs/plugin-vue@6 requires ≥20.19) - nosemgrep comment on allow_guest=True in get_doctype_fields (intentional for public form rendering) - Migrate form_fields.ts to re-export FIELD_TYPE_DEFINITIONS, fixing Multiselect missing from sidebar - Add Multiselect to FormFieldTypes enum; fix TS2367 comparison in SubmissionFieldValue.vue * fix(tests): use FrappeTestCase for version-15 compatibility IntegrationTestCase was introduced in Frappe v16; version-15 exposes FrappeTestCase from frappe.tests.utils. --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Still need to use doctypes created via desk.
Child table creation on the fly should come in next update of this
Summary by CodeRabbit