diff --git a/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py b/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py
index 6926d0b2..0d06ca43 100644
--- a/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py
+++ b/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py
@@ -2,6 +2,7 @@
# For license information, please see license.txt
import json
+
import frappe
from frappe import _
from frappe.model.document import Document
@@ -10,46 +11,54 @@ from frappe.model.document import Document
class CRMFieldsLayout(Document):
pass
+
@frappe.whitelist()
def get_fields_layout(doctype: str, type: str):
- sections = []
+ tabs = []
if frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": type}):
layout = frappe.get_doc("CRM Fields Layout", {"dt": doctype, "type": type})
else:
return []
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 = []
- for section in sections:
- if not section.get("fields"):
- continue
- allowed_fields.extend(section.get("fields"))
+ for tab in tabs:
+ for section in tab.get("sections"):
+ if not section.get("fields"):
+ continue
+ allowed_fields.extend(section.get("fields"))
fields = frappe.get_meta(doctype).fields
fields = [field for field in fields if field.fieldname in allowed_fields]
- for section in sections:
- for field in section.get("fields") if section.get("fields") else []:
- field = next((f for f in fields if f.fieldname == field), None)
- if field:
- if field.fieldtype == "Select" and field.options:
- field.options = field.options.split("\n")
- field.options = [{"label": _(option), "value": option} for option in field.options]
- field.options.insert(0, {"label": "", "value": ""})
- field = {
- "label": _(field.label),
- "name": field.fieldname,
- "type": field.fieldtype,
- "options": field.options,
- "mandatory": field.reqd,
- "placeholder": field.get("placeholder"),
- "filters": field.get("link_filters")
- }
- section["fields"][section.get("fields").index(field["name"])] = field
+ for tab in tabs:
+ for section in tab.get("sections"):
+ for field in section.get("fields") if section.get("fields") else []:
+ field = next((f for f in fields if f.fieldname == field), None)
+ if field:
+ if field.fieldtype == "Select" and field.options:
+ field.options = field.options.split("\n")
+ field.options = [{"label": _(option), "value": option} for option in field.options]
+ field.options.insert(0, {"label": "", "value": ""})
+ field = {
+ "label": _(field.label),
+ "name": field.fieldname,
+ "type": field.fieldtype,
+ "options": field.options,
+ "mandatory": field.reqd,
+ "placeholder": field.get("placeholder"),
+ "filters": field.get("link_filters"),
+ }
+ section["fields"][section.get("fields").index(field["name"])] = field
- return sections or []
+ return tabs or []
@frappe.whitelist()
@@ -59,11 +68,13 @@ def save_fields_layout(doctype: str, type: str, layout: str):
else:
doc = frappe.new_doc("CRM Fields Layout")
- doc.update({
- "dt": doctype,
- "type": type,
- "layout": layout,
- })
+ doc.update(
+ {
+ "dt": doctype,
+ "type": type,
+ "layout": layout,
+ }
+ )
doc.save(ignore_permissions=True)
return doc.layout
diff --git a/frontend/src/components/Activities/DataFields.vue b/frontend/src/components/Activities/DataFields.vue
index 4828e329..93330670 100644
--- a/frontend/src/components/Activities/DataFields.vue
+++ b/frontend/src/components/Activities/DataFields.vue
@@ -31,16 +31,8 @@
{{ __('Loading...') }}
-
-
+
+
-
-
+
-
-
+
-
+
+
-
- {{ __(field.label) }}
-
+ *
-
-
-
-
-
-
-
-
- (data[field.name] = e.target.checked)"
- :disabled="Boolean(field.read_only)"
- />
-
-
-
- (data[field.name] = v)"
- :placeholder="getPlaceholder(field)"
- :onCreate="field.create"
- />
-
-
+
+ {{ __(field.label) }}
+ *
+
+
+
+
+
+
+
+
+ (data[field.name] = e.target.checked)"
+ :disabled="Boolean(field.read_only)"
+ />
+
+
+
+ (data[field.name] = v)"
+ :placeholder="getPlaceholder(field)"
+ :onCreate="field.create"
+ />
+
+
- (data[field.name] = v)"
- :placeholder="getPlaceholder(field)"
- :hideMe="true"
- >
-
-
-
-
-
-
-
-
-
- {{ getUser(option.value).full_name }}
-
-
-
-
-
-
-
-
-
+ (data[field.name] = v)"
+ :placeholder="getPlaceholder(field)"
+ :hideMe="true"
+ >
+
+
+
+
+
+
+
+
+
+ {{ getUser(option.value).full_name }}
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
+
+
@@ -186,19 +215,24 @@ import UserAvatar from '@/components/UserAvatar.vue'
import Link from '@/components/Controls/Link.vue'
import { usersStore } from '@/stores/users'
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 props = defineProps({
- sections: Array,
+ tabs: Array,
data: Object,
- allowTabs: {
+ modal: {
type: Boolean,
default: false,
},
})
+const hasTabs = computed(() => !props.tabs[0].no_tabs)
+
+const tabIndex = ref(0)
+
function gridClass(columns) {
columns = columns || 3
let griColsMap = {
diff --git a/frontend/src/components/Modals/AddressModal.vue b/frontend/src/components/Modals/AddressModal.vue
index a23396de..fd7cd25a 100644
--- a/frontend/src/components/Modals/AddressModal.vue
+++ b/frontend/src/components/Modals/AddressModal.vue
@@ -22,8 +22,8 @@
-
@@ -106,7 +106,7 @@ const dialogOptions = computed(() => {
return { title, size, actions }
})
-const sections = createResource({
+const tabs = createResource({
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
cache: ['QuickEntry', 'Address'],
params: { doctype: 'Address', type: 'Quick Entry' },
diff --git a/frontend/src/components/Modals/ContactModal.vue b/frontend/src/components/Modals/ContactModal.vue
index c7993e7e..8487985a 100644
--- a/frontend/src/components/Modals/ContactModal.vue
+++ b/frontend/src/components/Modals/ContactModal.vue
@@ -59,7 +59,7 @@
@@ -245,7 +245,7 @@ const detailFields = computed(() => {
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',
cache: ['QuickEntry', 'Contact'],
params: { doctype: 'Contact', type: 'Quick Entry' },
@@ -253,7 +253,7 @@ const sections = createResource({
})
const filteredSections = computed(() => {
- let allSections = sections.data || []
+ let allSections = tabs.data?.[0]?.sections || []
if (!allSections.length) return []
allSections.forEach((s) => {
@@ -276,7 +276,7 @@ const filteredSections = computed(() => {
})
})
- return allSections
+ return [{ no_tabs: true, sections: allSections }]
})
const dirty = computed(() => {
diff --git a/frontend/src/components/Modals/DataFieldsModal.vue b/frontend/src/components/Modals/DataFieldsModal.vue
index dd763d89..17b56b48 100644
--- a/frontend/src/components/Modals/DataFieldsModal.vue
+++ b/frontend/src/components/Modals/DataFieldsModal.vue
@@ -29,13 +29,13 @@
size="sm"
/>
-
@@ -77,20 +77,20 @@ function getParams() {
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',
cache: ['DataFieldsModal', _doctype.value],
params: getParams(),
onSuccess(data) {
- sections.originalData = JSON.parse(JSON.stringify(data))
+ tabs.originalData = JSON.parse(JSON.stringify(data))
},
})
watch(
- () => sections?.data,
+ () => tabs?.data,
() => {
dirty.value =
- JSON.stringify(sections?.data) !== JSON.stringify(sections?.originalData)
+ JSON.stringify(tabs?.data) !== JSON.stringify(tabs?.originalData)
},
{ deep: true },
)
@@ -99,18 +99,21 @@ onMounted(() => useDebounceFn(reload, 100)())
function reload() {
nextTick(() => {
- sections.params = getParams()
- sections.reload()
+ tabs.params = getParams()
+ tabs.reload()
})
}
function saveChanges() {
- let _sections = JSON.parse(JSON.stringify(sections.data))
- _sections.forEach((section) => {
- if (!section.fields) return
- section.fields = section.fields.map(
- (field) => field.fieldname || field.name,
- )
+ let _tabs = JSON.parse(JSON.stringify(tabs.data))
+ _tabs.forEach((tab) => {
+ if (!tab.sections) return
+ tab.sections.forEach((section) => {
+ if (!section.fields) return
+ section.fields = section.fields.map(
+ (field) => field.fieldname || field.name,
+ )
+ })
})
loading.value = true
call(
@@ -118,7 +121,7 @@ function saveChanges() {
{
doctype: _doctype.value,
type: 'Data Fields',
- layout: JSON.stringify(_sections),
+ layout: JSON.stringify(_tabs),
},
).then(() => {
loading.value = false
diff --git a/frontend/src/components/Modals/DealModal.vue b/frontend/src/components/Modals/DealModal.vue
index da8c9c5a..2ccadec6 100644
--- a/frontend/src/components/Modals/DealModal.vue
+++ b/frontend/src/components/Modals/DealModal.vue
@@ -36,7 +36,7 @@
@@ -100,28 +100,30 @@ const isDealCreating = ref(false)
const chooseExistingContact = 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',
cache: ['QuickEntry', 'CRM Deal'],
params: { doctype: 'CRM Deal', type: 'Quick Entry' },
auto: true,
- transform: (data) => {
- return data.forEach((section) => {
- section.fields.forEach((field) => {
- if (field.name == 'status') {
- field.type = 'Select'
- field.options = dealStatuses.value
- field.prefix = getDealStatus(deal.status).iconColorClass
- } else if (field.name == 'deal_owner') {
- field.type = 'User'
- }
+ transform: (_tabs) => {
+ return _tabs.forEach((tab) => {
+ tab.sections.forEach((section) => {
+ section.fields.forEach((field) => {
+ if (field.name == 'status') {
+ field.type = 'Select'
+ field.options = dealStatuses.value
+ field.prefix = getDealStatus(deal.status).iconColorClass
+ } else if (field.name == 'deal_owner') {
+ field.type = 'User'
+ }
+ })
})
})
},
})
const filteredSections = computed(() => {
- let allSections = sections.data || []
+ let allSections = tabs.data?.[0]?.sections || []
if (!allSections.length) return []
let _filteredSections = []
@@ -159,7 +161,7 @@ const filteredSections = computed(() => {
}
})
- return _filteredSections
+ return [{ no_tabs: true, sections: _filteredSections }]
})
const dealStatuses = computed(() => {
diff --git a/frontend/src/components/Modals/LeadModal.vue b/frontend/src/components/Modals/LeadModal.vue
index 958b04fd..401afeac 100644
--- a/frontend/src/components/Modals/LeadModal.vue
+++ b/frontend/src/components/Modals/LeadModal.vue
@@ -23,7 +23,7 @@
-
+
@@ -63,21 +63,23 @@ const router = useRouter()
const error = ref(null)
const isLeadCreating = ref(false)
-const sections = createResource({
+const tabs = createResource({
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
cache: ['QuickEntry', 'CRM Lead'],
params: { doctype: 'CRM Lead', type: 'Quick Entry' },
auto: true,
- transform: (data) => {
- return data.forEach((section) => {
- section.fields.forEach((field) => {
- if (field.name == 'status') {
- field.type = 'Select'
- field.options = leadStatuses.value
- field.prefix = getLeadStatus(lead.status).iconColorClass
- } else if (field.name == 'lead_owner') {
- field.type = 'User'
- }
+ transform: (_tabs) => {
+ return _tabs.forEach((tab) => {
+ tab.sections.forEach((section) => {
+ section.fields.forEach((field) => {
+ if (field.name == 'status') {
+ field.type = 'Select'
+ field.options = leadStatuses.value
+ field.prefix = getLeadStatus(lead.status).iconColorClass
+ } else if (field.name == 'lead_owner') {
+ field.type = 'User'
+ }
+ })
})
})
},
diff --git a/frontend/src/components/Modals/OrganizationModal.vue b/frontend/src/components/Modals/OrganizationModal.vue
index 03bec690..e5f1d589 100644
--- a/frontend/src/components/Modals/OrganizationModal.vue
+++ b/frontend/src/components/Modals/OrganizationModal.vue
@@ -37,7 +37,7 @@
@@ -241,7 +241,7 @@ const fields = computed(() => {
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',
cache: ['QuickEntry', 'CRM Organization'],
params: { doctype: 'CRM Organization', type: 'Quick Entry' },
@@ -249,7 +249,7 @@ const sections = createResource({
})
const filteredSections = computed(() => {
- let allSections = sections.data || []
+ let allSections = tabs.data?.[0]?.sections || []
if (!allSections.length) return []
allSections.forEach((s) => {
@@ -272,7 +272,7 @@ const filteredSections = computed(() => {
})
})
- return allSections
+ return [{ no_tabs: true, sections: allSections }]
})
watch(
diff --git a/frontend/src/components/Modals/QuickEntryModal.vue b/frontend/src/components/Modals/QuickEntryModal.vue
index 903637a2..8859d386 100644
--- a/frontend/src/components/Modals/QuickEntryModal.vue
+++ b/frontend/src/components/Modals/QuickEntryModal.vue
@@ -35,13 +35,13 @@
size="sm"
/>
-
@@ -83,20 +83,20 @@ function getParams() {
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',
cache: ['QuickEntryModal', _doctype.value],
params: getParams(),
onSuccess(data) {
- sections.originalData = JSON.parse(JSON.stringify(data))
+ tabs.originalData = JSON.parse(JSON.stringify(data))
},
})
watch(
- () => sections?.data,
+ () => tabs?.data,
() => {
dirty.value =
- JSON.stringify(sections?.data) !== JSON.stringify(sections?.originalData)
+ JSON.stringify(tabs?.data) !== JSON.stringify(tabs?.originalData)
},
{ deep: true },
)
@@ -105,18 +105,21 @@ onMounted(() => useDebounceFn(reload, 100)())
function reload() {
nextTick(() => {
- sections.params = getParams()
- sections.reload()
+ tabs.params = getParams()
+ tabs.reload()
})
}
function saveChanges() {
- let _sections = JSON.parse(JSON.stringify(sections.data))
- _sections.forEach((section) => {
- if (!section.fields) return
- section.fields = section.fields.map(
- (field) => field.fieldname || field.name,
- )
+ let _tabs = JSON.parse(JSON.stringify(tabs.data))
+ _tabs.forEach((tab) => {
+ if (!tab.sections) return
+ tab.sections.forEach((section) => {
+ if (!section.fields) return
+ section.fields = section.fields.map(
+ (field) => field.fieldname || field.name,
+ )
+ })
})
loading.value = true
call(
@@ -124,7 +127,7 @@ function saveChanges() {
{
doctype: _doctype.value,
type: 'Quick Entry',
- layout: JSON.stringify(_sections),
+ layout: JSON.stringify(_tabs),
},
).then(() => {
loading.value = false
diff --git a/frontend/src/components/Modals/SidePanelModal.vue b/frontend/src/components/Modals/SidePanelModal.vue
index 06468737..6bd24b18 100644
--- a/frontend/src/components/Modals/SidePanelModal.vue
+++ b/frontend/src/components/Modals/SidePanelModal.vue
@@ -29,18 +29,20 @@
size="sm"
/>
-
+
sections?.data,
+ () => tabs?.data,
() => {
dirty.value =
- JSON.stringify(sections?.data) !== JSON.stringify(sections?.originalData)
+ JSON.stringify(tabs?.data) !== JSON.stringify(tabs?.originalData)
},
{ deep: true },
)
@@ -128,18 +130,20 @@ onMounted(() => useDebounceFn(reload, 100)())
function reload() {
nextTick(() => {
- sections.params = getParams()
- sections.reload()
+ tabs.params = getParams()
+ tabs.reload()
})
}
function saveChanges() {
- let _sections = JSON.parse(JSON.stringify(sections.data))
- _sections.forEach((section) => {
- if (!section.fields) return
- section.fields = section.fields
- .map((field) => field.fieldname || field.name)
- .filter(Boolean)
+ let _tabs = JSON.parse(JSON.stringify(tabs.data))
+ _tabs.forEach((tab) => {
+ tab.sections.forEach((section) => {
+ if (!section.fields) return
+ section.fields = section.fields
+ .map((field) => field.fieldname || field.name)
+ .filter(Boolean)
+ })
})
loading.value = true
call(
@@ -147,7 +151,7 @@ function saveChanges() {
{
doctype: _doctype.value,
type: 'Side Panel',
- layout: JSON.stringify(_sections),
+ layout: JSON.stringify(_tabs),
},
).then(() => {
loading.value = false
diff --git a/frontend/src/components/Settings/SettingsPage.vue b/frontend/src/components/Settings/SettingsPage.vue
index d3e60eb0..e8d6c876 100644
--- a/frontend/src/components/Settings/SettingsPage.vue
+++ b/frontend/src/components/Settings/SettingsPage.vue
@@ -12,11 +12,7 @@
/>
-
+
@@ -98,7 +94,7 @@ const data = createDocumentResource({
},
})
-const sections = computed(() => {
+const tabs = computed(() => {
if (!fields.data) return []
let _sections = []
let fieldsData = fields.data
@@ -138,7 +134,7 @@ const sections = computed(() => {
}
})
- return _sections
+ return [{ no_tabs: true, sections: _sections }]
})
function update() {
@@ -148,7 +144,8 @@ function update() {
}
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) {
if (
(field.mandatory ||