fix: send tabs instead of sections to FieldLayout component and render Tabs if exists

This commit is contained in:
Shariq Ansari 2024-12-06 14:25:49 +05:30
parent 851449bbc3
commit 38565a14f6
12 changed files with 351 additions and 303 deletions

View File

@ -2,6 +2,7 @@
# For license information, please see license.txt # For license information, please see license.txt
import json import json
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
@ -10,46 +11,54 @@ from frappe.model.document import Document
class CRMFieldsLayout(Document): class CRMFieldsLayout(Document):
pass pass
@frappe.whitelist() @frappe.whitelist()
def get_fields_layout(doctype: str, type: str): def get_fields_layout(doctype: str, type: str):
sections = [] tabs = []
if frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": type}): if frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": type}):
layout = frappe.get_doc("CRM Fields Layout", {"dt": doctype, "type": type}) layout = frappe.get_doc("CRM Fields Layout", {"dt": doctype, "type": type})
else: else:
return [] return []
if layout.layout: if layout.layout:
sections = json.loads(layout.layout) tabs = json.loads(layout.layout)
has_tabs = tabs[0].get("sections") if tabs and tabs[0] else False
if not has_tabs:
tabs = [{"no_tabs": True, "sections": tabs}]
allowed_fields = [] allowed_fields = []
for section in sections: for tab in tabs:
if not section.get("fields"): for section in tab.get("sections"):
continue if not section.get("fields"):
allowed_fields.extend(section.get("fields")) continue
allowed_fields.extend(section.get("fields"))
fields = frappe.get_meta(doctype).fields fields = frappe.get_meta(doctype).fields
fields = [field for field in fields if field.fieldname in allowed_fields] fields = [field for field in fields if field.fieldname in allowed_fields]
for section in sections: for tab in tabs:
for field in section.get("fields") if section.get("fields") else []: for section in tab.get("sections"):
field = next((f for f in fields if f.fieldname == field), None) for field in section.get("fields") if section.get("fields") else []:
if field: field = next((f for f in fields if f.fieldname == field), None)
if field.fieldtype == "Select" and field.options: if field:
field.options = field.options.split("\n") if field.fieldtype == "Select" and field.options:
field.options = [{"label": _(option), "value": option} for option in field.options] field.options = field.options.split("\n")
field.options.insert(0, {"label": "", "value": ""}) field.options = [{"label": _(option), "value": option} for option in field.options]
field = { field.options.insert(0, {"label": "", "value": ""})
"label": _(field.label), field = {
"name": field.fieldname, "label": _(field.label),
"type": field.fieldtype, "name": field.fieldname,
"options": field.options, "type": field.fieldtype,
"mandatory": field.reqd, "options": field.options,
"placeholder": field.get("placeholder"), "mandatory": field.reqd,
"filters": field.get("link_filters") "placeholder": field.get("placeholder"),
} "filters": field.get("link_filters"),
section["fields"][section.get("fields").index(field["name"])] = field }
section["fields"][section.get("fields").index(field["name"])] = field
return sections or [] return tabs or []
@frappe.whitelist() @frappe.whitelist()
@ -59,11 +68,13 @@ def save_fields_layout(doctype: str, type: str, layout: str):
else: else:
doc = frappe.new_doc("CRM Fields Layout") doc = frappe.new_doc("CRM Fields Layout")
doc.update({ doc.update(
"dt": doctype, {
"type": type, "dt": doctype,
"layout": layout, "type": type,
}) "layout": layout,
}
)
doc.save(ignore_permissions=True) doc.save(ignore_permissions=True)
return doc.layout return doc.layout

View File

@ -31,16 +31,8 @@
<LoadingIndicator class="h-6 w-6" /> <LoadingIndicator class="h-6 w-6" />
<span>{{ __('Loading...') }}</span> <span>{{ __('Loading...') }}</span>
</div> </div>
<div <div v-else>
v-else <FieldLayout v-if="tabs.data" :tabs="tabs.data" :data="data.doc" />
class="flex flex-col gap-3 border border-outline-gray-1 rounded-lg"
>
<FieldLayout
v-if="sections.data"
:sections="sections.data"
:data="data.doc"
:allowTabs="true"
/>
</div> </div>
<DataFieldsModal <DataFieldsModal
v-if="showDataFieldsModal" v-if="showDataFieldsModal"
@ -98,7 +90,7 @@ const data = createDocumentResource({
}, },
}) })
const sections = 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',
cache: ['DataFields', props.doctype], cache: ['DataFields', props.doctype],
params: { doctype: props.doctype, type: 'Data Fields' }, params: { doctype: props.doctype, type: 'Data Fields' },

View File

@ -1,180 +1,209 @@
<template> <template>
<div class="flex flex-col" :class="{ 'my-4 sm:my-6': allowTabs }"> <div
<div class="flex flex-col"
v-for="(section, i) in sections" :class="{
:key="section.label" 'border border-outline-gray-1 rounded-lg': hasTabs,
class="section first:border-t-0 border-outline-gray-modals first:pt-0" 'border-outline-gray-modals': modal && hasTabs,
}"
>
<Tabs
v-model="tabIndex"
class="!h-full"
:tabs="tabs"
v-slot="{ tab }"
:tablistClass="
!hasTabs ? 'hidden' : modal ? 'border-outline-gray-modals' : ''
"
> >
<div <div :class="{ 'my-4 sm:my-6': hasTabs }">
v-if="!section.hideBorder && i != 0"
class="h-px w-full border-t my-5"
/>
<Section
class="text-lg font-medium"
:class="{ 'px-3 sm:px-5': allowTabs }"
:label="section.label"
:hideLabel="section.hideLabel"
:opened="section.opened"
:collapsible="section.collapsible"
collapseIconPosition="right"
>
<div <div
class="grid gap-4 mt-6" v-for="(section, i) in tab.sections"
:class="[gridClass(section.columns), { 'px-3 sm:px-5': allowTabs }]" :key="section.label"
class="section"
> >
<div v-for="field in section.fields" :key="field.name"> <div
v-if="i != 0"
class="w-full"
:class="[section.hideBorder ? 'mt-4' : 'h-px border-t my-5']"
/>
<Section
class="text-lg font-medium"
:class="{ 'px-3 sm:px-5': hasTabs }"
:label="section.label"
:hideLabel="section.hideLabel"
:opened="section.opened"
:collapsible="section.collapsible"
collapseIconPosition="right"
>
<div <div
class="settings-field" class="grid gap-4"
v-if=" :class="[
(field.type == 'Check' || gridClass(section.columns),
(field.read_only && data[field.name]) || { 'px-3 sm:px-5': hasTabs },
!field.read_only || { 'mt-6': !section.hideLabel },
!field.hidden) && ]"
(!field.depends_on || field.display_via_depends_on)
"
> >
<div <div v-for="field in section.fields" :key="field.name">
v-if="field.type != 'Check'" <div
class="mb-2 text-sm text-ink-gray-5" class="settings-field"
>
{{ __(field.label) }}
<span
class="text-ink-red-3"
v-if=" v-if="
field.mandatory || (field.type == 'Check' ||
(field.mandatory_depends_on && (field.read_only && data[field.name]) ||
field.mandatory_via_depends_on) !field.read_only ||
!field.hidden) &&
(!field.depends_on || field.display_via_depends_on)
" "
>*</span
> >
</div> <div
<FormControl v-if="field.type != 'Check'"
v-if="field.read_only && field.type !== 'Check'" class="mb-2 text-sm text-ink-gray-5"
type="text" >
:placeholder="getPlaceholder(field)" {{ __(field.label) }}
v-model="data[field.name]" <span
:disabled="true" class="text-ink-red-3"
/> v-if="
<FormControl field.mandatory ||
v-else-if="field.type === 'Select'" (field.mandatory_depends_on &&
type="select" field.mandatory_via_depends_on)
class="form-control" "
:class="field.prefix ? 'prefix' : ''" >*</span
:options="field.options" >
v-model="data[field.name]" </div>
:placeholder="getPlaceholder(field)" <FormControl
> v-if="field.read_only && field.type !== 'Check'"
<template v-if="field.prefix" #prefix> type="text"
<IndicatorIcon :class="field.prefix" /> :placeholder="getPlaceholder(field)"
</template> v-model="data[field.name]"
</FormControl> :disabled="true"
<div />
v-else-if="field.type == 'Check'" <FormControl
class="flex items-center gap-2" v-else-if="field.type === 'Select'"
> type="select"
<FormControl class="form-control"
class="form-control" :class="field.prefix ? 'prefix' : ''"
type="checkbox" :options="field.options"
v-model="data[field.name]" v-model="data[field.name]"
@change="(e) => (data[field.name] = e.target.checked)" :placeholder="getPlaceholder(field)"
:disabled="Boolean(field.read_only)" >
/> <template v-if="field.prefix" #prefix>
<label <IndicatorIcon :class="field.prefix" />
class="text-sm text-ink-gray-5" </template>
@click="data[field.name] = !data[field.name]" </FormControl>
> <div
{{ __(field.label) }} v-else-if="field.type == 'Check'"
<span class="text-ink-red-3" v-if="field.mandatory">*</span> class="flex items-center gap-2"
</label> >
</div> <FormControl
<div class="flex gap-1" v-else-if="field.type === 'Link'"> class="form-control"
<Link type="checkbox"
class="form-control flex-1" v-model="data[field.name]"
:value="data[field.name]" @change="(e) => (data[field.name] = e.target.checked)"
:doctype="field.options" :disabled="Boolean(field.read_only)"
:filters="field.filters" />
@change="(v) => (data[field.name] = v)" <label
:placeholder="getPlaceholder(field)" class="text-sm text-ink-gray-5"
:onCreate="field.create" @click="data[field.name] = !data[field.name]"
/> >
<Button {{ __(field.label) }}
v-if="data[field.name] && field.edit" <span class="text-ink-red-3" v-if="field.mandatory"
class="shrink-0" >*</span
:label="__('Edit')" >
@click="field.edit(data[field.name])" </label>
> </div>
<template #prefix> <div class="flex gap-1" v-else-if="field.type === 'Link'">
<EditIcon class="h-4 w-4" /> <Link
</template> class="form-control flex-1"
</Button> :value="data[field.name]"
</div> :doctype="field.options"
:filters="field.filters"
@change="(v) => (data[field.name] = v)"
:placeholder="getPlaceholder(field)"
:onCreate="field.create"
/>
<Button
v-if="data[field.name] && field.edit"
class="shrink-0"
:label="__('Edit')"
@click="field.edit(data[field.name])"
>
<template #prefix>
<EditIcon class="h-4 w-4" />
</template>
</Button>
</div>
<Link <Link
v-else-if="field.type === 'User'" v-else-if="field.type === 'User'"
class="form-control" class="form-control"
:value="getUser(data[field.name]).full_name" :value="getUser(data[field.name]).full_name"
:doctype="field.options" :doctype="field.options"
:filters="field.filters" :filters="field.filters"
@change="(v) => (data[field.name] = v)" @change="(v) => (data[field.name] = v)"
:placeholder="getPlaceholder(field)" :placeholder="getPlaceholder(field)"
:hideMe="true" :hideMe="true"
> >
<template #prefix> <template #prefix>
<UserAvatar class="mr-2" :user="data[field.name]" size="sm" /> <UserAvatar
</template> class="mr-2"
<template #item-prefix="{ option }"> :user="data[field.name]"
<UserAvatar class="mr-2" :user="option.value" size="sm" /> size="sm"
</template> />
<template #item-label="{ option }"> </template>
<Tooltip :text="option.value"> <template #item-prefix="{ option }">
<div class="cursor-pointer"> <UserAvatar class="mr-2" :user="option.value" size="sm" />
{{ getUser(option.value).full_name }} </template>
</div> <template #item-label="{ option }">
</Tooltip> <Tooltip :text="option.value">
</template> <div class="cursor-pointer">
</Link> {{ getUser(option.value).full_name }}
<DateTimePicker </div>
v-else-if="field.type === 'Datetime'" </Tooltip>
v-model="data[field.name]" </template>
icon-left="" </Link>
:formatter="(date) => getFormat(date, '', true, true)" <DateTimePicker
:placeholder="getPlaceholder(field)" v-else-if="field.type === 'Datetime'"
input-class="border-none" v-model="data[field.name]"
/> icon-left=""
<DatePicker :formatter="(date) => getFormat(date, '', true, true)"
v-else-if="field.type === 'Date'" :placeholder="getPlaceholder(field)"
icon-left="" input-class="border-none"
v-model="data[field.name]" />
:formatter="(date) => getFormat(date, '', true)" <DatePicker
:placeholder="getPlaceholder(field)" v-else-if="field.type === 'Date'"
input-class="border-none" icon-left=""
/> v-model="data[field.name]"
<FormControl :formatter="(date) => getFormat(date, '', true)"
v-else-if=" :placeholder="getPlaceholder(field)"
['Small Text', 'Text', 'Long Text'].includes(field.type) input-class="border-none"
" />
type="textarea" <FormControl
:placeholder="getPlaceholder(field)" v-else-if="
v-model="data[field.name]" ['Small Text', 'Text', 'Long Text'].includes(field.type)
/> "
<FormControl type="textarea"
v-else-if="['Int'].includes(field.type)" :placeholder="getPlaceholder(field)"
type="number" v-model="data[field.name]"
:placeholder="getPlaceholder(field)" />
v-model="data[field.name]" <FormControl
/> v-else-if="['Int'].includes(field.type)"
<FormControl type="number"
v-else :placeholder="getPlaceholder(field)"
type="text" v-model="data[field.name]"
:placeholder="getPlaceholder(field)" />
v-model="data[field.name]" <FormControl
:disabled="Boolean(field.read_only)" v-else
/> type="text"
:placeholder="getPlaceholder(field)"
v-model="data[field.name]"
:disabled="Boolean(field.read_only)"
/>
</div>
</div>
</div> </div>
</div> </Section>
</div> </div>
</Section> </div>
</div> </Tabs>
</div> </div>
</template> </template>
@ -186,19 +215,24 @@ import UserAvatar from '@/components/UserAvatar.vue'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { getFormat } from '@/utils' import { getFormat } from '@/utils'
import { Tooltip, DatePicker, DateTimePicker } from 'frappe-ui' import { Tabs, Tooltip, DatePicker, DateTimePicker } from 'frappe-ui'
import { ref, computed } from 'vue'
const { getUser } = usersStore() const { getUser } = usersStore()
const props = defineProps({ const props = defineProps({
sections: Array, tabs: Array,
data: Object, data: Object,
allowTabs: { modal: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
}) })
const hasTabs = computed(() => !props.tabs[0].no_tabs)
const tabIndex = ref(0)
function gridClass(columns) { function gridClass(columns) {
columns = columns || 3 columns = columns || 3
let griColsMap = { let griColsMap = {

View File

@ -22,8 +22,8 @@
</Button> </Button>
</div> </div>
</div> </div>
<div v-if="sections.data"> <div v-if="tabs.data">
<FieldLayout :sections="sections.data" :data="_address" /> <FieldLayout :tabs="tabs.data" :data="_address" />
<ErrorMessage class="mt-2" :message="error" /> <ErrorMessage class="mt-2" :message="error" />
</div> </div>
</div> </div>
@ -106,7 +106,7 @@ const dialogOptions = computed(() => {
return { title, size, actions } return { title, size, actions }
}) })
const sections = 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',
cache: ['QuickEntry', 'Address'], cache: ['QuickEntry', 'Address'],
params: { doctype: 'Address', type: 'Quick Entry' }, params: { doctype: 'Address', type: 'Quick Entry' },

View File

@ -59,7 +59,7 @@
</div> </div>
<FieldLayout <FieldLayout
v-else-if="filteredSections.length" v-else-if="filteredSections.length"
:sections="filteredSections" :tabs="filteredSections"
:data="_contact" :data="_contact"
/> />
</div> </div>
@ -245,7 +245,7 @@ const detailFields = computed(() => {
return details.filter((detail) => detail.value) return details.filter((detail) => detail.value)
}) })
const sections = 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',
cache: ['QuickEntry', 'Contact'], cache: ['QuickEntry', 'Contact'],
params: { doctype: 'Contact', type: 'Quick Entry' }, params: { doctype: 'Contact', type: 'Quick Entry' },
@ -253,7 +253,7 @@ const sections = createResource({
}) })
const filteredSections = computed(() => { const filteredSections = computed(() => {
let allSections = sections.data || [] let allSections = tabs.data?.[0]?.sections || []
if (!allSections.length) return [] if (!allSections.length) return []
allSections.forEach((s) => { allSections.forEach((s) => {
@ -276,7 +276,7 @@ const filteredSections = computed(() => {
}) })
}) })
return allSections return [{ no_tabs: true, sections: allSections }]
}) })
const dirty = computed(() => { const dirty = computed(() => {

View File

@ -29,13 +29,13 @@
size="sm" size="sm"
/> />
</div> </div>
<div v-if="sections?.data"> <div v-if="tabs?.data">
<FieldLayoutEditor <FieldLayoutEditor
v-if="!preview" v-if="!preview"
:sections="sections.data" :sections="tabs.data"
:doctype="_doctype" :doctype="_doctype"
/> />
<FieldLayout v-else :sections="sections.data" :data="{}" /> <FieldLayout v-else :tabs="tabs.data" :data="{}" :modal="true" />
</div> </div>
</div> </div>
</template> </template>
@ -77,20 +77,20 @@ function getParams() {
return { doctype: _doctype.value, type: 'Data Fields' } return { doctype: _doctype.value, type: 'Data Fields' }
} }
const sections = 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',
cache: ['DataFieldsModal', _doctype.value], cache: ['DataFieldsModal', _doctype.value],
params: getParams(), params: getParams(),
onSuccess(data) { onSuccess(data) {
sections.originalData = JSON.parse(JSON.stringify(data)) tabs.originalData = JSON.parse(JSON.stringify(data))
}, },
}) })
watch( watch(
() => sections?.data, () => tabs?.data,
() => { () => {
dirty.value = dirty.value =
JSON.stringify(sections?.data) !== JSON.stringify(sections?.originalData) JSON.stringify(tabs?.data) !== JSON.stringify(tabs?.originalData)
}, },
{ deep: true }, { deep: true },
) )
@ -99,18 +99,21 @@ onMounted(() => useDebounceFn(reload, 100)())
function reload() { function reload() {
nextTick(() => { nextTick(() => {
sections.params = getParams() tabs.params = getParams()
sections.reload() tabs.reload()
}) })
} }
function saveChanges() { function saveChanges() {
let _sections = JSON.parse(JSON.stringify(sections.data)) let _tabs = JSON.parse(JSON.stringify(tabs.data))
_sections.forEach((section) => { _tabs.forEach((tab) => {
if (!section.fields) return if (!tab.sections) return
section.fields = section.fields.map( tab.sections.forEach((section) => {
(field) => field.fieldname || field.name, if (!section.fields) return
) section.fields = section.fields.map(
(field) => field.fieldname || field.name,
)
})
}) })
loading.value = true loading.value = true
call( call(
@ -118,7 +121,7 @@ function saveChanges() {
{ {
doctype: _doctype.value, doctype: _doctype.value,
type: 'Data Fields', type: 'Data Fields',
layout: JSON.stringify(_sections), layout: JSON.stringify(_tabs),
}, },
).then(() => { ).then(() => {
loading.value = false loading.value = false

View File

@ -36,7 +36,7 @@
<div class="h-px w-full border-t my-5" /> <div class="h-px w-full border-t my-5" />
<FieldLayout <FieldLayout
v-if="filteredSections.length" v-if="filteredSections.length"
:sections="filteredSections" :tabs="filteredSections"
:data="deal" :data="deal"
/> />
<ErrorMessage class="mt-4" v-if="error" :message="__(error)" /> <ErrorMessage class="mt-4" v-if="error" :message="__(error)" />
@ -100,28 +100,30 @@ const isDealCreating = ref(false)
const chooseExistingContact = ref(false) const chooseExistingContact = ref(false)
const chooseExistingOrganization = ref(false) const chooseExistingOrganization = ref(false)
const sections = 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',
cache: ['QuickEntry', 'CRM Deal'], cache: ['QuickEntry', 'CRM Deal'],
params: { doctype: 'CRM Deal', type: 'Quick Entry' }, params: { doctype: 'CRM Deal', type: 'Quick Entry' },
auto: true, auto: true,
transform: (data) => { transform: (_tabs) => {
return data.forEach((section) => { return _tabs.forEach((tab) => {
section.fields.forEach((field) => { tab.sections.forEach((section) => {
if (field.name == 'status') { section.fields.forEach((field) => {
field.type = 'Select' if (field.name == 'status') {
field.options = dealStatuses.value field.type = 'Select'
field.prefix = getDealStatus(deal.status).iconColorClass field.options = dealStatuses.value
} else if (field.name == 'deal_owner') { field.prefix = getDealStatus(deal.status).iconColorClass
field.type = 'User' } else if (field.name == 'deal_owner') {
} field.type = 'User'
}
})
}) })
}) })
}, },
}) })
const filteredSections = computed(() => { const filteredSections = computed(() => {
let allSections = sections.data || [] let allSections = tabs.data?.[0]?.sections || []
if (!allSections.length) return [] if (!allSections.length) return []
let _filteredSections = [] let _filteredSections = []
@ -159,7 +161,7 @@ const filteredSections = computed(() => {
} }
}) })
return _filteredSections return [{ no_tabs: true, sections: _filteredSections }]
}) })
const dealStatuses = computed(() => { const dealStatuses = computed(() => {

View File

@ -23,7 +23,7 @@
</div> </div>
</div> </div>
<div> <div>
<FieldLayout v-if="sections.data" :sections="sections.data" :data="lead" /> <FieldLayout v-if="tabs.data" :tabs="tabs.data" :data="lead" />
<ErrorMessage class="mt-4" v-if="error" :message="__(error)" /> <ErrorMessage class="mt-4" v-if="error" :message="__(error)" />
</div> </div>
</div> </div>
@ -63,21 +63,23 @@ const router = useRouter()
const error = ref(null) const error = ref(null)
const isLeadCreating = ref(false) const isLeadCreating = ref(false)
const sections = 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',
cache: ['QuickEntry', 'CRM Lead'], cache: ['QuickEntry', 'CRM Lead'],
params: { doctype: 'CRM Lead', type: 'Quick Entry' }, params: { doctype: 'CRM Lead', type: 'Quick Entry' },
auto: true, auto: true,
transform: (data) => { transform: (_tabs) => {
return data.forEach((section) => { return _tabs.forEach((tab) => {
section.fields.forEach((field) => { tab.sections.forEach((section) => {
if (field.name == 'status') { section.fields.forEach((field) => {
field.type = 'Select' if (field.name == 'status') {
field.options = leadStatuses.value field.type = 'Select'
field.prefix = getLeadStatus(lead.status).iconColorClass field.options = leadStatuses.value
} else if (field.name == 'lead_owner') { field.prefix = getLeadStatus(lead.status).iconColorClass
field.type = 'User' } else if (field.name == 'lead_owner') {
} field.type = 'User'
}
})
}) })
}) })
}, },

View File

@ -37,7 +37,7 @@
</div> </div>
<FieldLayout <FieldLayout
v-else-if="filteredSections.length" v-else-if="filteredSections.length"
:sections="filteredSections" :tabs="filteredSections"
:data="_organization" :data="_organization"
/> />
</div> </div>
@ -241,7 +241,7 @@ const fields = computed(() => {
return details.filter((field) => field.value) return details.filter((field) => field.value)
}) })
const sections = 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',
cache: ['QuickEntry', 'CRM Organization'], cache: ['QuickEntry', 'CRM Organization'],
params: { doctype: 'CRM Organization', type: 'Quick Entry' }, params: { doctype: 'CRM Organization', type: 'Quick Entry' },
@ -249,7 +249,7 @@ const sections = createResource({
}) })
const filteredSections = computed(() => { const filteredSections = computed(() => {
let allSections = sections.data || [] let allSections = tabs.data?.[0]?.sections || []
if (!allSections.length) return [] if (!allSections.length) return []
allSections.forEach((s) => { allSections.forEach((s) => {
@ -272,7 +272,7 @@ const filteredSections = computed(() => {
}) })
}) })
return allSections return [{ no_tabs: true, sections: allSections }]
}) })
watch( watch(

View File

@ -35,13 +35,13 @@
size="sm" size="sm"
/> />
</div> </div>
<div v-if="sections?.data"> <div v-if="tabs?.data">
<FieldLayoutEditor <FieldLayoutEditor
v-if="!preview" v-if="!preview"
:sections="sections.data" :sections="tabs.data"
:doctype="_doctype" :doctype="_doctype"
/> />
<FieldLayout v-else :sections="sections.data" :data="{}" /> <FieldLayout v-else :tabs="tabs.data" :data="{}" />
</div> </div>
</div> </div>
</template> </template>
@ -83,20 +83,20 @@ function getParams() {
return { doctype: _doctype.value, type: 'Quick Entry' } return { doctype: _doctype.value, type: 'Quick Entry' }
} }
const sections = 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',
cache: ['QuickEntryModal', _doctype.value], cache: ['QuickEntryModal', _doctype.value],
params: getParams(), params: getParams(),
onSuccess(data) { onSuccess(data) {
sections.originalData = JSON.parse(JSON.stringify(data)) tabs.originalData = JSON.parse(JSON.stringify(data))
}, },
}) })
watch( watch(
() => sections?.data, () => tabs?.data,
() => { () => {
dirty.value = dirty.value =
JSON.stringify(sections?.data) !== JSON.stringify(sections?.originalData) JSON.stringify(tabs?.data) !== JSON.stringify(tabs?.originalData)
}, },
{ deep: true }, { deep: true },
) )
@ -105,18 +105,21 @@ onMounted(() => useDebounceFn(reload, 100)())
function reload() { function reload() {
nextTick(() => { nextTick(() => {
sections.params = getParams() tabs.params = getParams()
sections.reload() tabs.reload()
}) })
} }
function saveChanges() { function saveChanges() {
let _sections = JSON.parse(JSON.stringify(sections.data)) let _tabs = JSON.parse(JSON.stringify(tabs.data))
_sections.forEach((section) => { _tabs.forEach((tab) => {
if (!section.fields) return if (!tab.sections) return
section.fields = section.fields.map( tab.sections.forEach((section) => {
(field) => field.fieldname || field.name, if (!section.fields) return
) section.fields = section.fields.map(
(field) => field.fieldname || field.name,
)
})
}) })
loading.value = true loading.value = true
call( call(
@ -124,7 +127,7 @@ function saveChanges() {
{ {
doctype: _doctype.value, doctype: _doctype.value,
type: 'Quick Entry', type: 'Quick Entry',
layout: JSON.stringify(_sections), layout: JSON.stringify(_tabs),
}, },
).then(() => { ).then(() => {
loading.value = false loading.value = false

View File

@ -29,18 +29,20 @@
size="sm" size="sm"
/> />
</div> </div>
<div v-if="sections.data" class="flex gap-4"> <div v-if="tabs.data?.[0]?.sections" class="flex gap-4">
<SidePanelLayoutEditor <SidePanelLayoutEditor
class="flex flex-1 flex-col pr-2" class="flex flex-1 flex-col pr-2"
:sections="sections.data" :sections="tabs.data[0].sections"
:doctype="_doctype" :doctype="_doctype"
/> />
<div v-if="preview" class="flex flex-1 flex-col border rounded"> <div v-if="preview" class="flex flex-1 flex-col border rounded">
<div <div
v-for="(section, i) in sections.data" v-for="(section, i) in tabs.data[0].sections"
:key="section.label" :key="section.label"
class="flex flex-col py-1.5 px-1" class="flex flex-col py-1.5 px-1"
:class="{ 'border-b': i !== sections.data?.length - 1 }" :class="{
'border-b': i !== tabs.data[0].sections?.length - 1,
}"
> >
<Section <Section
class="p-2" class="p-2"
@ -106,20 +108,20 @@ function getParams() {
return { doctype: _doctype.value, type: 'Side Panel' } return { doctype: _doctype.value, type: 'Side Panel' }
} }
const sections = 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',
cache: ['SidePanel', _doctype.value], cache: ['SidePanel', _doctype.value],
params: getParams(), params: getParams(),
onSuccess(data) { onSuccess(data) {
sections.originalData = JSON.parse(JSON.stringify(data)) tabs.originalData = JSON.parse(JSON.stringify(data))
}, },
}) })
watch( watch(
() => sections?.data, () => tabs?.data,
() => { () => {
dirty.value = dirty.value =
JSON.stringify(sections?.data) !== JSON.stringify(sections?.originalData) JSON.stringify(tabs?.data) !== JSON.stringify(tabs?.originalData)
}, },
{ deep: true }, { deep: true },
) )
@ -128,18 +130,20 @@ onMounted(() => useDebounceFn(reload, 100)())
function reload() { function reload() {
nextTick(() => { nextTick(() => {
sections.params = getParams() tabs.params = getParams()
sections.reload() tabs.reload()
}) })
} }
function saveChanges() { function saveChanges() {
let _sections = JSON.parse(JSON.stringify(sections.data)) let _tabs = JSON.parse(JSON.stringify(tabs.data))
_sections.forEach((section) => { _tabs.forEach((tab) => {
if (!section.fields) return tab.sections.forEach((section) => {
section.fields = section.fields if (!section.fields) return
.map((field) => field.fieldname || field.name) section.fields = section.fields
.filter(Boolean) .map((field) => field.fieldname || field.name)
.filter(Boolean)
})
}) })
loading.value = true loading.value = true
call( call(
@ -147,7 +151,7 @@ function saveChanges() {
{ {
doctype: _doctype.value, doctype: _doctype.value,
type: 'Side Panel', type: 'Side Panel',
layout: JSON.stringify(_sections), layout: JSON.stringify(_tabs),
}, },
).then(() => { ).then(() => {
loading.value = false loading.value = false

View File

@ -12,11 +12,7 @@
/> />
</h2> </h2>
<div v-if="!data.get.loading" class="flex-1 overflow-y-auto"> <div v-if="!data.get.loading" class="flex-1 overflow-y-auto">
<FieldLayout <FieldLayout v-if="data?.doc && tabs" :tabs="tabs" :data="data.doc" />
v-if="data?.doc && sections"
:sections="sections"
:data="data.doc"
/>
<ErrorMessage class="mt-2" :message="error" /> <ErrorMessage class="mt-2" :message="error" />
</div> </div>
<div v-else class="flex flex-1 items-center justify-center"> <div v-else class="flex flex-1 items-center justify-center">
@ -98,7 +94,7 @@ const data = createDocumentResource({
}, },
}) })
const sections = computed(() => { const tabs = computed(() => {
if (!fields.data) return [] if (!fields.data) return []
let _sections = [] let _sections = []
let fieldsData = fields.data let fieldsData = fields.data
@ -138,7 +134,7 @@ const sections = computed(() => {
} }
}) })
return _sections return [{ no_tabs: true, sections: _sections }]
}) })
function update() { function update() {
@ -148,7 +144,8 @@ function update() {
} }
function validateMandatoryFields() { function validateMandatoryFields() {
for (let section of sections.value) { if (!tabs.value) return false
for (let section of tabs.value[0].sections) {
for (let field of section.fields) { for (let field of section.fields) {
if ( if (
(field.mandatory || (field.mandatory ||