Merge pull request #808 from frappe/mergify/bp/main-hotfix/pr-806
This commit is contained in:
commit
b82362a869
@ -47,7 +47,10 @@ def get_fields_layout(doctype: str, type: str, parent_doctype: str | None = None
|
|||||||
|
|
||||||
for tab in tabs:
|
for tab in tabs:
|
||||||
for section in tab.get("sections"):
|
for section in tab.get("sections"):
|
||||||
|
if section.get("columns"):
|
||||||
|
section["columns"] = [column for column in section.get("columns") if column]
|
||||||
for column in section.get("columns") if section.get("columns") else []:
|
for column in section.get("columns") if section.get("columns") else []:
|
||||||
|
column["fields"] = [field for field in column.get("fields") if field]
|
||||||
for field in column.get("fields") if column.get("fields") else []:
|
for field in column.get("fields") if column.get("fields") else []:
|
||||||
field = next((f for f in fields if f.fieldname == field), None)
|
field = next((f for f in fields if f.fieldname == field), None)
|
||||||
if field:
|
if field:
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
<div class="flex h-8 items-center text-xl font-semibold text-ink-gray-8">
|
<div class="flex h-8 items-center text-xl font-semibold text-ink-gray-8">
|
||||||
{{ __('Data') }}
|
{{ __('Data') }}
|
||||||
<Badge
|
<Badge
|
||||||
v-if="data.isDirty"
|
v-if="document.isDirty"
|
||||||
class="ml-3"
|
class="ml-3"
|
||||||
:label="'Not Saved'"
|
:label="'Not Saved'"
|
||||||
theme="orange"
|
theme="orange"
|
||||||
@ -20,15 +20,15 @@
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
label="Save"
|
label="Save"
|
||||||
:disabled="!data.isDirty"
|
:disabled="!document.isDirty"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
:loading="data.save.loading"
|
:loading="document.save.loading"
|
||||||
@click="saveChanges"
|
@click="saveChanges"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="data.get.loading"
|
v-if="document.get.loading"
|
||||||
class="flex flex-1 flex-col items-center justify-center gap-3 text-xl font-medium text-gray-500"
|
class="flex flex-1 flex-col items-center justify-center gap-3 text-xl font-medium text-gray-500"
|
||||||
>
|
>
|
||||||
<LoadingIndicator class="h-6 w-6" />
|
<LoadingIndicator class="h-6 w-6" />
|
||||||
@ -38,7 +38,7 @@
|
|||||||
<FieldLayout
|
<FieldLayout
|
||||||
v-if="tabs.data"
|
v-if="tabs.data"
|
||||||
:tabs="tabs.data"
|
:tabs="tabs.data"
|
||||||
:data="data.doc"
|
:data="document.doc"
|
||||||
:doctype="doctype"
|
:doctype="doctype"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -49,7 +49,7 @@
|
|||||||
@reload="
|
@reload="
|
||||||
() => {
|
() => {
|
||||||
tabs.reload()
|
tabs.reload()
|
||||||
data.reload()
|
document.reload()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
@ -59,10 +59,10 @@
|
|||||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||||
import DataFieldsModal from '@/components/Modals/DataFieldsModal.vue'
|
import DataFieldsModal from '@/components/Modals/DataFieldsModal.vue'
|
||||||
import FieldLayout from '@/components/FieldLayout/FieldLayout.vue'
|
import FieldLayout from '@/components/FieldLayout/FieldLayout.vue'
|
||||||
import { Badge, createResource, createDocumentResource } from 'frappe-ui'
|
import { Badge, createResource } from 'frappe-ui'
|
||||||
import LoadingIndicator from '@/components/Icons/LoadingIndicator.vue'
|
import LoadingIndicator from '@/components/Icons/LoadingIndicator.vue'
|
||||||
import { createToast } from '@/utils'
|
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
|
import { useDocument } from '@/data/document'
|
||||||
import { isMobileView } from '@/composables/settings'
|
import { isMobileView } from '@/composables/settings'
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
@ -76,33 +76,11 @@ const props = defineProps({
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { isManager } = usersStore()
|
const { isManager } = usersStore()
|
||||||
|
|
||||||
const showDataFieldsModal = ref(false)
|
const showDataFieldsModal = ref(false)
|
||||||
|
|
||||||
const data = createDocumentResource({
|
const { document } = useDocument(props.doctype, props.docname)
|
||||||
doctype: props.doctype,
|
|
||||||
name: props.docname,
|
|
||||||
setValue: {
|
|
||||||
onSuccess: () => {
|
|
||||||
data.reload()
|
|
||||||
createToast({
|
|
||||||
title: 'Data Updated',
|
|
||||||
icon: 'check',
|
|
||||||
iconClasses: 'text-ink-green-3',
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onError: (err) => {
|
|
||||||
createToast({
|
|
||||||
title: 'Error',
|
|
||||||
text: err.messages[0],
|
|
||||||
icon: 'x',
|
|
||||||
iconClasses: 'text-red-600',
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const tabs = createResource({
|
const tabs = createResource({
|
||||||
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||||
@ -112,19 +90,19 @@ const tabs = createResource({
|
|||||||
})
|
})
|
||||||
|
|
||||||
function saveChanges() {
|
function saveChanges() {
|
||||||
data.save.submit()
|
document.save.submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => data.doc,
|
() => document.doc,
|
||||||
(newValue, oldValue) => {
|
(newValue, oldValue) => {
|
||||||
if (!oldValue) return
|
if (!oldValue) return
|
||||||
if (newValue && oldValue) {
|
if (newValue && oldValue) {
|
||||||
const isDirty =
|
const isDirty =
|
||||||
JSON.stringify(newValue) !== JSON.stringify(data.originalDoc)
|
JSON.stringify(newValue) !== JSON.stringify(document.originalDoc)
|
||||||
data.isDirty = isDirty
|
document.isDirty = isDirty
|
||||||
if (isDirty) {
|
if (isDirty) {
|
||||||
data.save.loading = false
|
document.save.loading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -93,7 +93,10 @@
|
|||||||
:key="field.fieldname"
|
:key="field.fieldname"
|
||||||
>
|
>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-if="field.read_only && field.fieldtype !== 'Check'"
|
v-if="
|
||||||
|
field.read_only &&
|
||||||
|
!['Float', 'Currency', 'Check'].includes(field.fieldtype)
|
||||||
|
"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
v-model="row[field.fieldname]"
|
v-model="row[field.fieldname]"
|
||||||
@ -104,13 +107,14 @@
|
|||||||
['Link', 'Dynamic Link'].includes(field.fieldtype)
|
['Link', 'Dynamic Link'].includes(field.fieldtype)
|
||||||
"
|
"
|
||||||
class="text-sm text-ink-gray-8"
|
class="text-sm text-ink-gray-8"
|
||||||
v-model="row[field.fieldname]"
|
:value="row[field.fieldname]"
|
||||||
:doctype="
|
:doctype="
|
||||||
field.fieldtype == 'Link'
|
field.fieldtype == 'Link'
|
||||||
? field.options
|
? field.options
|
||||||
: row[field.options]
|
: row[field.options]
|
||||||
"
|
"
|
||||||
:filters="field.filters"
|
:filters="field.filters"
|
||||||
|
@change="(v) => fieldChange(v, field, row)"
|
||||||
/>
|
/>
|
||||||
<Link
|
<Link
|
||||||
v-else-if="field.fieldtype === 'User'"
|
v-else-if="field.fieldtype === 'User'"
|
||||||
@ -118,7 +122,7 @@
|
|||||||
:value="getUser(row[field.fieldname]).full_name"
|
:value="getUser(row[field.fieldname]).full_name"
|
||||||
:doctype="field.options"
|
:doctype="field.options"
|
||||||
:filters="field.filters"
|
:filters="field.filters"
|
||||||
@change="(v) => (row[field.fieldname] = v)"
|
@change="(v) => fieldChange(v, field, row)"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
:hideMe="true"
|
:hideMe="true"
|
||||||
>
|
>
|
||||||
@ -148,23 +152,26 @@
|
|||||||
class="cursor-pointer duration-300"
|
class="cursor-pointer duration-300"
|
||||||
v-model="row[field.fieldname]"
|
v-model="row[field.fieldname]"
|
||||||
:disabled="!gridSettings.editable_grid"
|
:disabled="!gridSettings.editable_grid"
|
||||||
|
@change="(e) => fieldChange(e.target.checked, field, row)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
v-else-if="field.fieldtype === 'Date'"
|
v-else-if="field.fieldtype === 'Date'"
|
||||||
v-model="row[field.fieldname]"
|
:value="row[field.fieldname]"
|
||||||
icon-left=""
|
icon-left=""
|
||||||
variant="outline"
|
variant="outline"
|
||||||
:formatter="(date) => getFormat(date, '', true)"
|
:formatter="(date) => getFormat(date, '', true)"
|
||||||
input-class="border-none text-sm text-ink-gray-8"
|
input-class="border-none text-sm text-ink-gray-8"
|
||||||
|
@change="(v) => fieldChange(v, field, row)"
|
||||||
/>
|
/>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
v-else-if="field.fieldtype === 'Datetime'"
|
v-else-if="field.fieldtype === 'Datetime'"
|
||||||
v-model="row[field.fieldname]"
|
:value="row[field.fieldname]"
|
||||||
icon-left=""
|
icon-left=""
|
||||||
variant="outline"
|
variant="outline"
|
||||||
:formatter="(date) => getFormat(date, '', true, true)"
|
:formatter="(date) => getFormat(date, '', true, true)"
|
||||||
input-class="border-none text-sm text-ink-gray-8"
|
input-class="border-none text-sm text-ink-gray-8"
|
||||||
|
@change="(v) => fieldChange(v, field, row)"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-else-if="
|
v-else-if="
|
||||||
@ -175,13 +182,8 @@
|
|||||||
rows="1"
|
rows="1"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
v-model="row[field.fieldname]"
|
:value="row[field.fieldname]"
|
||||||
/>
|
@change="fieldChange($event.target.value, field, row)"
|
||||||
<FormControl
|
|
||||||
v-else-if="['Int'].includes(field.fieldtype)"
|
|
||||||
type="number"
|
|
||||||
variant="outline"
|
|
||||||
v-model="row[field.fieldname]"
|
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-else-if="field.fieldtype === 'Select'"
|
v-else-if="field.fieldtype === 'Select'"
|
||||||
@ -190,6 +192,38 @@
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
v-model="row[field.fieldname]"
|
v-model="row[field.fieldname]"
|
||||||
:options="field.options"
|
:options="field.options"
|
||||||
|
@change="(e) => fieldChange(e.target.value, field, row)"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-else-if="['Int'].includes(field.fieldtype)"
|
||||||
|
type="number"
|
||||||
|
variant="outline"
|
||||||
|
:value="row[field.fieldname]"
|
||||||
|
@change="fieldChange($event.target.value, field, row)"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-else-if="field.fieldtype === 'Percent'"
|
||||||
|
type="text"
|
||||||
|
variant="outline"
|
||||||
|
:value="getFormattedPercent(field.fieldname, row)"
|
||||||
|
:disabled="Boolean(field.read_only)"
|
||||||
|
@change="fieldChange(flt($event.target.value), field, row)"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-else-if="field.fieldtype === 'Float'"
|
||||||
|
type="text"
|
||||||
|
variant="outline"
|
||||||
|
:value="getFormattedFloat(field.fieldname, row)"
|
||||||
|
:disabled="Boolean(field.read_only)"
|
||||||
|
@change="fieldChange(flt($event.target.value), field, row)"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-else-if="field.fieldtype === 'Currency'"
|
||||||
|
type="text"
|
||||||
|
variant="outline"
|
||||||
|
:value="getFormattedCurrency(field.fieldname, row)"
|
||||||
|
:disabled="Boolean(field.read_only)"
|
||||||
|
@change="fieldChange(flt($event.target.value), field, row)"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-else
|
v-else
|
||||||
@ -198,6 +232,7 @@
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
v-model="row[field.fieldname]"
|
v-model="row[field.fieldname]"
|
||||||
:options="field.options"
|
:options="field.options"
|
||||||
|
@change="fieldChange($event.target.value, field, row)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -265,6 +300,7 @@ import EditIcon from '@/components/Icons/EditIcon.vue'
|
|||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
import { getRandom, getFormat, isTouchScreenDevice } from '@/utils'
|
import { getRandom, getFormat, isTouchScreenDevice } from '@/utils'
|
||||||
|
import { flt } from '@/utils/numberFormat.js'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { getMeta } from '@/stores/meta'
|
import { getMeta } from '@/stores/meta'
|
||||||
import {
|
import {
|
||||||
@ -274,9 +310,10 @@ import {
|
|||||||
DateTimePicker,
|
DateTimePicker,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
dayjs,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import Draggable from 'vuedraggable'
|
import Draggable from 'vuedraggable'
|
||||||
import { ref, reactive, computed } from 'vue'
|
import { ref, reactive, computed, inject } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
label: {
|
label: {
|
||||||
@ -291,11 +328,24 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
parentFieldname: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { getGridViewSettings, getFields, getGridSettings } = getMeta(
|
const triggerOnChange = inject('triggerOnChange')
|
||||||
props.doctype,
|
const triggerOnRowAdd = inject('triggerOnRowAdd')
|
||||||
)
|
const triggerOnRowRemove = inject('triggerOnRowRemove')
|
||||||
|
|
||||||
|
const {
|
||||||
|
getGridViewSettings,
|
||||||
|
getFields,
|
||||||
|
getFormattedPercent,
|
||||||
|
getFormattedFloat,
|
||||||
|
getFormattedCurrency,
|
||||||
|
getGridSettings,
|
||||||
|
} = getMeta(props.doctype)
|
||||||
getMeta(props.parentDoctype)
|
getMeta(props.parentDoctype)
|
||||||
const { getUser } = usersStore()
|
const { getUser } = usersStore()
|
||||||
|
|
||||||
@ -322,6 +372,10 @@ const fields = computed(() => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const allFields = computed(() => {
|
||||||
|
return getFields()?.map((f) => getFieldObj(f)) || []
|
||||||
|
})
|
||||||
|
|
||||||
function getFieldObj(field) {
|
function getFieldObj(field) {
|
||||||
return {
|
return {
|
||||||
...field,
|
...field,
|
||||||
@ -367,21 +421,71 @@ const toggleSelectRow = (row) => {
|
|||||||
|
|
||||||
const addRow = () => {
|
const addRow = () => {
|
||||||
const newRow = {}
|
const newRow = {}
|
||||||
fields.value?.forEach((field) => {
|
allFields.value?.forEach((field) => {
|
||||||
if (field.fieldtype === 'Check') newRow[field.fieldname] = false
|
if (field.fieldtype === 'Check') {
|
||||||
else newRow[field.fieldname] = ''
|
newRow[field.fieldname] = false
|
||||||
|
} else {
|
||||||
|
newRow[field.fieldname] = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.default) {
|
||||||
|
newRow[field.fieldname] = getDefaultValue(field.default, field.fieldtype)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
newRow.name = getRandom(10)
|
newRow.name = getRandom(10)
|
||||||
showRowList.value.push(false)
|
showRowList.value.push(false)
|
||||||
newRow['__islocal'] = true
|
newRow['__islocal'] = true
|
||||||
|
newRow['idx'] = rows.value.length + 1
|
||||||
|
newRow['doctype'] = props.doctype
|
||||||
|
newRow['parentfield'] = props.parentFieldname
|
||||||
|
newRow['parenttype'] = props.parentDoctype
|
||||||
rows.value.push(newRow)
|
rows.value.push(newRow)
|
||||||
|
triggerOnRowAdd(newRow)
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteRows = () => {
|
const deleteRows = () => {
|
||||||
rows.value = rows.value.filter((row) => !selectedRows.has(row.name))
|
rows.value = rows.value.filter((row) => !selectedRows.has(row.name))
|
||||||
|
triggerOnRowRemove(selectedRows, rows.value)
|
||||||
|
|
||||||
showRowList.value.pop()
|
showRowList.value.pop()
|
||||||
selectedRows.clear()
|
selectedRows.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fieldChange(value, field, row) {
|
||||||
|
row[field.fieldname] = value
|
||||||
|
triggerOnChange(field.fieldname, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultValue(defaultValue, fieldtype) {
|
||||||
|
if (['Float', 'Currency', 'Percent'].includes(fieldtype)) {
|
||||||
|
return flt(defaultValue)
|
||||||
|
} else if (fieldtype === 'Check') {
|
||||||
|
if (['1', 'true', 'True'].includes(defaultValue)) {
|
||||||
|
return true
|
||||||
|
} else if (['0', 'false', 'False'].includes(defaultValue)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else if (fieldtype === 'Int') {
|
||||||
|
return parseInt(defaultValue)
|
||||||
|
} else if (defaultValue === 'Today' && fieldtype === 'Date') {
|
||||||
|
return dayjs().format('YYYY-MM-DD')
|
||||||
|
} else if (
|
||||||
|
['Now', 'now'].includes(defaultValue) &&
|
||||||
|
fieldtype === 'Datetime'
|
||||||
|
) {
|
||||||
|
return dayjs().format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
} else if (['Now', 'now'].includes(defaultValue) && fieldtype === 'Time') {
|
||||||
|
return dayjs().format('HH:mm:ss')
|
||||||
|
} else if (fieldtype === 'Date') {
|
||||||
|
return dayjs(defaultValue).format('YYYY-MM-DD')
|
||||||
|
} else if (fieldtype === 'Datetime') {
|
||||||
|
return dayjs(defaultValue).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
} else if (fieldtype === 'Time') {
|
||||||
|
return dayjs(defaultValue).format('HH:mm:ss')
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -139,9 +139,14 @@ const oldFields = computed(() => {
|
|||||||
const fields = ref(JSON.parse(JSON.stringify(oldFields.value || [])))
|
const fields = ref(JSON.parse(JSON.stringify(oldFields.value || [])))
|
||||||
|
|
||||||
const dropdownFields = computed(() => {
|
const dropdownFields = computed(() => {
|
||||||
return getFields()?.filter(
|
return getFields()?.filter((field) => {
|
||||||
(field) => !fields.value.find((f) => f.fieldname === field.fieldname),
|
return (
|
||||||
)
|
!fields.value.find((f) => f.fieldname === field.fieldname) &&
|
||||||
|
!['Tab Break', 'Section Break', 'Column Break', 'Table'].includes(
|
||||||
|
field.fieldtype,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
|
|||||||
@ -23,7 +23,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<FieldLayout v-if="tabs.data" :tabs="tabs.data" :data="data" />
|
<FieldLayout
|
||||||
|
v-if="tabs.data"
|
||||||
|
:tabs="tabs.data"
|
||||||
|
:data="data"
|
||||||
|
:doctype="doctype"
|
||||||
|
:isGridRow="true"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -60,6 +60,8 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['change'])
|
||||||
|
|
||||||
const { getFields } = getMeta(props.doctype)
|
const { getFields } = getMeta(props.doctype)
|
||||||
|
|
||||||
const values = defineModel()
|
const values = defineModel()
|
||||||
@ -109,14 +111,16 @@ const addValue = (value) => {
|
|||||||
|
|
||||||
if (value) {
|
if (value) {
|
||||||
values.value.push({ [linkField.value.fieldname]: value })
|
values.value.push({ [linkField.value.fieldname]: value })
|
||||||
|
emit('change', values.value)
|
||||||
!error.value && (query.value = '')
|
!error.value && (query.value = '')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeValue = (value) => {
|
const removeValue = (value) => {
|
||||||
values.value = values.value.filter(
|
let _value = values.value.filter(
|
||||||
(row) => row[linkField.value.fieldname] !== value,
|
(row) => row[linkField.value.fieldname] !== value,
|
||||||
)
|
)
|
||||||
|
emit('change', _value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeLastValue = () => {
|
const removeLastValue = () => {
|
||||||
@ -125,12 +129,11 @@ const removeLastValue = () => {
|
|||||||
let valueRef = valuesRef.value[valuesRef.value.length - 1]?.$el
|
let valueRef = valuesRef.value[valuesRef.value.length - 1]?.$el
|
||||||
if (document.activeElement === valueRef) {
|
if (document.activeElement === valueRef) {
|
||||||
values.value.pop()
|
values.value.pop()
|
||||||
|
emit('change', values.value)
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (values.value.length) {
|
if (values.value.length) {
|
||||||
valueRef = valuesRef.value[valuesRef.value.length - 1].$el
|
valueRef = valuesRef.value[valuesRef.value.length - 1].$el
|
||||||
valueRef?.focus()
|
valueRef?.focus()
|
||||||
} else {
|
|
||||||
setFocus()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -37,8 +37,8 @@ import { isMobileView } from '@/composables/settings'
|
|||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
actions: {
|
actions: {
|
||||||
type: Object,
|
type: [Object, Array, undefined],
|
||||||
required: true,
|
default: () => [],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -85,7 +85,7 @@ const groupedActions = computed(() => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
_actions = _actions.concat(
|
_actions = _actions.concat(
|
||||||
props.actions.filter((action) => action.group && !action.buttonLabel)
|
props.actions.filter((action) => action.group && !action.buttonLabel),
|
||||||
)
|
)
|
||||||
return _actions
|
return _actions
|
||||||
})
|
})
|
||||||
|
|||||||
@ -12,7 +12,10 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-if="field.read_only && field.fieldtype !== 'Check'"
|
v-if="
|
||||||
|
field.read_only &&
|
||||||
|
!['Float', 'Currency', 'Check'].includes(field.fieldtype)
|
||||||
|
"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="getPlaceholder(field)"
|
:placeholder="getPlaceholder(field)"
|
||||||
v-model="data[field.fieldname]"
|
v-model="data[field.fieldname]"
|
||||||
@ -23,6 +26,7 @@
|
|||||||
v-model="data[field.fieldname]"
|
v-model="data[field.fieldname]"
|
||||||
:doctype="field.options"
|
:doctype="field.options"
|
||||||
:parentDoctype="doctype"
|
:parentDoctype="doctype"
|
||||||
|
:parentFieldname="field.fieldname"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-else-if="field.fieldtype === 'Select'"
|
v-else-if="field.fieldtype === 'Select'"
|
||||||
@ -31,6 +35,7 @@
|
|||||||
:class="field.prefix ? 'prefix' : ''"
|
:class="field.prefix ? 'prefix' : ''"
|
||||||
:options="field.options"
|
:options="field.options"
|
||||||
v-model="data[field.fieldname]"
|
v-model="data[field.fieldname]"
|
||||||
|
@change="(e) => fieldChange(e.target.value, field)"
|
||||||
:placeholder="getPlaceholder(field)"
|
:placeholder="getPlaceholder(field)"
|
||||||
>
|
>
|
||||||
<template v-if="field.prefix" #prefix>
|
<template v-if="field.prefix" #prefix>
|
||||||
@ -42,7 +47,7 @@
|
|||||||
class="form-control"
|
class="form-control"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
v-model="data[field.fieldname]"
|
v-model="data[field.fieldname]"
|
||||||
@change="(e) => (data[field.fieldname] = e.target.checked)"
|
@change="(e) => fieldChange(e.target.checked, field)"
|
||||||
:disabled="Boolean(field.read_only)"
|
:disabled="Boolean(field.read_only)"
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
@ -70,7 +75,7 @@
|
|||||||
field.fieldtype == 'Link' ? field.options : data[field.options]
|
field.fieldtype == 'Link' ? field.options : data[field.options]
|
||||||
"
|
"
|
||||||
:filters="field.filters"
|
:filters="field.filters"
|
||||||
@change="(v) => (data[field.fieldname] = v)"
|
@change="(v) => fieldChange(v, field)"
|
||||||
:placeholder="getPlaceholder(field)"
|
:placeholder="getPlaceholder(field)"
|
||||||
:onCreate="field.create"
|
:onCreate="field.create"
|
||||||
/>
|
/>
|
||||||
@ -90,6 +95,7 @@
|
|||||||
v-else-if="field.fieldtype === 'Table MultiSelect'"
|
v-else-if="field.fieldtype === 'Table MultiSelect'"
|
||||||
v-model="data[field.fieldname]"
|
v-model="data[field.fieldname]"
|
||||||
:doctype="field.options"
|
:doctype="field.options"
|
||||||
|
@change="(v) => fieldChange(v, field)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
@ -98,7 +104,7 @@
|
|||||||
:value="data[field.fieldname] && getUser(data[field.fieldname]).full_name"
|
:value="data[field.fieldname] && getUser(data[field.fieldname]).full_name"
|
||||||
:doctype="field.options"
|
:doctype="field.options"
|
||||||
:filters="field.filters"
|
:filters="field.filters"
|
||||||
@change="(v) => (data[field.fieldname] = v)"
|
@change="(v) => fieldChange(v, field)"
|
||||||
:placeholder="getPlaceholder(field)"
|
:placeholder="getPlaceholder(field)"
|
||||||
:hideMe="true"
|
:hideMe="true"
|
||||||
>
|
>
|
||||||
@ -123,19 +129,21 @@
|
|||||||
</Link>
|
</Link>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
v-else-if="field.fieldtype === 'Datetime'"
|
v-else-if="field.fieldtype === 'Datetime'"
|
||||||
v-model="data[field.fieldname]"
|
:value="data[field.fieldname]"
|
||||||
icon-left=""
|
icon-left=""
|
||||||
:formatter="(date) => getFormat(date, '', true, true)"
|
:formatter="(date) => getFormat(date, '', true, true)"
|
||||||
:placeholder="getPlaceholder(field)"
|
:placeholder="getPlaceholder(field)"
|
||||||
input-class="border-none"
|
input-class="border-none"
|
||||||
|
@change="(v) => fieldChange(v, field)"
|
||||||
/>
|
/>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
v-else-if="field.fieldtype === 'Date'"
|
v-else-if="field.fieldtype === 'Date'"
|
||||||
icon-left=""
|
icon-left=""
|
||||||
v-model="data[field.fieldname]"
|
:value="data[field.fieldname]"
|
||||||
:formatter="(date) => getFormat(date, '', true)"
|
:formatter="(date) => getFormat(date, '', true)"
|
||||||
:placeholder="getPlaceholder(field)"
|
:placeholder="getPlaceholder(field)"
|
||||||
input-class="border-none"
|
input-class="border-none"
|
||||||
|
@change="(v) => fieldChange(v, field)"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-else-if="
|
v-else-if="
|
||||||
@ -143,13 +151,15 @@
|
|||||||
"
|
"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:placeholder="getPlaceholder(field)"
|
:placeholder="getPlaceholder(field)"
|
||||||
v-model="data[field.fieldname]"
|
:value="data[field.fieldname]"
|
||||||
|
@change="fieldChange($event.target.value, field)"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-else-if="['Int'].includes(field.fieldtype)"
|
v-else-if="['Int'].includes(field.fieldtype)"
|
||||||
type="number"
|
type="number"
|
||||||
:placeholder="getPlaceholder(field)"
|
:placeholder="getPlaceholder(field)"
|
||||||
v-model="data[field.fieldname]"
|
:value="data[field.fieldname]"
|
||||||
|
@change="fieldChange($event.target.value, field)"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-else-if="field.fieldtype === 'Percent'"
|
v-else-if="field.fieldtype === 'Percent'"
|
||||||
@ -157,7 +167,7 @@
|
|||||||
:value="getFormattedPercent(field.fieldname, data)"
|
:value="getFormattedPercent(field.fieldname, data)"
|
||||||
:placeholder="getPlaceholder(field)"
|
:placeholder="getPlaceholder(field)"
|
||||||
:disabled="Boolean(field.read_only)"
|
:disabled="Boolean(field.read_only)"
|
||||||
@change="data[field.fieldname] = flt($event.target.value)"
|
@change="fieldChange(flt($event.target.value), field)"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-else-if="field.fieldtype === 'Float'"
|
v-else-if="field.fieldtype === 'Float'"
|
||||||
@ -165,7 +175,7 @@
|
|||||||
:value="getFormattedFloat(field.fieldname, data)"
|
:value="getFormattedFloat(field.fieldname, data)"
|
||||||
:placeholder="getPlaceholder(field)"
|
:placeholder="getPlaceholder(field)"
|
||||||
:disabled="Boolean(field.read_only)"
|
:disabled="Boolean(field.read_only)"
|
||||||
@change="data[field.fieldname] = flt($event.target.value)"
|
@change="fieldChange(flt($event.target.value), field)"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-else-if="field.fieldtype === 'Currency'"
|
v-else-if="field.fieldtype === 'Currency'"
|
||||||
@ -173,14 +183,15 @@
|
|||||||
:value="getFormattedCurrency(field.fieldname, data)"
|
:value="getFormattedCurrency(field.fieldname, data)"
|
||||||
:placeholder="getPlaceholder(field)"
|
:placeholder="getPlaceholder(field)"
|
||||||
:disabled="Boolean(field.read_only)"
|
:disabled="Boolean(field.read_only)"
|
||||||
@change="data[field.fieldname] = flt($event.target.value)"
|
@change="fieldChange(flt($event.target.value), field)"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-else
|
v-else
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="getPlaceholder(field)"
|
:placeholder="getPlaceholder(field)"
|
||||||
v-model="data[field.fieldname]"
|
:value="data[field.fieldname]"
|
||||||
:disabled="Boolean(field.read_only)"
|
:disabled="Boolean(field.read_only)"
|
||||||
|
@change="fieldChange($event.target.value, field)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -195,8 +206,9 @@ import { getFormat, evaluateDependsOnValue } from '@/utils'
|
|||||||
import { flt } from '@/utils/numberFormat.js'
|
import { flt } from '@/utils/numberFormat.js'
|
||||||
import { getMeta } from '@/stores/meta'
|
import { getMeta } from '@/stores/meta'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
|
import { useDocument } from '@/data/document'
|
||||||
import { Tooltip, DatePicker, DateTimePicker } from 'frappe-ui'
|
import { Tooltip, DatePicker, DateTimePicker } from 'frappe-ui'
|
||||||
import { computed, inject } from 'vue'
|
import { computed, provide, inject } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
field: Object,
|
field: Object,
|
||||||
@ -205,11 +217,30 @@ const props = defineProps({
|
|||||||
const data = inject('data')
|
const data = inject('data')
|
||||||
const doctype = inject('doctype')
|
const doctype = inject('doctype')
|
||||||
const preview = inject('preview')
|
const preview = inject('preview')
|
||||||
|
const isGridRow = inject('isGridRow')
|
||||||
|
|
||||||
const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } =
|
const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } =
|
||||||
getMeta(doctype)
|
getMeta(doctype)
|
||||||
|
|
||||||
const { getUser } = usersStore()
|
const { getUser } = usersStore()
|
||||||
|
|
||||||
|
let triggerOnChange
|
||||||
|
|
||||||
|
if (!isGridRow) {
|
||||||
|
const {
|
||||||
|
triggerOnChange: trigger,
|
||||||
|
triggerOnRowAdd,
|
||||||
|
triggerOnRowRemove,
|
||||||
|
} = useDocument(doctype, data.value.name)
|
||||||
|
triggerOnChange = trigger
|
||||||
|
|
||||||
|
provide('triggerOnChange', triggerOnChange)
|
||||||
|
provide('triggerOnRowAdd', triggerOnRowAdd)
|
||||||
|
provide('triggerOnRowRemove', triggerOnRowRemove)
|
||||||
|
} else {
|
||||||
|
triggerOnChange = inject('triggerOnChange')
|
||||||
|
}
|
||||||
|
|
||||||
const field = computed(() => {
|
const field = computed(() => {
|
||||||
let field = props.field
|
let field = props.field
|
||||||
if (field.fieldtype == 'Select' && typeof field.options === 'string') {
|
if (field.fieldtype == 'Select' && typeof field.options === 'string') {
|
||||||
@ -265,6 +296,16 @@ const getPlaceholder = (field) => {
|
|||||||
return __('Enter {0}', [__(field.label)])
|
return __('Enter {0}', [__(field.label)])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fieldChange(value, df) {
|
||||||
|
data.value[df.fieldname] = value
|
||||||
|
|
||||||
|
if (isGridRow) {
|
||||||
|
triggerOnChange(df.fieldname, data.value)
|
||||||
|
} else {
|
||||||
|
triggerOnChange(df.fieldname)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
:deep(.form-control.prefix select) {
|
:deep(.form-control.prefix select) {
|
||||||
|
|||||||
@ -34,6 +34,10 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'CRM Lead',
|
default: 'CRM Lead',
|
||||||
},
|
},
|
||||||
|
isGridRow: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
preview: {
|
preview: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
@ -55,6 +59,7 @@ provide(
|
|||||||
provide('hasTabs', hasTabs)
|
provide('hasTabs', hasTabs)
|
||||||
provide('doctype', props.doctype)
|
provide('doctype', props.doctype)
|
||||||
provide('preview', props.preview)
|
provide('preview', props.preview)
|
||||||
|
provide('isGridRow', props.isGridRow)
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.section:not(:has(.field)) {
|
.section:not(:has(.field)) {
|
||||||
|
|||||||
@ -101,6 +101,7 @@
|
|||||||
v-model="settings.doc.dropdown_items"
|
v-model="settings.doc.dropdown_items"
|
||||||
doctype="CRM Dropdown Item"
|
doctype="CRM Dropdown Item"
|
||||||
parentDoctype="FCRM Settings"
|
parentDoctype="FCRM Settings"
|
||||||
|
parentFieldname="dropdown_items"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="sections flex flex-col overflow-y-auto">
|
<div
|
||||||
|
v-if="!document.get.loading"
|
||||||
|
class="sections flex flex-col overflow-y-auto"
|
||||||
|
>
|
||||||
<template v-for="(section, i) in _sections" :key="section.name">
|
<template v-for="(section, i) in _sections" :key="section.name">
|
||||||
<div v-if="section.visible" class="section flex flex-col">
|
<div v-if="section.visible" class="section flex flex-col">
|
||||||
<div
|
<div
|
||||||
@ -67,21 +70,21 @@
|
|||||||
class="flex h-7 cursor-pointer items-center px-2 py-1 text-ink-gray-5"
|
class="flex h-7 cursor-pointer items-center px-2 py-1 text-ink-gray-5"
|
||||||
>
|
>
|
||||||
<Tooltip :text="__(field.tooltip)">
|
<Tooltip :text="__(field.tooltip)">
|
||||||
<div>{{ data[field.fieldname] }}</div>
|
<div>{{ document.doc[field.fieldname] }}</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="field.fieldtype === 'Dropdown'">
|
<div v-else-if="field.fieldtype === 'Dropdown'">
|
||||||
<NestedPopover>
|
<NestedPopover>
|
||||||
<template #target="{ open }">
|
<template #target="{ open }">
|
||||||
<Button
|
<Button
|
||||||
:label="data[field.fieldname]"
|
:label="document.doc[field.fieldname]"
|
||||||
class="dropdown-button flex w-full items-center justify-between rounded border border-gray-100 bg-surface-gray-2 px-2 py-1.5 text-base text-ink-gray-8 placeholder-ink-gray-4 transition-colors hover:border-outline-gray-modals hover:bg-surface-gray-3 focus:border-outline-gray-4 focus:bg-surface-white focus:shadow-sm focus:outline-none focus:ring-0 focus-visible:ring-2 focus-visible:ring-outline-gray-3"
|
class="dropdown-button flex w-full items-center justify-between rounded border border-gray-100 bg-surface-gray-2 px-2 py-1.5 text-base text-ink-gray-8 placeholder-ink-gray-4 transition-colors hover:border-outline-gray-modals hover:bg-surface-gray-3 focus:border-outline-gray-4 focus:bg-surface-white focus:shadow-sm focus:outline-none focus:ring-0 focus-visible:ring-2 focus-visible:ring-outline-gray-3"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="data[field.fieldname]"
|
v-if="document.doc[field.fieldname]"
|
||||||
class="truncate"
|
class="truncate"
|
||||||
>
|
>
|
||||||
{{ data[field.fieldname] }}
|
{{ document.doc[field.fieldname] }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
@ -138,13 +141,9 @@
|
|||||||
v-else-if="field.fieldtype == 'Check'"
|
v-else-if="field.fieldtype == 'Check'"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
v-model="data[field.fieldname]"
|
v-model="document.doc[field.fieldname]"
|
||||||
@change.stop="
|
@change.stop="
|
||||||
emit(
|
fieldChange($event.target.checked, field)
|
||||||
'update',
|
|
||||||
field.fieldname,
|
|
||||||
$event.target.checked,
|
|
||||||
)
|
|
||||||
"
|
"
|
||||||
:disabled="Boolean(field.read_only)"
|
:disabled="Boolean(field.read_only)"
|
||||||
/>
|
/>
|
||||||
@ -159,43 +158,40 @@
|
|||||||
"
|
"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:value="data[field.fieldname]"
|
:value="document.doc[field.fieldname]"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
:debounce="500"
|
:debounce="500"
|
||||||
@change.stop="
|
@change.stop="fieldChange($event.target.value, field)"
|
||||||
emit('update', field.fieldname, $event.target.value)
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-else-if="field.fieldtype === 'Select'"
|
v-else-if="field.fieldtype === 'Select'"
|
||||||
class="form-control cursor-pointer [&_select]:cursor-pointer truncate"
|
class="form-control cursor-pointer [&_select]:cursor-pointer truncate"
|
||||||
type="select"
|
type="select"
|
||||||
v-model="data[field.fieldname]"
|
v-model="document.doc[field.fieldname]"
|
||||||
:options="field.options"
|
:options="field.options"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
@change.stop="
|
@change.stop="fieldChange($event.target.value, field)"
|
||||||
emit('update', field.fieldname, $event.target.value)
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
<Link
|
<Link
|
||||||
v-else-if="field.fieldtype === 'User'"
|
v-else-if="field.fieldtype === 'User'"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
:value="
|
:value="
|
||||||
data[field.fieldname] &&
|
document.doc[field.fieldname] &&
|
||||||
getUser(data[field.fieldname]).full_name
|
getUser(document.doc[field.fieldname]).full_name
|
||||||
"
|
"
|
||||||
doctype="User"
|
doctype="User"
|
||||||
:filters="field.filters"
|
:filters="field.filters"
|
||||||
@change="
|
@change="(v) => fieldChange(v, field)"
|
||||||
(data) => emit('update', field.fieldname, data)
|
|
||||||
"
|
|
||||||
:placeholder="'Select' + ' ' + field.label + '...'"
|
:placeholder="'Select' + ' ' + field.label + '...'"
|
||||||
:hideMe="true"
|
:hideMe="true"
|
||||||
>
|
>
|
||||||
<template v-if="data[field.fieldname]" #prefix>
|
<template
|
||||||
|
v-if="document.doc[field.fieldname]"
|
||||||
|
#prefix
|
||||||
|
>
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
class="mr-1.5"
|
class="mr-1.5"
|
||||||
:user="data[field.fieldname]"
|
:user="document.doc[field.fieldname]"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@ -219,17 +215,15 @@
|
|||||||
['Link', 'Dynamic Link'].includes(field.fieldtype)
|
['Link', 'Dynamic Link'].includes(field.fieldtype)
|
||||||
"
|
"
|
||||||
class="form-control select-text"
|
class="form-control select-text"
|
||||||
:value="data[field.fieldname]"
|
:value="document.doc[field.fieldname]"
|
||||||
:doctype="
|
:doctype="
|
||||||
field.fieldtype == 'Link'
|
field.fieldtype == 'Link'
|
||||||
? field.options
|
? field.options
|
||||||
: data[field.options]
|
: document.doc[field.options]
|
||||||
"
|
"
|
||||||
:filters="field.filters"
|
:filters="field.filters"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
@change="
|
@change="(v) => fieldChange(v, field)"
|
||||||
(data) => emit('update', field.fieldname, data)
|
|
||||||
"
|
|
||||||
:onCreate="field.create"
|
:onCreate="field.create"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
@ -238,15 +232,13 @@
|
|||||||
>
|
>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
icon-left=""
|
icon-left=""
|
||||||
:value="data[field.fieldname]"
|
:value="document.doc[field.fieldname]"
|
||||||
:formatter="
|
:formatter="
|
||||||
(date) => getFormat(date, '', true, true)
|
(date) => getFormat(date, '', true, true)
|
||||||
"
|
"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
placement="left-start"
|
placement="left-start"
|
||||||
@change="
|
@change="(v) => fieldChange(v, field)"
|
||||||
(data) => emit('update', field.fieldname, data)
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@ -255,81 +247,69 @@
|
|||||||
>
|
>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
icon-left=""
|
icon-left=""
|
||||||
:value="data[field.fieldname]"
|
:value="document.doc[field.fieldname]"
|
||||||
:formatter="(date) => getFormat(date, '', true)"
|
:formatter="(date) => getFormat(date, '', true)"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
placement="left-start"
|
placement="left-start"
|
||||||
@change="
|
@change="(v) => fieldChange(v, field)"
|
||||||
(data) => emit('update', field.fieldname, data)
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-else-if="field.fieldtype === 'Percent'"
|
v-else-if="field.fieldtype === 'Percent'"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
type="text"
|
type="text"
|
||||||
:value="getFormattedPercent(field.fieldname, data)"
|
:value="
|
||||||
|
getFormattedPercent(field.fieldname, document.doc)
|
||||||
|
"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
:debounce="500"
|
:debounce="500"
|
||||||
@change.stop="
|
@change.stop="
|
||||||
emit(
|
fieldChange(flt($event.target.value), field)
|
||||||
'update',
|
|
||||||
field.fieldname,
|
|
||||||
flt($event.target.value),
|
|
||||||
)
|
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-else-if="field.fieldtype === 'Int'"
|
v-else-if="field.fieldtype === 'Int'"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
type="number"
|
type="number"
|
||||||
v-model="data[field.fieldname]"
|
v-model="document.doc[field.fieldname]"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
:debounce="500"
|
:debounce="500"
|
||||||
@change.stop="
|
@change.stop="fieldChange($event.target.value, field)"
|
||||||
emit('update', field.fieldname, $event.target.value)
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-else-if="field.fieldtype === 'Float'"
|
v-else-if="field.fieldtype === 'Float'"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
type="text"
|
type="text"
|
||||||
:value="getFormattedFloat(field.fieldname, data)"
|
:value="
|
||||||
|
getFormattedFloat(field.fieldname, document.doc)
|
||||||
|
"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
:debounce="500"
|
:debounce="500"
|
||||||
@change.stop="
|
@change.stop="
|
||||||
emit(
|
fieldChange(flt($event.target.value), field)
|
||||||
'update',
|
|
||||||
field.fieldname,
|
|
||||||
flt($event.target.value),
|
|
||||||
)
|
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-else-if="field.fieldtype === 'Currency'"
|
v-else-if="field.fieldtype === 'Currency'"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
type="text"
|
type="text"
|
||||||
:value="getFormattedCurrency(field.fieldname, data)"
|
:value="
|
||||||
|
getFormattedCurrency(field.fieldname, document.doc)
|
||||||
|
"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
:debounce="500"
|
:debounce="500"
|
||||||
@change.stop="
|
@change.stop="
|
||||||
emit(
|
fieldChange(flt($event.target.value), field)
|
||||||
'update',
|
|
||||||
field.fieldname,
|
|
||||||
flt($event.target.value),
|
|
||||||
)
|
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-else
|
v-else
|
||||||
class="form-control"
|
class="form-control"
|
||||||
type="text"
|
type="text"
|
||||||
:value="data[field.fieldname]"
|
:value="document.doc[field.fieldname]"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
:debounce="500"
|
:debounce="500"
|
||||||
@change.stop="
|
@change.stop="fieldChange($event.target.value, field)"
|
||||||
emit('update', field.fieldname, $event.target.value)
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-1">
|
<div class="ml-1">
|
||||||
@ -337,19 +317,23 @@
|
|||||||
v-if="
|
v-if="
|
||||||
field.fieldtype === 'Link' &&
|
field.fieldtype === 'Link' &&
|
||||||
field.link &&
|
field.link &&
|
||||||
data[field.fieldname]
|
document.doc[field.fieldname]
|
||||||
"
|
"
|
||||||
class="h-4 w-4 shrink-0 cursor-pointer text-ink-gray-5 hover:text-ink-gray-8"
|
class="h-4 w-4 shrink-0 cursor-pointer text-ink-gray-5 hover:text-ink-gray-8"
|
||||||
@click.stop="field.link(data[field.fieldname])"
|
@click.stop="
|
||||||
|
field.link(document.doc[field.fieldname])
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
<EditIcon
|
<EditIcon
|
||||||
v-if="
|
v-if="
|
||||||
field.fieldtype === 'Link' &&
|
field.fieldtype === 'Link' &&
|
||||||
field.edit &&
|
field.edit &&
|
||||||
data[field.fieldname]
|
document.doc[field.fieldname]
|
||||||
"
|
"
|
||||||
class="size-3.5 shrink-0 cursor-pointer text-ink-gray-5 hover:text-ink-gray-8"
|
class="size-3.5 shrink-0 cursor-pointer text-ink-gray-5 hover:text-ink-gray-8"
|
||||||
@click.stop="field.edit(data[field.fieldname])"
|
@click.stop="
|
||||||
|
field.edit(document.doc[field.fieldname])
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -386,6 +370,7 @@ import { isMobileView } from '@/composables/settings'
|
|||||||
import { getFormat, evaluateDependsOnValue } from '@/utils'
|
import { getFormat, evaluateDependsOnValue } from '@/utils'
|
||||||
import { flt } from '@/utils/numberFormat.js'
|
import { flt } from '@/utils/numberFormat.js'
|
||||||
import { Tooltip, DateTimePicker, DatePicker } from 'frappe-ui'
|
import { Tooltip, DateTimePicker, DatePicker } from 'frappe-ui'
|
||||||
|
import { useDocument } from '@/data/document'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -395,6 +380,11 @@ const props = defineProps({
|
|||||||
doctype: {
|
doctype: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'CRM Lead',
|
default: 'CRM Lead',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
docname: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
preview: {
|
preview: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -407,13 +397,15 @@ const props = defineProps({
|
|||||||
|
|
||||||
const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } =
|
const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } =
|
||||||
getMeta(props.doctype)
|
getMeta(props.doctype)
|
||||||
|
|
||||||
const { isManager, getUser } = usersStore()
|
const { isManager, getUser } = usersStore()
|
||||||
|
|
||||||
const emit = defineEmits(['update', 'reload'])
|
const emit = defineEmits(['update', 'reload'])
|
||||||
|
|
||||||
const data = defineModel()
|
|
||||||
const showSidePanelModal = ref(false)
|
const showSidePanelModal = ref(false)
|
||||||
|
|
||||||
|
const { document, triggerOnChange } = useDocument(props.doctype, props.docname)
|
||||||
|
|
||||||
const _sections = computed(() => {
|
const _sections = computed(() => {
|
||||||
if (!props.sections?.length) return []
|
if (!props.sections?.length) return []
|
||||||
let editButtonAdded = false
|
let editButtonAdded = false
|
||||||
@ -453,11 +445,11 @@ function parsedField(field) {
|
|||||||
placeholder: field.placeholder || field.label,
|
placeholder: field.placeholder || field.label,
|
||||||
display_via_depends_on: evaluateDependsOnValue(
|
display_via_depends_on: evaluateDependsOnValue(
|
||||||
field.depends_on,
|
field.depends_on,
|
||||||
data.value,
|
document.doc,
|
||||||
),
|
),
|
||||||
mandatory_via_depends_on: evaluateDependsOnValue(
|
mandatory_via_depends_on: evaluateDependsOnValue(
|
||||||
field.mandatory_depends_on,
|
field.mandatory_depends_on,
|
||||||
data.value,
|
document.doc,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -465,6 +457,14 @@ function parsedField(field) {
|
|||||||
return _field
|
return _field
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fieldChange(value, df) {
|
||||||
|
document.doc[df.fieldname] = value
|
||||||
|
|
||||||
|
await triggerOnChange(df.fieldname)
|
||||||
|
|
||||||
|
document.save.submit()
|
||||||
|
}
|
||||||
|
|
||||||
function parsedSection(section, editButtonAdded) {
|
function parsedSection(section, editButtonAdded) {
|
||||||
let isContactSection = section.name == 'contacts_section'
|
let isContactSection = section.name == 'contacts_section'
|
||||||
section.showEditButton = !(
|
section.showEditButton = !(
|
||||||
@ -485,7 +485,7 @@ function isFieldVisible(field) {
|
|||||||
if (props.preview) return true
|
if (props.preview) return true
|
||||||
return (
|
return (
|
||||||
(field.fieldtype == 'Check' ||
|
(field.fieldtype == 'Check' ||
|
||||||
(field.read_only && data.value[field.fieldname]) ||
|
(field.read_only && document.doc?.[field.fieldname]) ||
|
||||||
!field.read_only) &&
|
!field.read_only) &&
|
||||||
(!field.depends_on || field.display_via_depends_on) &&
|
(!field.depends_on || field.display_via_depends_on) &&
|
||||||
!field.hidden
|
!field.hidden
|
||||||
|
|||||||
136
frontend/src/data/document.js
Normal file
136
frontend/src/data/document.js
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import { getScript } from '@/data/script'
|
||||||
|
import { createToast, runSequentially } from '@/utils'
|
||||||
|
import { createDocumentResource } from 'frappe-ui'
|
||||||
|
|
||||||
|
const documentsCache = {}
|
||||||
|
const controllersCache = {}
|
||||||
|
|
||||||
|
export function useDocument(doctype, docname) {
|
||||||
|
const { setupScript } = getScript(doctype)
|
||||||
|
|
||||||
|
documentsCache[doctype] = documentsCache[doctype] || {}
|
||||||
|
|
||||||
|
if (!documentsCache[doctype][docname]) {
|
||||||
|
documentsCache[doctype][docname] = createDocumentResource({
|
||||||
|
doctype: doctype,
|
||||||
|
name: docname,
|
||||||
|
onSuccess: () => setupFormScript(),
|
||||||
|
setValue: {
|
||||||
|
onSuccess: () => {
|
||||||
|
createToast({
|
||||||
|
title: __('Document updated successfully'),
|
||||||
|
icon: 'check',
|
||||||
|
iconClasses: 'text-ink-green-3',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
createToast({
|
||||||
|
title: __('Error updating document'),
|
||||||
|
text: err.messages[0],
|
||||||
|
icon: 'x',
|
||||||
|
iconClasses: 'text-red-600',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupFormScript() {
|
||||||
|
if (controllersCache[doctype]) return
|
||||||
|
|
||||||
|
controllersCache[doctype] = setupScript(documentsCache[doctype][docname])
|
||||||
|
}
|
||||||
|
|
||||||
|
function getControllers(row = null) {
|
||||||
|
const _doctype = row?.doctype || doctype
|
||||||
|
return (controllersCache[doctype] || []).filter(
|
||||||
|
(c) => c.constructor.name === _doctype.replace(/\s+/g, ''),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function triggerOnRefresh() {
|
||||||
|
const handler = async function () {
|
||||||
|
await this.refresh()
|
||||||
|
}
|
||||||
|
await trigger(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function triggerOnChange(fieldname, row) {
|
||||||
|
const handler = async function () {
|
||||||
|
if (row) {
|
||||||
|
this.currentRowIdx = row.idx
|
||||||
|
this.value = row[fieldname]
|
||||||
|
this.oldValue = getOldValue(fieldname, row)
|
||||||
|
} else {
|
||||||
|
this.value = documentsCache[doctype][docname].doc[fieldname]
|
||||||
|
this.oldValue = getOldValue(fieldname)
|
||||||
|
}
|
||||||
|
await this[fieldname]?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
await trigger(handler, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function triggerOnRowAdd(row) {
|
||||||
|
const handler = async function () {
|
||||||
|
this.currentRowIdx = row.idx
|
||||||
|
this.value = row
|
||||||
|
await this[row.parentfield + '_add']?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
await trigger(handler, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function triggerOnRowRemove(selectedRows, rows) {
|
||||||
|
const handler = async function () {
|
||||||
|
if (selectedRows.size === 1) {
|
||||||
|
const selectedRow = Array.from(selectedRows)[0]
|
||||||
|
this.currentRowIdx = rows.find((r) => r.name === selectedRow).idx
|
||||||
|
} else {
|
||||||
|
delete this.currentRowIdx
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedRows = Array.from(selectedRows)
|
||||||
|
this.rows = rows
|
||||||
|
|
||||||
|
await this[rows[0].parentfield + '_remove']?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
await trigger(handler, rows[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
async function trigger(taskFn, row = null) {
|
||||||
|
const controllers = getControllers(row)
|
||||||
|
if (!controllers.length) return
|
||||||
|
|
||||||
|
const tasks = controllers.map(
|
||||||
|
(controller) => async () => await taskFn.call(controller),
|
||||||
|
)
|
||||||
|
|
||||||
|
await runSequentially(tasks)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOldValue(fieldname, row) {
|
||||||
|
if (!documentsCache[doctype][docname]) return ''
|
||||||
|
|
||||||
|
const document = documentsCache[doctype][docname]
|
||||||
|
const oldDoc = document.originalDoc
|
||||||
|
|
||||||
|
if (row?.name) {
|
||||||
|
return oldDoc?.[row.parentfield]?.find((r) => r.name === row.name)?.[
|
||||||
|
fieldname
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return oldDoc?.[fieldname] || document.doc[fieldname]
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
document: documentsCache[doctype][docname],
|
||||||
|
triggerOnChange,
|
||||||
|
triggerOnRowAdd,
|
||||||
|
triggerOnRowRemove,
|
||||||
|
triggerOnRefresh,
|
||||||
|
setupFormScript,
|
||||||
|
}
|
||||||
|
}
|
||||||
257
frontend/src/data/script.js
Normal file
257
frontend/src/data/script.js
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
import { globalStore } from '@/stores/global'
|
||||||
|
import { getMeta } from '@/stores/meta'
|
||||||
|
import { createToast } from '@/utils'
|
||||||
|
import { call, createListResource } from 'frappe-ui'
|
||||||
|
import { reactive } from 'vue'
|
||||||
|
import router from '@/router'
|
||||||
|
|
||||||
|
const doctypeScripts = reactive({})
|
||||||
|
|
||||||
|
export function getScript(doctype, view = 'Form') {
|
||||||
|
const scripts = createListResource({
|
||||||
|
doctype: 'CRM Form Script',
|
||||||
|
cache: ['Form Scripts', doctype, view],
|
||||||
|
fields: ['name', 'dt', 'view', 'script'],
|
||||||
|
filters: { view, dt: doctype, enabled: 1 },
|
||||||
|
onSuccess: (_scripts) => {
|
||||||
|
for (let script of _scripts) {
|
||||||
|
if (!doctypeScripts[doctype]) {
|
||||||
|
doctypeScripts[doctype] = {}
|
||||||
|
}
|
||||||
|
doctypeScripts[doctype][script.name] = script || {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!doctypeScripts[doctype] && !scripts.loading) {
|
||||||
|
scripts.fetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupScript(document, helpers = {}) {
|
||||||
|
let scripts = doctypeScripts[doctype]
|
||||||
|
if (!scripts) return null
|
||||||
|
|
||||||
|
const { $dialog, $socket, makeCall } = globalStore()
|
||||||
|
|
||||||
|
helpers.createDialog = $dialog
|
||||||
|
helpers.createToast = createToast
|
||||||
|
helpers.socket = $socket
|
||||||
|
helpers.router = router
|
||||||
|
helpers.call = call
|
||||||
|
|
||||||
|
helpers.crm = {
|
||||||
|
makePhoneCall: makeCall,
|
||||||
|
}
|
||||||
|
|
||||||
|
return setupMultipleFormControllers(scripts, document, helpers)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupMultipleFormControllers(scriptStrings, document, helpers) {
|
||||||
|
const controllers = []
|
||||||
|
let parentInstanceIdx = null
|
||||||
|
|
||||||
|
for (let scriptName in scriptStrings) {
|
||||||
|
let script = scriptStrings[scriptName]?.script
|
||||||
|
if (!script) continue
|
||||||
|
try {
|
||||||
|
const classNames = getClassNames(script)
|
||||||
|
if (!classNames) continue
|
||||||
|
|
||||||
|
classNames.forEach((className) => {
|
||||||
|
const FormClass = evaluateFormClass(script, className, helpers)
|
||||||
|
if (!FormClass) return
|
||||||
|
|
||||||
|
let parentInstance = null
|
||||||
|
let doctypeName = doctype.replace(/\s+/g, '')
|
||||||
|
|
||||||
|
let { doctypeMeta } = getMeta(doctype)
|
||||||
|
|
||||||
|
// if className is not doctype name, then it is a child doctype
|
||||||
|
let isChildDoctype = className !== doctypeName
|
||||||
|
|
||||||
|
if (isChildDoctype) {
|
||||||
|
if (!controllers.length) {
|
||||||
|
console.error(
|
||||||
|
__(
|
||||||
|
'⚠️ No class found for doctype: {0}, it is mandatory to have a class for the parent doctype. it can be empty, but it should be present.',
|
||||||
|
[doctype],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parentInstance = controllers[parentInstanceIdx]
|
||||||
|
} else {
|
||||||
|
parentInstanceIdx = controllers.length || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = setupFormController(
|
||||||
|
FormClass,
|
||||||
|
doctypeMeta,
|
||||||
|
document,
|
||||||
|
parentInstance,
|
||||||
|
isChildDoctype,
|
||||||
|
)
|
||||||
|
|
||||||
|
controllers.push(instance)
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.error(__('Failed to load form controller: {0}', [err]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return controllers
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupFormController(
|
||||||
|
FormClass,
|
||||||
|
meta,
|
||||||
|
document,
|
||||||
|
parentInstance = null,
|
||||||
|
isChildDoctype = false,
|
||||||
|
) {
|
||||||
|
let instance = new FormClass()
|
||||||
|
|
||||||
|
for (const key in document) {
|
||||||
|
if (document.hasOwnProperty(key)) {
|
||||||
|
instance[key] = document[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
instance.getMeta = async (doctype) => {
|
||||||
|
if (!meta[doctype]) {
|
||||||
|
await getMeta(doctype)
|
||||||
|
return meta[doctype]
|
||||||
|
}
|
||||||
|
return meta[doctype]
|
||||||
|
}
|
||||||
|
|
||||||
|
setupHelperMethods(FormClass, instance, parentInstance, document)
|
||||||
|
|
||||||
|
if (isChildDoctype) {
|
||||||
|
instance.doc = createDocProxy(document.doc, parentInstance)
|
||||||
|
} else {
|
||||||
|
instance.doc = createDocProxy(document.doc, instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupHelperMethods(FormClass, instance, parentInstance, document) {
|
||||||
|
if (typeof FormClass.prototype.getRow !== 'function') {
|
||||||
|
FormClass.prototype.getRow = (parentField, idx) =>
|
||||||
|
getRow(parentField, idx, document.doc, instance)
|
||||||
|
}
|
||||||
|
exposeHiddenMethods(instance, parentInstance, ['getRow'])
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRow(parentField, idx, data, instance) {
|
||||||
|
idx = idx || instance.currentRowIdx
|
||||||
|
|
||||||
|
if (!data[parentField]) {
|
||||||
|
console.warn(__('⚠️ No data found for parent field: {0}', [parentField]))
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const row = data[parentField].find((r) => r.idx === idx)
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
console.warn(
|
||||||
|
__('⚠️ No row found for idx: {0} in parent field: {1}', [
|
||||||
|
idx,
|
||||||
|
parentField,
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
row.parent = row.parent || data.name
|
||||||
|
|
||||||
|
return createDocProxy(row, instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
// utility function to setup a form controller
|
||||||
|
function getClassNames(script) {
|
||||||
|
const withoutComments = script
|
||||||
|
.replace(/\/\/.*$/gm, '') // Remove single-line comments
|
||||||
|
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove multi-line comments
|
||||||
|
|
||||||
|
// Match class declarations
|
||||||
|
return (
|
||||||
|
[...withoutComments.matchAll(/class\s+([A-Za-z0-9_]+)/g)].map(
|
||||||
|
(match) => match[1],
|
||||||
|
) || []
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateFormClass(script, className, helpers = {}) {
|
||||||
|
const helperKeys = Object.keys(helpers)
|
||||||
|
const helperValues = Object.values(helpers)
|
||||||
|
|
||||||
|
const wrappedScript = `
|
||||||
|
${script}
|
||||||
|
return ${className};
|
||||||
|
`
|
||||||
|
|
||||||
|
const FormClass = new Function(...helperKeys, wrappedScript)(
|
||||||
|
...helperValues,
|
||||||
|
)
|
||||||
|
return FormClass
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDocProxy(data, instance) {
|
||||||
|
return new Proxy(data, {
|
||||||
|
get(target, prop) {
|
||||||
|
if (prop === 'trigger') {
|
||||||
|
if ('trigger' in data) {
|
||||||
|
console.warn(
|
||||||
|
__(
|
||||||
|
'⚠️ Avoid using "trigger" as a field name — it conflicts with the built-in trigger() method.',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (methodName, ...args) => {
|
||||||
|
const method = instance[methodName]
|
||||||
|
if (typeof method === 'function') {
|
||||||
|
return method.apply(instance, args)
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
__('⚠️ Method "{0}" not found in class.', [methodName]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return target[prop]
|
||||||
|
},
|
||||||
|
set(target, prop, value) {
|
||||||
|
target[prop] = value
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function exposeHiddenMethods(instance, parentInstance, methodNames = []) {
|
||||||
|
for (const name of methodNames) {
|
||||||
|
// remove the method from parent instance if it exists
|
||||||
|
if (parentInstance && parentInstance[name]) {
|
||||||
|
delete instance.doc[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof instance[name] === 'function' && !instance.doc[name]) {
|
||||||
|
// Show as actual method on doc, bound to instance
|
||||||
|
Object.defineProperty(instance.doc, name, {
|
||||||
|
value: (...args) => instance[name](...args),
|
||||||
|
writable: false,
|
||||||
|
enumerable: false,
|
||||||
|
configurable: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
scripts,
|
||||||
|
setupScript,
|
||||||
|
setupFormController,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -129,10 +129,10 @@
|
|||||||
class="flex flex-1 flex-col justify-between overflow-hidden"
|
class="flex flex-1 flex-col justify-between overflow-hidden"
|
||||||
>
|
>
|
||||||
<SidePanelLayout
|
<SidePanelLayout
|
||||||
v-model="deal.data"
|
|
||||||
:sections="sections.data"
|
:sections="sections.data"
|
||||||
:addContact="addContact"
|
:addContact="addContact"
|
||||||
doctype="CRM Deal"
|
doctype="CRM Deal"
|
||||||
|
:docname="deal.data.name"
|
||||||
@update="updateField"
|
@update="updateField"
|
||||||
@reload="sections.reload"
|
@reload="sections.reload"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -182,9 +182,9 @@
|
|||||||
class="flex flex-1 flex-col justify-between overflow-hidden"
|
class="flex flex-1 flex-col justify-between overflow-hidden"
|
||||||
>
|
>
|
||||||
<SidePanelLayout
|
<SidePanelLayout
|
||||||
v-model="lead.data"
|
|
||||||
:sections="sections.data"
|
:sections="sections.data"
|
||||||
doctype="CRM Lead"
|
doctype="CRM Lead"
|
||||||
|
:docname="lead.data.name"
|
||||||
@update="updateField"
|
@update="updateField"
|
||||||
@reload="sections.reload"
|
@reload="sections.reload"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -152,6 +152,7 @@ export function setupAssignees(doc) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getFormScript(script, obj) {
|
async function getFormScript(script, obj) {
|
||||||
|
if (!script.includes('setupForm(')) return {}
|
||||||
let scriptFn = new Function(script + '\nreturn setupForm')()
|
let scriptFn = new Function(script + '\nreturn setupForm')()
|
||||||
let formScript = await scriptFn(obj)
|
let formScript = await scriptFn(obj)
|
||||||
return formScript || {}
|
return formScript || {}
|
||||||
@ -348,3 +349,9 @@ export function getRandom(len = 4) {
|
|||||||
|
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function runSequentially(functions) {
|
||||||
|
return functions.reduce((promise, fn) => {
|
||||||
|
return promise.then(() => fn())
|
||||||
|
}, Promise.resolve())
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user