From 16d49c4b9df6dbb536226d6d67a9b55a4f9591a4 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 31 Dec 2024 19:32:51 +0530 Subject: [PATCH 01/40] fix: render tabs, sections & columns --- .../crm_fields_layout/crm_fields_layout.json | 4 +- .../crm_fields_layout/crm_fields_layout.py | 36 +- frontend/src/components/FieldLayout.vue | 386 ++++++++++-------- 3 files changed, 227 insertions(+), 199 deletions(-) diff --git a/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.json b/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.json index 7094e58d5..ef784e799 100644 --- a/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.json +++ b/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.json @@ -37,7 +37,7 @@ "fieldname": "layout", "fieldtype": "Code", "label": "Layout", - "options": "JS" + "options": "JSON" }, { "fieldname": "column_break_post", @@ -46,7 +46,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2024-12-29 12:58:54.280569", + "modified": "2024-12-31 19:06:24.679782", "modified_by": "Administrator", "module": "FCRM", "name": "CRM Fields Layout", 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 23552e5ce..bdd41568c 100644 --- a/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py +++ b/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py @@ -34,29 +34,31 @@ def get_fields_layout(doctype: str, type: str): allowed_fields = [] for tab in tabs: for section in tab.get("sections"): - if not section.get("fields"): - continue - allowed_fields.extend(section.get("fields")) + for column in section.get("columns"): + if not column.get("fields"): + continue + allowed_fields.extend(column.get("fields")) fields = frappe.get_meta(doctype).fields fields = [field for field in fields if field.fieldname in allowed_fields] 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: - field = { - "label": _(field.label), - "name": field.fieldname, - "type": field.fieldtype, - "options": getOptions(field), - "mandatory": field.reqd, - "read_only": field.read_only, - "placeholder": field.get("placeholder"), - "filters": field.get("link_filters"), - } - section["fields"][section.get("fields").index(field["name"])] = field + for column in section.get("columns") if section.get("columns") else []: + for field in column.get("fields") if column.get("fields") else []: + field = next((f for f in fields if f.fieldname == field), None) + if field: + field = { + "label": _(field.label), + "name": field.fieldname, + "type": field.fieldtype, + "options": getOptions(field), + "mandatory": field.reqd, + "read_only": field.read_only, + "placeholder": field.get("placeholder"), + "filters": field.get("link_filters"), + } + column["fields"][column.get("fields").index(field["name"])] = field return tabs or [] diff --git a/frontend/src/components/FieldLayout.vue b/frontend/src/components/FieldLayout.vue index fa75b303e..2da46961d 100644 --- a/frontend/src/components/FieldLayout.vue +++ b/frontend/src/components/FieldLayout.vue @@ -36,196 +36,213 @@ collapseIconPosition="right" >
-
-
-
- {{ __(field.label) }} - * +
+ {{ column.label }} +
+
+
+
-
- - - - - -
+ {{ __(field.label) }} + * +
-
- - - - - - - - - - - - - -
@@ -272,14 +289,23 @@ const hasTabs = computed(() => !props.tabs[0].no_tabs) const _tabs = computed(() => { return props.tabs.map((tab) => { tab.sections = tab.sections.map((section) => { - section.fields = section.fields.filter( - (field) => - (field.type == 'Check' || - (field.read_only && props.data[field.name]) || - !field.read_only) && - (!field.depends_on || field.display_via_depends_on) && - !field.hidden, - ) + section.columns = section.columns.map((column) => { + column.fields = column.fields.map((field) => { + if (field.type == 'Link' && field.options == 'User') { + field.type = 'User' + } + if ( + (field.type == 'Check' || + (field.read_only && props.data[field.name]) || + !field.read_only) && + (!field.depends_on || field.display_via_depends_on) && + !field.hidden + ) { + return field + } + }) + return column + }) return section }) return tab From 0b4ee64a36b0fc8dd93cf6c9ccfd4bb100ef5a41 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 31 Dec 2024 22:28:19 +0530 Subject: [PATCH 02/40] patch: convert old fields layout format to new format --- crm/patches.txt | 3 +- .../update_fields_layout_to_new_format.py | 77 +++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 crm/patches/v1_0/update_fields_layout_to_new_format.py diff --git a/crm/patches.txt b/crm/patches.txt index a0e91a3dc..e30c693a5 100644 --- a/crm/patches.txt +++ b/crm/patches.txt @@ -8,4 +8,5 @@ crm.patches.v1_0.move_crm_note_data_to_fcrm_note crm.patches.v1_0.create_email_template_custom_fields crm.patches.v1_0.create_default_fields_layout #10/12/2024 crm.patches.v1_0.create_default_sidebar_fields_layout -crm.patches.v1_0.update_deal_quick_entry_layout \ No newline at end of file +crm.patches.v1_0.update_deal_quick_entry_layout +crm.patches.v1_0.update_fields_layout_to_new_format \ No newline at end of file diff --git a/crm/patches/v1_0/update_fields_layout_to_new_format.py b/crm/patches/v1_0/update_fields_layout_to_new_format.py new file mode 100644 index 000000000..b41925ab7 --- /dev/null +++ b/crm/patches/v1_0/update_fields_layout_to_new_format.py @@ -0,0 +1,77 @@ +import json +from math import ceil + +import frappe + + +def execute(): + layouts = frappe.get_all("CRM Fields Layout", fields=["name", "layout", "type"]) + + for layout in layouts: + old_layout = layout.layout + new_layout = get_new_layout(old_layout, layout.type) + + frappe.db.set_value("CRM Fields Layout", layout.name, "layout", new_layout) + + +def get_new_layout(old_layout, type): + if isinstance(old_layout, str): + old_layout = json.loads(old_layout) + new_layout = [] + already_converted = False + + starts_with_sections = False + + if not old_layout[0].get("sections"): + starts_with_sections = True + + if starts_with_sections: + old_layout = [{"sections": old_layout}] + + for tab in old_layout: + new_tab = tab.copy() + new_tab["sections"] = [] + for section in tab.get("sections"): + if "contacts" in section: + new_tab["sections"].append(section) + continue + if isinstance(section.get("columns"), list): + already_converted = True + break + column_count = section.get("columns") or 3 + if type == "Side Panel": + column_count = 1 + fields = section.get("fields") or [] + + new_section = section.copy() + + if "fields" in new_section: + new_section.pop("fields") + new_section["columns"] = [] + + if len(fields) == 0: + new_section["columns"].append({"fields": []}) + new_tab["sections"].append(new_section) + continue + + if len(fields) == 1 and column_count > 1: + new_section["columns"].append({"fields": fields[0]}) + new_section["columns"].append({"fields": []}) + new_tab["sections"].append(new_section) + continue + + fields_per_column = ceil(len(fields) / column_count) + for i in range(column_count): + new_column = { + "fields": fields[i * fields_per_column: (i + 1) * fields_per_column] + } + new_section["columns"].append(new_column) + new_tab["sections"].append(new_section) + new_layout.append(new_tab) + + if starts_with_sections: + new_layout = new_layout[0].get("sections") + + if already_converted: + return json.dumps(old_layout) + return json.dumps(new_layout) From 37f357ac0802fd51f462d96c15172c7349683925 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 31 Dec 2024 22:30:22 +0530 Subject: [PATCH 03/40] fix: render fields in side panel, quick entry & data tab in new format --- crm/api/doc.py | 23 +- .../Controls/GridRowFieldsModal.vue | 10 +- .../src/components/Modals/ContactModal.vue | 10 +- .../src/components/Modals/DataFieldsModal.vue | 10 +- frontend/src/components/Modals/DealModal.vue | 26 +- frontend/src/components/Modals/LeadModal.vue | 24 +- .../components/Modals/OrganizationModal.vue | 10 +- .../src/components/Modals/QuickEntryModal.vue | 10 +- .../src/components/Modals/SidePanelModal.vue | 12 +- frontend/src/pages/Contact.vue | 273 +++++++++--------- frontend/src/pages/Deal.vue | 6 +- frontend/src/pages/Lead.vue | 2 +- frontend/src/pages/MobileContact.vue | 269 +++++++++-------- frontend/src/pages/MobileDeal.vue | 8 +- frontend/src/pages/MobileLead.vue | 2 +- frontend/src/pages/MobileOrganization.vue | 54 ++-- frontend/src/pages/Organization.vue | 55 ++-- 17 files changed, 408 insertions(+), 396 deletions(-) diff --git a/crm/api/doc.py b/crm/api/doc.py index 64d14f6f2..2e1e0d5c7 100644 --- a/crm/api/doc.py +++ b/crm/api/doc.py @@ -593,17 +593,18 @@ def get_sidebar_fields(doctype, name): for section in layout: section["name"] = section.get("name") or section.get("label") - for field in section.get("fields") if section.get("fields") else []: - field_obj = next((f for f in fields if f.fieldname == field), None) - if field_obj: - if field_obj.permlevel > 0: - field_has_write_access = field_obj.permlevel in has_write_access_to_permlevels - field_has_read_access = field_obj.permlevel in has_read_access_to_permlevels - if not field_has_write_access and field_has_read_access: - field_obj.read_only = 1 - if not field_has_read_access and not field_has_write_access: - field_obj.hidden = 1 - section["fields"][section.get("fields").index(field)] = get_field_obj(field_obj) + for column in section.get("columns") if section.get("columns") else []: + for field in column.get("fields") if column.get("fields") else []: + field_obj = next((f for f in fields if f.fieldname == field), None) + if field_obj: + if field_obj.permlevel > 0: + field_has_write_access = field_obj.permlevel in has_write_access_to_permlevels + field_has_read_access = field_obj.permlevel in has_read_access_to_permlevels + if not field_has_write_access and field_has_read_access: + field_obj.read_only = 1 + if not field_has_read_access and not field_has_write_access: + field_obj.hidden = 1 + column["fields"][column.get("fields").index(field)] = get_field_obj(field_obj) fields_meta = {} for field in fields: diff --git a/frontend/src/components/Controls/GridRowFieldsModal.vue b/frontend/src/components/Controls/GridRowFieldsModal.vue index 4c18383fd..894baf724 100644 --- a/frontend/src/components/Controls/GridRowFieldsModal.vue +++ b/frontend/src/components/Controls/GridRowFieldsModal.vue @@ -101,10 +101,12 @@ function saveChanges() { _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, - ) + section.columns.forEach((column) => { + if (!column.fields) return + column.fields = column.fields.map( + (field) => field.fieldname || field.name, + ) + }) }) }) loading.value = true diff --git a/frontend/src/components/Modals/ContactModal.vue b/frontend/src/components/Modals/ContactModal.vue index 465610f9c..88c9953f2 100644 --- a/frontend/src/components/Modals/ContactModal.vue +++ b/frontend/src/components/Modals/ContactModal.vue @@ -125,10 +125,12 @@ const tabs = createResource({ transform: (_tabs) => { return _tabs.forEach((tab) => { tab.sections.forEach((section) => { - section.fields.forEach((field) => { - if (field.type === 'Table') { - _contact.value[field.name] = [] - } + section.columns.forEach((column) => { + column.fields.forEach((field) => { + if (field.type === 'Table') { + _contact.value[field.name] = [] + } + }) }) }) }) diff --git a/frontend/src/components/Modals/DataFieldsModal.vue b/frontend/src/components/Modals/DataFieldsModal.vue index 87b4ce627..04c3c6f7b 100644 --- a/frontend/src/components/Modals/DataFieldsModal.vue +++ b/frontend/src/components/Modals/DataFieldsModal.vue @@ -101,10 +101,12 @@ function saveChanges() { _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, - ) + section.columns.forEach((column) => { + if (!column.fields) return + column.fields = column.fields.map( + (field) => field.fieldname || field.name, + ) + }) }) }) loading.value = true diff --git a/frontend/src/components/Modals/DealModal.vue b/frontend/src/components/Modals/DealModal.vue index 492436721..f2b97ed80 100644 --- a/frontend/src/components/Modals/DealModal.vue +++ b/frontend/src/components/Modals/DealModal.vue @@ -109,18 +109,20 @@ const tabs = createResource({ 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' - } - - if (field.type === 'Table') { - deal[field.name] = [] - } + section.columns.forEach((column) => { + column.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' + } + + if (field.type === 'Table') { + deal[field.name] = [] + } + }) }) }) }) diff --git a/frontend/src/components/Modals/LeadModal.vue b/frontend/src/components/Modals/LeadModal.vue index 848d6cb09..08f0ccad9 100644 --- a/frontend/src/components/Modals/LeadModal.vue +++ b/frontend/src/components/Modals/LeadModal.vue @@ -71,18 +71,20 @@ const tabs = createResource({ 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' - } + section.columns.forEach((column) => { + column.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' + } - if (field.type === 'Table') { - lead[field.name] = [] - } + if (field.type === 'Table') { + lead[field.name] = [] + } + }) }) }) }) diff --git a/frontend/src/components/Modals/OrganizationModal.vue b/frontend/src/components/Modals/OrganizationModal.vue index 022dadd6c..8818f0e43 100644 --- a/frontend/src/components/Modals/OrganizationModal.vue +++ b/frontend/src/components/Modals/OrganizationModal.vue @@ -124,10 +124,12 @@ const tabs = createResource({ transform: (_tabs) => { return _tabs.forEach((tab) => { tab.sections.forEach((section) => { - section.fields.forEach((field) => { - if (field.type === 'Table') { - _organization.value[field.name] = [] - } + section.columns.forEach((column) => { + column.fields.forEach((field) => { + if (field.type === 'Table') { + _organization.value[field.name] = [] + } + }) }) }) }) diff --git a/frontend/src/components/Modals/QuickEntryModal.vue b/frontend/src/components/Modals/QuickEntryModal.vue index 4f32a7e5d..c9e83790c 100644 --- a/frontend/src/components/Modals/QuickEntryModal.vue +++ b/frontend/src/components/Modals/QuickEntryModal.vue @@ -99,10 +99,12 @@ function saveChanges() { _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, - ) + section.columns.forEach((column) => { + if (!column.fields) return + column.fields = column.fields.map( + (field) => field.fieldname || field.name, + ) + }) }) }) loading.value = true diff --git a/frontend/src/components/Modals/SidePanelModal.vue b/frontend/src/components/Modals/SidePanelModal.vue index 2302cb003..a209f6bae 100644 --- a/frontend/src/components/Modals/SidePanelModal.vue +++ b/frontend/src/components/Modals/SidePanelModal.vue @@ -51,7 +51,7 @@ :opened="section.opened" > @@ -129,10 +129,12 @@ function saveChanges() { 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) + section.columns.forEach((column) => { + if (!column.fields) return + column.fields = column.fields + .map((field) => field.fieldname || field.name) + .filter(Boolean) + }) }) }) loading.value = true diff --git a/frontend/src/pages/Contact.vue b/frontend/src/pages/Contact.vue index db0fa94a8..68ada588d 100644 --- a/frontend/src/pages/Contact.vue +++ b/frontend/src/pages/Contact.vue @@ -139,8 +139,8 @@ { - return { - ...section, - fields: computed(() => - section.fields.map((field) => { - if (field.name === 'email_id') { - return { - ...field, - type: 'dropdown', - options: - contact.data?.email_ids?.map((email) => { - return { - name: email.name, - value: email.email_id, - selected: email.email_id === contact.data.email_id, - placeholder: 'john@doe.com', - onClick: () => { - _contact.value.email_id = email.email_id - setAsPrimary('email', email.email_id) - }, - onSave: (option, isNew) => { - if (isNew) { - createNew('email', option.value) - if (contact.data.email_ids.length === 1) { - _contact.value.email_id = option.value - } - } else { - editOption( - 'Contact Email', - option.name, - 'email_id', - option.value, - ) + section.columns = section.columns.map((column) => { + column.fields = column.fields.map((field) => { + if (field.name === 'email_id') { + return { + ...field, + read_only: false, + type: 'dropdown', + options: + contact.data?.email_ids?.map((email) => { + return { + name: email.name, + value: email.email_id, + selected: email.email_id === contact.data.email_id, + placeholder: 'john@doe.com', + onClick: () => { + _contact.value.email_id = email.email_id + setAsPrimary('email', email.email_id) + }, + onSave: (option, isNew) => { + if (isNew) { + createNew('email', option.value) + if (contact.data.email_ids.length === 1) { + _contact.value.email_id = option.value } - }, - onDelete: async (option, isNew) => { - contact.data.email_ids = contact.data.email_ids.filter( - (email) => email.name !== option.name, + } else { + editOption( + 'Contact Email', + option.name, + 'email_id', + option.value, ) - !isNew && - (await deleteOption('Contact Email', option.name)) - if (_contact.value.email_id === option.value) { - if (contact.data.email_ids.length === 0) { - _contact.value.email_id = '' - } else { - _contact.value.email_id = contact.data.email_ids.find( - (email) => email.is_primary, - )?.email_id - } - } - }, - } - }) || [], - create: () => { - contact.data?.email_ids?.push({ - name: 'new-1', - value: '', - selected: false, - isNew: true, - }) - }, - } - } else if (field.name === 'mobile_no') { - return { - ...field, - type: 'dropdown', - options: - contact.data?.phone_nos?.map((phone) => { - return { - name: phone.name, - value: phone.phone, - selected: phone.phone === contact.data.actual_mobile_no, - onClick: () => { - _contact.value.actual_mobile_no = phone.phone - _contact.value.mobile_no = phone.phone - setAsPrimary('mobile_no', phone.phone) - }, - onSave: (option, isNew) => { - if (isNew) { - createNew('phone', option.value) - if (contact.data.phone_nos.length === 1) { - _contact.value.actual_mobile_no = option.value - } + } + }, + onDelete: async (option, isNew) => { + contact.data.email_ids = contact.data.email_ids.filter( + (email) => email.name !== option.name, + ) + !isNew && (await deleteOption('Contact Email', option.name)) + if (_contact.value.email_id === option.value) { + if (contact.data.email_ids.length === 0) { + _contact.value.email_id = '' } else { - editOption( - 'Contact Phone', - option.name, - 'phone', - option.value, - ) + _contact.value.email_id = contact.data.email_ids.find( + (email) => email.is_primary, + )?.email_id + } + } + }, + } + }) || [], + create: () => { + contact.data?.email_ids?.push({ + name: 'new-1', + value: '', + selected: false, + isNew: true, + }) + }, + } + } else if (field.name === 'mobile_no') { + return { + ...field, + read_only: false, + type: 'dropdown', + options: + contact.data?.phone_nos?.map((phone) => { + return { + name: phone.name, + value: phone.phone, + selected: phone.phone === contact.data.actual_mobile_no, + onClick: () => { + _contact.value.actual_mobile_no = phone.phone + _contact.value.mobile_no = phone.phone + setAsPrimary('mobile_no', phone.phone) + }, + onSave: (option, isNew) => { + if (isNew) { + createNew('phone', option.value) + if (contact.data.phone_nos.length === 1) { + _contact.value.actual_mobile_no = option.value } - }, - onDelete: async (option, isNew) => { - contact.data.phone_nos = contact.data.phone_nos.filter( - (phone) => phone.name !== option.name, + } else { + editOption( + 'Contact Phone', + option.name, + 'phone', + option.value, ) - !isNew && - (await deleteOption('Contact Phone', option.name)) - if (_contact.value.actual_mobile_no === option.value) { - if (contact.data.phone_nos.length === 0) { - _contact.value.actual_mobile_no = '' - } else { - _contact.value.actual_mobile_no = - contact.data.phone_nos.find( - (phone) => phone.is_primary_mobile_no, - )?.phone - } + } + }, + onDelete: async (option, isNew) => { + contact.data.phone_nos = contact.data.phone_nos.filter( + (phone) => phone.name !== option.name, + ) + !isNew && (await deleteOption('Contact Phone', option.name)) + if (_contact.value.actual_mobile_no === option.value) { + if (contact.data.phone_nos.length === 0) { + _contact.value.actual_mobile_no = '' + } else { + _contact.value.actual_mobile_no = + contact.data.phone_nos.find( + (phone) => phone.is_primary_mobile_no, + )?.phone } - }, - } - }) || [], - create: () => { - contact.data?.phone_nos?.push({ - name: 'new-1', - value: '', - selected: false, - isNew: true, - }) - }, - } - } else if (field.name === 'address') { - return { - ...field, - create: (value, close) => { - _contact.value.address = value - _address.value = {} - showAddressModal.value = true - close() - }, - edit: async (addr) => { - _address.value = await call('frappe.client.get', { - doctype: 'Address', - name: addr, - }) - showAddressModal.value = true - }, - } - } else { - return field + } + }, + } + }) || [], + create: () => { + contact.data?.phone_nos?.push({ + name: 'new-1', + value: '', + selected: false, + isNew: true, + }) + }, } - }), - ), - } + } else if (field.name === 'address') { + return { + ...field, + create: (value, close) => { + _contact.value.address = value + _address.value = {} + showAddressModal.value = true + close() + }, + edit: async (addr) => { + _address.value = await call('frappe.client.get', { + doctype: 'Address', + name: addr, + }) + showAddressModal.value = true + }, + } + } else { + return field + } + }) + return column + }) + return section }) } diff --git a/frontend/src/pages/Deal.vue b/frontend/src/pages/Deal.vue index 2ee383c69..6154ecc8b 100644 --- a/frontend/src/pages/Deal.vue +++ b/frontend/src/pages/Deal.vue @@ -168,8 +168,8 @@ { if (section.name == 'contacts_section') return - section.fields.forEach((field) => { + section.columns[0].fields.forEach((field) => { if (field.name == 'organization') { field.create = (value, close) => { _organization.value.organization_name = value diff --git a/frontend/src/pages/Lead.vue b/frontend/src/pages/Lead.vue index 5e768d422..ba5863649 100644 --- a/frontend/src/pages/Lead.vue +++ b/frontend/src/pages/Lead.vue @@ -183,7 +183,7 @@ :opened="section.opened" >
{ - return { - ...section, - fields: computed(() => - section.fields.map((field) => { - if (field.name === 'email_id') { - return { - ...field, - type: 'dropdown', - options: - contact.data?.email_ids?.map((email) => { - return { - name: email.name, - value: email.email_id, - selected: email.email_id === contact.data.email_id, - placeholder: 'john@doe.com', - onClick: () => { - _contact.value.email_id = email.email_id - setAsPrimary('email', email.email_id) - }, - onSave: (option, isNew) => { - if (isNew) { - createNew('email', option.value) - if (contact.data.email_ids.length === 1) { - _contact.value.email_id = option.value - } - } else { - editOption( - 'Contact Email', - option.name, - 'email_id', - option.value, - ) + section.columns = section.columns.map((column) => { + column.fields = column.fields.map((field) => { + if (field.name === 'email_id') { + return { + ...field, + type: 'dropdown', + options: + contact.data?.email_ids?.map((email) => { + return { + name: email.name, + value: email.email_id, + selected: email.email_id === contact.data.email_id, + placeholder: 'john@doe.com', + onClick: () => { + _contact.value.email_id = email.email_id + setAsPrimary('email', email.email_id) + }, + onSave: (option, isNew) => { + if (isNew) { + createNew('email', option.value) + if (contact.data.email_ids.length === 1) { + _contact.value.email_id = option.value } - }, - onDelete: async (option, isNew) => { - contact.data.email_ids = contact.data.email_ids.filter( - (email) => email.name !== option.name, + } else { + editOption( + 'Contact Email', + option.name, + 'email_id', + option.value, ) - !isNew && - (await deleteOption('Contact Email', option.name)) - if (_contact.value.email_id === option.value) { - if (contact.data.email_ids.length === 0) { - _contact.value.email_id = '' - } else { - _contact.value.email_id = contact.data.email_ids.find( - (email) => email.is_primary, - )?.email_id - } - } - }, - } - }) || [], - create: () => { - contact.data?.email_ids?.push({ - name: 'new-1', - value: '', - selected: false, - isNew: true, - }) - }, - } - } else if (field.name === 'mobile_no') { - return { - ...field, - type: 'dropdown', - options: - contact.data?.phone_nos?.map((phone) => { - return { - name: phone.name, - value: phone.phone, - selected: phone.phone === contact.data.actual_mobile_no, - onClick: () => { - _contact.value.actual_mobile_no = phone.phone - _contact.value.mobile_no = phone.phone - setAsPrimary('mobile_no', phone.phone) - }, - onSave: (option, isNew) => { - if (isNew) { - createNew('phone', option.value) - if (contact.data.phone_nos.length === 1) { - _contact.value.actual_mobile_no = option.value - } + } + }, + onDelete: async (option, isNew) => { + contact.data.email_ids = contact.data.email_ids.filter( + (email) => email.name !== option.name, + ) + !isNew && (await deleteOption('Contact Email', option.name)) + if (_contact.value.email_id === option.value) { + if (contact.data.email_ids.length === 0) { + _contact.value.email_id = '' } else { - editOption( - 'Contact Phone', - option.name, - 'phone', - option.value, - ) + _contact.value.email_id = contact.data.email_ids.find( + (email) => email.is_primary, + )?.email_id + } + } + }, + } + }) || [], + create: () => { + contact.data?.email_ids?.push({ + name: 'new-1', + value: '', + selected: false, + isNew: true, + }) + }, + } + } else if (field.name === 'mobile_no') { + return { + ...field, + type: 'dropdown', + options: + contact.data?.phone_nos?.map((phone) => { + return { + name: phone.name, + value: phone.phone, + selected: phone.phone === contact.data.actual_mobile_no, + onClick: () => { + _contact.value.actual_mobile_no = phone.phone + _contact.value.mobile_no = phone.phone + setAsPrimary('mobile_no', phone.phone) + }, + onSave: (option, isNew) => { + if (isNew) { + createNew('phone', option.value) + if (contact.data.phone_nos.length === 1) { + _contact.value.actual_mobile_no = option.value } - }, - onDelete: async (option, isNew) => { - contact.data.phone_nos = contact.data.phone_nos.filter( - (phone) => phone.name !== option.name, + } else { + editOption( + 'Contact Phone', + option.name, + 'phone', + option.value, ) - !isNew && - (await deleteOption('Contact Phone', option.name)) - if (_contact.value.actual_mobile_no === option.value) { - if (contact.data.phone_nos.length === 0) { - _contact.value.actual_mobile_no = '' - } else { - _contact.value.actual_mobile_no = - contact.data.phone_nos.find( - (phone) => phone.is_primary_mobile_no, - )?.phone - } + } + }, + onDelete: async (option, isNew) => { + contact.data.phone_nos = contact.data.phone_nos.filter( + (phone) => phone.name !== option.name, + ) + !isNew && (await deleteOption('Contact Phone', option.name)) + if (_contact.value.actual_mobile_no === option.value) { + if (contact.data.phone_nos.length === 0) { + _contact.value.actual_mobile_no = '' + } else { + _contact.value.actual_mobile_no = + contact.data.phone_nos.find( + (phone) => phone.is_primary_mobile_no, + )?.phone } - }, - } - }) || [], - create: () => { - contact.data?.phone_nos?.push({ - name: 'new-1', - value: '', - selected: false, - isNew: true, - }) - }, - } - } else if (field.name === 'address') { - return { - ...field, - create: (value, close) => { - _contact.value.address = value - _address.value = {} - showAddressModal.value = true - close() - }, - edit: async (addr) => { - _address.value = await call('frappe.client.get', { - doctype: 'Address', - name: addr, - }) - showAddressModal.value = true - }, - } - } else { - return field + } + }, + } + }) || [], + create: () => { + contact.data?.phone_nos?.push({ + name: 'new-1', + value: '', + selected: false, + isNew: true, + }) + }, } - }), - ), - } + } else if (field.name === 'address') { + return { + ...field, + create: (value, close) => { + _contact.value.address = value + _address.value = {} + showAddressModal.value = true + close() + }, + edit: async (addr) => { + _address.value = await call('frappe.client.get', { + doctype: 'Address', + name: addr, + }) + showAddressModal.value = true + }, + } + } else { + return field + } + }) + return column + }) + return section }) } diff --git a/frontend/src/pages/MobileDeal.vue b/frontend/src/pages/MobileDeal.vue index 8d5493db9..3af5ff1c3 100644 --- a/frontend/src/pages/MobileDeal.vue +++ b/frontend/src/pages/MobileDeal.vue @@ -99,8 +99,8 @@
{ if (section.name == 'contacts_section') return - section.fields.forEach((field) => { + section.columns[0].fields.forEach((field) => { if (field.name == 'organization') { field.create = (value, close) => { _organization.value.organization_name = value diff --git a/frontend/src/pages/MobileLead.vue b/frontend/src/pages/MobileLead.vue index 954a2d1b8..91e4b30b9 100644 --- a/frontend/src/pages/MobileLead.vue +++ b/frontend/src/pages/MobileLead.vue @@ -76,7 +76,7 @@ >
{ - return { - ...section, - fields: computed(() => - section.fields.map((field) => { - if (field.name === 'address') { - return { - ...field, - create: (value, close) => { - _organization.value.address = value - _address.value = {} - showAddressModal.value = true - close() - }, - edit: async (addr) => { - _address.value = await call('frappe.client.get', { - doctype: 'Address', - name: addr, - }) - showAddressModal.value = true - }, - } - } else { - return field + section.columns = section.columns.map((column) => { + column.fields = column.fields.map((field) => { + if (field.name === 'address') { + return { + ...field, + create: (value, close) => { + _organization.value.address = value + _address.value = {} + showAddressModal.value = true + close() + }, + edit: async (addr) => { + _address.value = await call('frappe.client.get', { + doctype: 'Address', + name: addr, + }) + showAddressModal.value = true + }, } - }), - ), - } + } else { + return field + } + }) + return column + }) + return section }) } diff --git a/frontend/src/pages/Organization.vue b/frontend/src/pages/Organization.vue index 2a1c7917f..777be19bd 100644 --- a/frontend/src/pages/Organization.vue +++ b/frontend/src/pages/Organization.vue @@ -124,9 +124,9 @@ { - return { - ...section, - fields: computed(() => - section.fields.map((field) => { - if (field.name === 'address') { - return { - ...field, - create: (value, close) => { - _organization.value.address = value - _address.value = {} - showAddressModal.value = true - close() - }, - edit: async (addr) => { - _address.value = await call('frappe.client.get', { - doctype: 'Address', - name: addr, - }) - showAddressModal.value = true - }, - } - } else { - return field + section.columns = section.columns.map((column) => { + column.fields = column.fields.map((field) => { + if (field.name === 'address') { + return { + ...field, + create: (value, close) => { + _organization.value.address = value + _address.value = {} + showAddressModal.value = true + close() + }, + edit: async (addr) => { + _address.value = await call('frappe.client.get', { + doctype: 'Address', + name: addr, + }) + showAddressModal.value = true + }, } - }), - ), - } + } else { + return field + } + }) + return column + }) + return section }) } From ca487d56764ff192ef00d97bdd399de7d38f2760 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 31 Dec 2024 22:55:13 +0530 Subject: [PATCH 04/40] fix: handle columns in side panel layout editor --- .../crm_fields_layout/crm_fields_layout.py | 2 ++ frontend/src/components/Modals/SidePanelModal.vue | 1 + frontend/src/components/SidePanelLayoutEditor.vue | 15 +++++++++++---- 3 files changed, 14 insertions(+), 4 deletions(-) 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 bdd41568c..077d333d5 100644 --- a/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py +++ b/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py @@ -34,6 +34,8 @@ def get_fields_layout(doctype: str, type: str): allowed_fields = [] for tab in tabs: for section in tab.get("sections"): + if "columns" not in section: + continue for column in section.get("columns"): if not column.get("fields"): continue diff --git a/frontend/src/components/Modals/SidePanelModal.vue b/frontend/src/components/Modals/SidePanelModal.vue index a209f6bae..fb0fa183d 100644 --- a/frontend/src/components/Modals/SidePanelModal.vue +++ b/frontend/src/components/Modals/SidePanelModal.vue @@ -129,6 +129,7 @@ function saveChanges() { let _tabs = JSON.parse(JSON.stringify(tabs.data)) _tabs.forEach((tab) => { tab.sections.forEach((section) => { + if (!section.columns) return section.columns.forEach((column) => { if (!column.fields) return column.fields = column.fields diff --git a/frontend/src/components/SidePanelLayoutEditor.vue b/frontend/src/components/SidePanelLayoutEditor.vue index 99d84da8d..b92130708 100644 --- a/frontend/src/components/SidePanelLayoutEditor.vue +++ b/frontend/src/components/SidePanelLayoutEditor.vue @@ -54,7 +54,7 @@
@@ -124,7 +127,11 @@ variant="subtle" :label="__('Add Section')" @click=" - sections.push({ label: __('New Section'), opened: true, fields: [] }) + sections.push({ + label: __('New Section'), + opened: true, + columns: [{ fields: [] }], + }) " > @@ -208,7 +215,7 @@ const props = defineProps({ const tabIndex = ref(0) const slotName = computed(() => { - if (props.tabs.length == 1 && props.tabs[0].no_tabs) { + if (props.tabs.length == 1 && !props.tabs[0].label) { return 'prefix' } return 'default' @@ -252,7 +259,9 @@ const fields = createResource({ for (let tab of props.tabs) { for (let section of tab.sections) { - existingFields = existingFields.concat(section.fields) + for (let column of section.columns) { + existingFields = existingFields.concat(column.fields) + } } } @@ -266,22 +275,23 @@ const fields = createResource({ }) function addTab() { - if (props.tabs.length == 1 && props.tabs[0].no_tabs) { - delete props.tabs[0].no_tabs + if (props.tabs.length == 1 && !props.tabs[0].label) { + props.tabs[0].label = __('New Tab') return } + props.tabs.push({ label: __('New Tab'), sections: [] }) tabIndex.value = props.tabs.length ? props.tabs.length - 1 : 0 } -function addField(section, field) { +function addField(column, field) { if (!field) return let newFieldObj = { ...field, name: field.fieldname, type: field.fieldtype, } - section.fields.push(newFieldObj) + column.fields.push(newFieldObj) } function getTabOptions(tab) { @@ -296,7 +306,7 @@ function getTabOptions(tab) { icon: 'trash-2', onClick: () => { if (props.tabs.length == 1) { - props.tabs[0].no_tabs = true + props.tabs[0].label = '' return } props.tabs.splice(tabIndex.value, 1) diff --git a/frontend/src/components/Modals/ContactModal.vue b/frontend/src/components/Modals/ContactModal.vue index 88c9953f2..9a7570a93 100644 --- a/frontend/src/components/Modals/ContactModal.vue +++ b/frontend/src/components/Modals/ContactModal.vue @@ -161,7 +161,7 @@ const filteredSections = computed(() => { }) }) - return [{ no_tabs: true, sections: allSections }] + return [{ sections: allSections }] }) watch( diff --git a/frontend/src/components/Modals/DealModal.vue b/frontend/src/components/Modals/DealModal.vue index f2b97ed80..59b589a72 100644 --- a/frontend/src/components/Modals/DealModal.vue +++ b/frontend/src/components/Modals/DealModal.vue @@ -168,7 +168,7 @@ const filteredSections = computed(() => { } }) - return [{ no_tabs: true, sections: _filteredSections }] + return [{ sections: _filteredSections }] }) const dealStatuses = computed(() => { diff --git a/frontend/src/components/Modals/OrganizationModal.vue b/frontend/src/components/Modals/OrganizationModal.vue index 8818f0e43..1b5dfde09 100644 --- a/frontend/src/components/Modals/OrganizationModal.vue +++ b/frontend/src/components/Modals/OrganizationModal.vue @@ -160,7 +160,7 @@ const filteredSections = computed(() => { }) }) - return [{ no_tabs: true, sections: allSections }] + return [{ sections: allSections }] }) watch( diff --git a/frontend/src/components/Settings/SettingsPage.vue b/frontend/src/components/Settings/SettingsPage.vue index fdd74912d..dbde52bc5 100644 --- a/frontend/src/components/Settings/SettingsPage.vue +++ b/frontend/src/components/Settings/SettingsPage.vue @@ -106,12 +106,9 @@ const tabs = computed(() => { if (fieldsData[0].type != 'Tab Break') { let _sections = [] if (fieldsData[0].type != 'Section Break') { - _sections.push({ no_tabs: true, columns: [{ fields: [] }] }) + _sections.push({ columns: [{ fields: [] }] }) } - _tabs.push({ - no_tabs: true, - sections: _sections, - }) + _tabs.push({ sections: _sections }) } fieldsData.forEach((field) => { From 29d23d78c5aaf44b5d4bfa405ac9f7531b3df74b Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Wed, 1 Jan 2025 03:28:54 +0530 Subject: [PATCH 09/40] fix: allow adding/removing/editing columns --- .../Controls/GridRowFieldsModal.vue | 2 +- frontend/src/components/FieldLayoutEditor.vue | 362 ++++++++++-------- .../src/components/Modals/DataFieldsModal.vue | 2 +- .../src/components/Modals/QuickEntryModal.vue | 2 +- 4 files changed, 215 insertions(+), 153 deletions(-) diff --git a/frontend/src/components/Controls/GridRowFieldsModal.vue b/frontend/src/components/Controls/GridRowFieldsModal.vue index 894baf724..e5e95354f 100644 --- a/frontend/src/components/Controls/GridRowFieldsModal.vue +++ b/frontend/src/components/Controls/GridRowFieldsModal.vue @@ -1,5 +1,5 @@ @@ -381,14 +381,14 @@ const { getFormattedPercent, getFormattedFloat, getFormattedCurrency } = getMeta(props.doctype) const { isManager, getUser } = usersStore() -const emit = defineEmits(['update']) +const emit = defineEmits(['update', 'reload']) const data = defineModel() const showSidePanelModal = ref(false) const _sections = computed(() => { - if (!props.sections?.data?.length) return [] - return props.sections.data.map((section) => { + if (!props.sections?.length) return [] + return props.sections.map((section) => { if (section.columns?.length) { section.columns[0].fields = section.columns[0].fields.map((field) => { let df = field?.all_properties || {} diff --git a/frontend/src/pages/Contact.vue b/frontend/src/pages/Contact.vue index ca257883b..ccfab3c68 100644 --- a/frontend/src/pages/Contact.vue +++ b/frontend/src/pages/Contact.vue @@ -122,9 +122,10 @@ > diff --git a/frontend/src/pages/Deal.vue b/frontend/src/pages/Deal.vue index de5213ade..25a1c6072 100644 --- a/frontend/src/pages/Deal.vue +++ b/frontend/src/pages/Deal.vue @@ -119,10 +119,11 @@ >
diff --git a/frontend/src/pages/Organization.vue b/frontend/src/pages/Organization.vue index 615246e4f..9553203a5 100644 --- a/frontend/src/pages/Organization.vue +++ b/frontend/src/pages/Organization.vue @@ -107,9 +107,10 @@ >
From 24d60c9f12ee3fd7ad7e4e9f89e9974cf26115e7 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 2 Jan 2025 15:29:32 +0530 Subject: [PATCH 31/40] fix: hide section header if label is not set --- frontend/src/components/SidePanelLayout.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/SidePanelLayout.vue b/frontend/src/components/SidePanelLayout.vue index dbf77fe43..fd0688197 100644 --- a/frontend/src/components/SidePanelLayout.vue +++ b/frontend/src/components/SidePanelLayout.vue @@ -10,6 +10,7 @@