-
Notifications
You must be signed in to change notification settings - Fork 6
feat: child table support v1 #42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7085d32
cbd05ad
c7db530
e49e31c
1bb215a
4dca693
42a4b2c
cf69953
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,8 +1,9 @@ | ||||||||||||||||||||||||||||||||||
| <script setup lang="ts"> | ||||||||||||||||||||||||||||||||||
| import { computed, ref, watch, onMounted } from "vue"; | ||||||||||||||||||||||||||||||||||
| import { computed } from "vue"; | ||||||||||||||||||||||||||||||||||
| import { Asterisk } from "lucide-vue-next"; | ||||||||||||||||||||||||||||||||||
| import RenderField from "../RenderField.vue"; | ||||||||||||||||||||||||||||||||||
| import { createResource } from "frappe-ui"; | ||||||||||||||||||||||||||||||||||
| import Table from "@/components/fields/Table.vue"; | ||||||||||||||||||||||||||||||||||
| import { useFieldOptions } from "@/utils/selectOptions"; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const props = defineProps({ | ||||||||||||||||||||||||||||||||||
| field: { | ||||||||||||||||||||||||||||||||||
|
|
@@ -48,59 +49,7 @@ const getClasses = computed(() => { | |||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| type SelectOption = { | ||||||||||||||||||||||||||||||||||
| label: string; | ||||||||||||||||||||||||||||||||||
| value: string; | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Reactive ref to store options | ||||||||||||||||||||||||||||||||||
| const selectOptions = ref<string[] | SelectOption[] | null>(null); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const getOptions = async () => { | ||||||||||||||||||||||||||||||||||
| if (!fieldData.value.options) { | ||||||||||||||||||||||||||||||||||
| return ""; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (fieldData.value.fieldtype === "Select") { | ||||||||||||||||||||||||||||||||||
| return fieldData.value.options.split("\n"); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (fieldData.value.fieldtype === "Link") { | ||||||||||||||||||||||||||||||||||
| const _options = createResource({ | ||||||||||||||||||||||||||||||||||
| url: "forms_pro.api.form.get_link_field_options", | ||||||||||||||||||||||||||||||||||
| makeParams: () => { | ||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||
| doctype: fieldData.value.options, | ||||||||||||||||||||||||||||||||||
| filters: {}, | ||||||||||||||||||||||||||||||||||
| page_length: 999, | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
| await _options.fetch(); | ||||||||||||||||||||||||||||||||||
| return _options.data; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return fieldData.value.options; | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Load options when component mounts or field data changes | ||||||||||||||||||||||||||||||||||
| const loadOptions = async () => { | ||||||||||||||||||||||||||||||||||
| selectOptions.value = await getOptions(); | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Watch for changes to field type or options | ||||||||||||||||||||||||||||||||||
| watch( | ||||||||||||||||||||||||||||||||||
| () => [fieldData.value.fieldtype, fieldData.value.options], | ||||||||||||||||||||||||||||||||||
| () => { | ||||||||||||||||||||||||||||||||||
| loadOptions(); | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
| { immediate: true } | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Also load on mount | ||||||||||||||||||||||||||||||||||
| onMounted(() => { | ||||||||||||||||||||||||||||||||||
| loadOptions(); | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
| const { options: selectOptions } = useFieldOptions(fieldData); | ||||||||||||||||||||||||||||||||||
| </script> | ||||||||||||||||||||||||||||||||||
| <template> | ||||||||||||||||||||||||||||||||||
| <div :class="getClasses" v-if="fieldData.fieldtype == 'Switch'"> | ||||||||||||||||||||||||||||||||||
|
|
@@ -200,6 +149,27 @@ onMounted(() => { | |||||||||||||||||||||||||||||||||
| {{ fieldData.description }} | ||||||||||||||||||||||||||||||||||
| </small> | ||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||
| <div v-else-if="fieldData.fieldtype == 'Table'" class="w-full space-y-4"> | ||||||||||||||||||||||||||||||||||
| <div class="flex gap-2 items-start"> | ||||||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||||||
| v-if="inEditMode" | ||||||||||||||||||||||||||||||||||
| placeholder="Label" | ||||||||||||||||||||||||||||||||||
| type="text" | ||||||||||||||||||||||||||||||||||
| v-model="fieldData.label" | ||||||||||||||||||||||||||||||||||
| class="bg-transparent border-none outline-none text-base focus:ring-0 w-fit px-0 py-1" | ||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||
| <label class="text-base" v-else>{{ fieldData.label }}</label> | ||||||||||||||||||||||||||||||||||
| <Asterisk v-if="fieldData.reqd" class="w-4 h-4 text-red-400" /> | ||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||
| <small class="text-gray-500"> | ||||||||||||||||||||||||||||||||||
| {{ fieldData.description }} | ||||||||||||||||||||||||||||||||||
| </small> | ||||||||||||||||||||||||||||||||||
| <Table | ||||||||||||||||||||||||||||||||||
| v-model="modelValue as undefined" | ||||||||||||||||||||||||||||||||||
| :in-edit-mode="inEditMode" | ||||||||||||||||||||||||||||||||||
| :doctype="fieldData.options" | ||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+164
to
+171
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two small issues in the Table branch.
♻️ 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||
| <div v-else :class="getClasses"> | ||||||||||||||||||||||||||||||||||
| <div class="flex gap-2 items-start"> | ||||||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,87 @@ | ||||||||||||||||||||||||||||||||
| <script setup lang="ts"> | ||||||||||||||||||||||||||||||||
| import { useCall, Button } from "frappe-ui"; | ||||||||||||||||||||||||||||||||
| import { computed } from "vue"; | ||||||||||||||||||||||||||||||||
| import FieldRenderer from "../builder/FieldRenderer.vue"; | ||||||||||||||||||||||||||||||||
| import { mapDoctypeFieldForForm } from "@/utils/form_fields"; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| type Row = { | ||||||||||||||||||||||||||||||||
| [key: string]: any; | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const rows = defineModel<Row[]>({ default: [] }); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| type props = { | ||||||||||||||||||||||||||||||||
| inEditMode: boolean; | ||||||||||||||||||||||||||||||||
| doctype: string; | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const props = defineProps<props>(); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const columnResource = useCall({ | ||||||||||||||||||||||||||||||||
| url: "forms_pro.api.form.get_doctype_fields", | ||||||||||||||||||||||||||||||||
| baseUrl: "/api/v2/method/", | ||||||||||||||||||||||||||||||||
| params: { | ||||||||||||||||||||||||||||||||
| doctype: props.doctype, | ||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const columns = computed(() => { | ||||||||||||||||||||||||||||||||
| const data = columnResource.data; | ||||||||||||||||||||||||||||||||
| if (!Array.isArray(data)) { | ||||||||||||||||||||||||||||||||
| return []; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| return data.map((column: any) => ({ | ||||||||||||||||||||||||||||||||
| label: column.label, | ||||||||||||||||||||||||||||||||
| key: column.fieldname, | ||||||||||||||||||||||||||||||||
| ...column, | ||||||||||||||||||||||||||||||||
| fieldtype: mapDoctypeFieldForForm(column.fieldtype) ?? "Data", | ||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| function addRow() { | ||||||||||||||||||||||||||||||||
| const newRow = columns.value.reduce((acc, column) => { | ||||||||||||||||||||||||||||||||
| acc[column.key] = null; | ||||||||||||||||||||||||||||||||
| return acc; | ||||||||||||||||||||||||||||||||
| }, {} as Row); | ||||||||||||||||||||||||||||||||
| rows.value = [...(rows.value ?? []), newRow]; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
+41
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If a user clicks "Add Row" before the ♻️ 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| function removeRow(index: number) { | ||||||||||||||||||||||||||||||||
| rows.value = rows.value?.filter((_, i) => i !== index) ?? []; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| function updateCell(rowIndex: number, key: string, value: unknown) { | ||||||||||||||||||||||||||||||||
| const list = rows.value ?? []; | ||||||||||||||||||||||||||||||||
| if (rowIndex < 0 || rowIndex >= list.length) return; | ||||||||||||||||||||||||||||||||
| rows.value = list.map((row, i) => (i === rowIndex ? { ...row, [key]: value } : row)); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| </script> | ||||||||||||||||||||||||||||||||
| <template> | ||||||||||||||||||||||||||||||||
| <div class="flex flex-col gap-5 p-4 bg-surface-gray-1 border rounded"> | ||||||||||||||||||||||||||||||||
| <div v-if="!rows.length" class="flex flex-col gap-2 items-center justify-center"> | ||||||||||||||||||||||||||||||||
| <h5 class="text-base font-medium text-ink-gray-6">No rows added</h5> | ||||||||||||||||||||||||||||||||
| <p class="text-sm text-ink-gray-5">Add an item to get started</p> | ||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||
| v-for="(_, index) in rows" | ||||||||||||||||||||||||||||||||
| :key="index" | ||||||||||||||||||||||||||||||||
| class="border-b border-gray-400 pb-4 border p-4 rounded bg-surface-white relative" | ||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||
| <Button | ||||||||||||||||||||||||||||||||
| icon="x" | ||||||||||||||||||||||||||||||||
| variant="outline" | ||||||||||||||||||||||||||||||||
| class="absolute -top-2 -right-2 w-4 h-4" | ||||||||||||||||||||||||||||||||
| @click="removeRow(index)" | ||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||
| <FieldRenderer | ||||||||||||||||||||||||||||||||
| v-for="column in columns" | ||||||||||||||||||||||||||||||||
| :key="column.key" | ||||||||||||||||||||||||||||||||
| :model-value="rows[index][column.key]" | ||||||||||||||||||||||||||||||||
| @update:model-value="updateCell(index, column.key, $event)" | ||||||||||||||||||||||||||||||||
| :field="column" | ||||||||||||||||||||||||||||||||
| :in-edit-mode="false" | ||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||
| <Button v-if="!inEditMode" @click="addRow">Add Row</Button> | ||||||||||||||||||||||||||||||||
| </template> | ||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| import { createResource } from "frappe-ui"; | ||
| import type { Ref, ComputedRef } from "vue"; | ||
| import { ref, watch } from "vue"; | ||
|
|
||
| export type SelectOption = { | ||
| label: string; | ||
| value: string; | ||
| }; | ||
|
|
||
| export type FieldForOptions = { | ||
| fieldtype?: string; | ||
| options?: string; | ||
| [key: string]: unknown; | ||
| }; | ||
|
|
||
| /** | ||
| * Resolves options for Select and Link form fields. | ||
| * Returns string[] for Select (newline-separated options), API result for Link, or raw options otherwise. | ||
| */ | ||
| export async function getFieldOptions( | ||
| field: FieldForOptions | ||
| ): Promise<string[] | SelectOption[] | string | ""> { | ||
| if (!field?.options) { | ||
| return ""; | ||
| } | ||
|
|
||
| if (field.fieldtype === "Select") { | ||
| return field.options.split("\n"); | ||
| } | ||
|
|
||
| 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[]; | ||
| } | ||
|
|
||
| return field.options; | ||
| } | ||
|
|
||
| /** | ||
| * Composable to load and reactively track options for a Select/Link field. | ||
| * Call load() or rely on automatic load on mount and when field type/options change. | ||
| */ | ||
| export function useFieldOptions( | ||
| field: Ref<FieldForOptions> | ComputedRef<FieldForOptions> | ||
| ) { | ||
| const options = ref<string[] | SelectOption[] | null>(null); | ||
|
|
||
| const load = async () => { | ||
| const result = await getFieldOptions(field.value); | ||
| options.value = | ||
| result === "" || result === undefined | ||
| ? null | ||
| : (result as string[] | SelectOption[]); | ||
| }; | ||
|
Comment on lines
+56
to
+62
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Non-array When 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 |
||
|
|
||
| watch( | ||
| () => [field.value.fieldtype, field.value.options], | ||
| () => load(), | ||
| { immediate: true } | ||
| ); | ||
|
|
||
| return { options, load }; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
v-model="modelValue as undefined"silences a legitimate type mismatch.defineModel()inFieldRendereris untyped (unknown), whileTable.vuedeclaresdefineModel<Row[]>(). Cast-to-undefinedis a no-op at runtime but hides the real fix: either typemodelValueasRow[]in this component or use a properly typed intermediate.♻️ Proposed fix
Then in the template:
🤖 Prompt for AI Agents