Merge pull request #222 from shariquerik/quick-entry-layout-builder
feat: Quick Entry Layout Builder
This commit is contained in:
commit
9da213c799
@ -8,31 +8,40 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"organization_tab",
|
"organization_tab",
|
||||||
|
"naming_series",
|
||||||
"organization",
|
"organization",
|
||||||
"website",
|
|
||||||
"territory",
|
|
||||||
"annual_revenue",
|
|
||||||
"close_date",
|
|
||||||
"probability",
|
|
||||||
"next_step",
|
"next_step",
|
||||||
|
"probability",
|
||||||
|
"column_break_ijan",
|
||||||
|
"status",
|
||||||
|
"close_date",
|
||||||
"deal_owner",
|
"deal_owner",
|
||||||
"contacts_tab",
|
"contacts_tab",
|
||||||
|
"contacts",
|
||||||
|
"contact",
|
||||||
|
"lead_details_tab",
|
||||||
|
"lead",
|
||||||
|
"source",
|
||||||
|
"column_break_wsde",
|
||||||
"lead_name",
|
"lead_name",
|
||||||
|
"organization_details_section",
|
||||||
|
"organization_name",
|
||||||
|
"website",
|
||||||
|
"no_of_employees",
|
||||||
|
"job_title",
|
||||||
|
"column_break_xbyf",
|
||||||
|
"territory",
|
||||||
|
"annual_revenue",
|
||||||
|
"industry",
|
||||||
|
"person_section",
|
||||||
|
"salutation",
|
||||||
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
"column_break_xjmy",
|
||||||
"email",
|
"email",
|
||||||
"mobile_no",
|
"mobile_no",
|
||||||
"phone",
|
"phone",
|
||||||
"contacts",
|
"gender",
|
||||||
"others_tab",
|
|
||||||
"naming_series",
|
|
||||||
"status",
|
|
||||||
"section_break_sygz",
|
|
||||||
"no_of_employees",
|
|
||||||
"column_break_nwob",
|
|
||||||
"job_title",
|
|
||||||
"section_break_eepu",
|
|
||||||
"lead",
|
|
||||||
"column_break_bqvs",
|
|
||||||
"source",
|
|
||||||
"sla_tab",
|
"sla_tab",
|
||||||
"sla",
|
"sla",
|
||||||
"sla_creation",
|
"sla_creation",
|
||||||
@ -88,14 +97,6 @@
|
|||||||
"label": "Lead",
|
"label": "Lead",
|
||||||
"options": "CRM Lead"
|
"options": "CRM Lead"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "section_break_eepu",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_bqvs",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "deal_owner",
|
"fieldname": "deal_owner",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
@ -142,12 +143,6 @@
|
|||||||
"label": "Contacts",
|
"label": "Contacts",
|
||||||
"options": "CRM Contacts"
|
"options": "CRM Contacts"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "others_tab",
|
|
||||||
"fieldtype": "Tab Break",
|
|
||||||
"label": "Others",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "organization_tab",
|
"fieldname": "organization_tab",
|
||||||
"fieldtype": "Tab Break",
|
"fieldtype": "Tab Break",
|
||||||
@ -235,14 +230,6 @@
|
|||||||
"label": "No. of Employees",
|
"label": "No. of Employees",
|
||||||
"options": "1-10\n11-50\n51-200\n201-500\n501-1000\n1000+"
|
"options": "1-10\n11-50\n51-200\n201-500\n501-1000\n1000+"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "section_break_sygz",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_nwob",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "job_title",
|
"fieldname": "job_title",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
@ -270,11 +257,81 @@
|
|||||||
"fieldname": "lead_name",
|
"fieldname": "lead_name",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Lead Name"
|
"label": "Lead Name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_ijan",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "lead_details_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Lead Details"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_wsde",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "organization_details_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Organization Details"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "organization_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Organization Name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_xbyf",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "industry",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Industry",
|
||||||
|
"options": "CRM Industry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "person_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Person"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "salutation",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Salutation",
|
||||||
|
"options": "Salutation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "first_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "First Name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "last_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Last Name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_xjmy",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "gender",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Gender",
|
||||||
|
"options": "Gender"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "contact",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Contact",
|
||||||
|
"options": "Contact"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-01-25 21:00:08.216020",
|
"modified": "2024-06-19 18:01:59.213811",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "FCRM",
|
"module": "FCRM",
|
||||||
"name": "CRM Deal",
|
"name": "CRM Deal",
|
||||||
|
|||||||
@ -113,19 +113,19 @@ def add_default_fields_layout():
|
|||||||
layouts = {
|
layouts = {
|
||||||
"CRM Lead-Quick Entry": {
|
"CRM Lead-Quick Entry": {
|
||||||
"doctype": "CRM Lead",
|
"doctype": "CRM Lead",
|
||||||
"layout": '[\n{\n"label": "Person",\n\t"fields": ["salutation", "first_name", "last_name", "email", "mobile_no", "gender"]\n},\n{\n"label": "Organization",\n\t"fields": ["organization", "website", "no_of_employees", "territory", "annual_revenue", "industry"]\n},\n{\n"label": "Other",\n"columns": 2,\n\t"fields": ["status", "lead_owner"]\n}\n]'
|
"layout": '[{"label":"Person","fields":["salutation","first_name","last_name","email","mobile_no", "gender"],"hideLabel":true},{"label":"Organization","fields":["organization","website","no_of_employees","territory","annual_revenue","industry"],"hideLabel":true,"hideBorder":false},{"label":"Other","columns":2,"fields":["status","lead_owner"],"hideLabel":true,"hideBorder":false}]'
|
||||||
},
|
},
|
||||||
"CRM Deal-Quick Entry": {
|
"CRM Deal-Quick Entry": {
|
||||||
"doctype": "CRM Deal",
|
"doctype": "CRM Deal",
|
||||||
"layout": '[\n{\n"label": "Select Organization",\n\t"fields": ["organization"]\n},\n{\n"label": "Organization Details",\n\t"fields": [{"label": "Organization Name", "name": "organization_name", "type": "Data"}, "website", "no_of_employees", "territory", "annual_revenue", {"label": "Industry", "name": "industry", "type": "Link", "options": "CRM Industry"}]\n},\n{\n"label": "Select Contact",\n\t"fields": [{"label": "Contact", "name": "contact", "type": "Link", "options": "Contact"}]\n},\n{\n"label": "Contact Details",\n\t"fields": [{"label": "Salutation", "name": "salutation", "type": "Link", "options": "Salutation"}, {"label": "First Name", "name": "first_name", "type": "Data"}, {"label": "Last Name", "name": "last_name", "type": "Data"}, "email", "mobile_no", {"label": "Gender", "name": "gender", "type": "Link", "options": "Gender"}]\n},\n{\n"label": "Other",\n"columns": 2,\n\t"fields": ["status", "deal_owner"]\n}\n]'
|
"layout": '[{"label": "Select Organization", "fields": ["organization"], "hideLabel": true, "editable": true}, {"label": "Organization Details", "fields": ["organization_name", "website", "no_of_employees", "territory", "annual_revenue", "industry"], "hideLabel": true, "editable": true}, {"label": "Select Contact", "fields": ["contact"], "hideLabel": true, "editable": true}, {"label": "Contact Details", "fields": ["salutation", "first_name", "last_name", "email", "mobile_no", "gender"], "hideLabel": true, "editable": true}, {"label": "Other", "columns": 2, "fields": ["status", "deal_owner"], "hideLabel": true}]'
|
||||||
},
|
},
|
||||||
"Contact-Quick Entry": {
|
"Contact-Quick Entry": {
|
||||||
"doctype": "Contact",
|
"doctype": "Contact",
|
||||||
"layout": '[\n{\n"label": "Salutation",\n"columns": 1,\n"fields": ["salutation"]\n},\n{\n"label": "Full Name",\n"columns": 2,\n"hideBorder": true,\n"fields": ["first_name", "last_name"]\n},\n{\n"label": "Email",\n"columns": 1,\n"hideBorder": true,\n"fields": ["email_id"]\n},\n{\n"label": "Mobile No. & Gender",\n"columns": 2,\n"hideBorder": true,\n"fields": ["mobile_no", "gender"]\n},\n{\n"label": "Organization",\n"columns": 1,\n"hideBorder": true,\n"fields": ["company_name"]\n},\n{\n"label": "Designation",\n"columns": 1,\n"hideBorder": true,\n"fields": ["designation"]\n}\n]'
|
"layout": '[{"label":"Salutation","columns":1,"fields":["salutation"],"hideLabel":true},{"label":"Full Name","columns":2,"hideBorder":true,"fields":["first_name","last_name"],"hideLabel":true},{"label":"Email","columns":1,"hideBorder":true,"fields":["email_id"],"hideLabel":true},{"label":"Mobile No. & Gender","columns":2,"hideBorder":true,"fields":["mobile_no","gender"],"hideLabel":true},{"label":"Organization","columns":1,"hideBorder":true,"fields":["company_name"],"hideLabel":true},{"label":"Designation","columns":1,"hideBorder":true,"fields":["designation"],"hideLabel":true}]'
|
||||||
},
|
},
|
||||||
"Organization-Quick Entry": {
|
"Organization-Quick Entry": {
|
||||||
"doctype": "CRM Organization",
|
"doctype": "CRM Organization",
|
||||||
"layout": '[\n{\n"label": "Organization Name",\n"columns": 1,\n"fields": ["organization_name"]\n},\n{\n"label": "Website & Revenue",\n"columns": 2,\n"hideBorder": true,\n"fields": ["website", "annual_revenue"]\n},\n{\n"label": "Territory",\n"columns": 1,\n"hideBorder": true,\n"fields": ["territory"]\n},\n{\n"label": "No of Employees & Industry",\n"columns": 2,\n"hideBorder": true,\n"fields": ["no_of_employees", "industry"]\n}\n]'
|
"layout": '[{"label":"Organization Name","columns":1,"fields":["organization_name"],"hideLabel":true},{"label":"Website & Revenue","columns":2,"hideBorder":true,"fields":["website","annual_revenue"],"hideLabel":true},{"label":"Territory","columns":1,"hideBorder":true,"fields":["territory"],"hideLabel":true},{"label":"No of Employees & Industry","columns":2,"hideBorder":true,"fields":["no_of_employees","industry"],"hideLabel":true}]'
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,3 +8,4 @@ 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_email_template_custom_fields
|
||||||
crm.patches.v1_0.create_default_fields_layout
|
crm.patches.v1_0.create_default_fields_layout
|
||||||
crm.patches.v1_0.create_default_sidebar_fields_layout
|
crm.patches.v1_0.create_default_sidebar_fields_layout
|
||||||
|
crm.patches.v1_0.update_deal_quick_entry_layout
|
||||||
15
crm/patches/v1_0/update_deal_quick_entry_layout.py
Normal file
15
crm/patches/v1_0/update_deal_quick_entry_layout.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import json
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
if not frappe.db.exists("CRM Fields Layout", "CRM Deal-Quick Entry"):
|
||||||
|
return
|
||||||
|
|
||||||
|
deal = frappe.db.get_value("CRM Fields Layout", "CRM Deal-Quick Entry", "layout")
|
||||||
|
|
||||||
|
layout = json.loads(deal)
|
||||||
|
for section in layout:
|
||||||
|
if section.get("label") in ["Select Organization", "Organization Details", "Select Contact", "Contact Details"]:
|
||||||
|
section["editable"] = True
|
||||||
|
|
||||||
|
frappe.db.set_value("CRM Fields Layout", "CRM Deal-Quick Entry", "layout", json.dumps(layout))
|
||||||
@ -6,6 +6,12 @@
|
|||||||
class="first:border-t-0 first:pt-0"
|
class="first:border-t-0 first:pt-0"
|
||||||
:class="section.hideBorder ? '' : 'border-t pt-4'"
|
:class="section.hideBorder ? '' : 'border-t pt-4'"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
v-if="!section.hideLabel"
|
||||||
|
class="flex h-7 mb-3 max-w-fit cursor-pointer items-center gap-2 text-base font-semibold leading-5"
|
||||||
|
>
|
||||||
|
{{ section.label }}
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="grid gap-4"
|
class="grid gap-4"
|
||||||
:class="
|
:class="
|
||||||
|
|||||||
18
frontend/src/components/Icons/RightSideLayoutIcon.vue
Normal file
18
frontend/src/components/Icons/RightSideLayoutIcon.vue
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="lucide lucide-panel-right-close"
|
||||||
|
>
|
||||||
|
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||||
|
<path d="M15 3v18" />
|
||||||
|
<path d="m8 9 3 3-3 3" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
@ -36,7 +36,7 @@
|
|||||||
<FormControl
|
<FormControl
|
||||||
v-else-if="
|
v-else-if="
|
||||||
['email', 'number', 'date', 'password', 'textarea'].includes(
|
['email', 'number', 'date', 'password', 'textarea'].includes(
|
||||||
field.type
|
field.type,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@ -137,7 +137,10 @@ const _fields = computed(() => {
|
|||||||
props.fields?.forEach((field) => {
|
props.fields?.forEach((field) => {
|
||||||
let df = field.all_properties
|
let df = field.all_properties
|
||||||
if (df?.depends_on) evaluate_depends_on(df.depends_on, field)
|
if (df?.depends_on) evaluate_depends_on(df.depends_on, field)
|
||||||
all_fields.push(field)
|
all_fields.push({
|
||||||
|
...field,
|
||||||
|
placeholder: field.placeholder || field.label,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
return all_fields
|
return all_fields
|
||||||
})
|
})
|
||||||
|
|||||||
113
frontend/src/components/Settings/QuickEntryLayout.vue
Normal file
113
frontend/src/components/Settings/QuickEntryLayout.vue
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col overflow-hidden">
|
||||||
|
<div class="flex flex-col gap-2 p-8 pb-5">
|
||||||
|
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5 mb-4">
|
||||||
|
<div>{{ __('Quick Entry Layout') }}</div>
|
||||||
|
<Badge
|
||||||
|
v-if="dirty"
|
||||||
|
:label="__('Not Saved')"
|
||||||
|
variant="subtle"
|
||||||
|
theme="orange"
|
||||||
|
/>
|
||||||
|
</h2>
|
||||||
|
<div class="flex gap-6 items-end">
|
||||||
|
<FormControl
|
||||||
|
class="flex-1"
|
||||||
|
type="select"
|
||||||
|
v-model="doctype"
|
||||||
|
:label="__('DocType')"
|
||||||
|
:options="['CRM Lead', 'CRM Deal', 'Contact', 'CRM Organization']"
|
||||||
|
@change="reload"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-row-reverse gap-2">
|
||||||
|
<Button
|
||||||
|
:loading="loading"
|
||||||
|
:label="__('Save')"
|
||||||
|
variant="solid"
|
||||||
|
@click="saveChanges"
|
||||||
|
/>
|
||||||
|
<Button :label="__('Reset')" @click="reload" />
|
||||||
|
<Button
|
||||||
|
:label="preview ? __('Hide Preview') : __('Show Preview')"
|
||||||
|
@click="preview = !preview"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="sections?.data" class="overflow-y-auto p-8 pt-3">
|
||||||
|
<div
|
||||||
|
class="rounded-xl h-full inline-block w-full px-4 pb-6 pt-5 sm:px-6 transform overflow-y-auto bg-white text-left align-middle shadow-xl transition-all"
|
||||||
|
>
|
||||||
|
<QuickEntryLayoutBuilder
|
||||||
|
v-if="!preview"
|
||||||
|
:sections="sections.data"
|
||||||
|
:doctype="doctype"
|
||||||
|
/>
|
||||||
|
<Fields v-else :sections="sections.data" :data="{}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import Fields from '@/components/Fields.vue'
|
||||||
|
import QuickEntryLayoutBuilder from '@/components/Settings/QuickEntryLayoutBuilder.vue'
|
||||||
|
import { useDebounceFn } from '@vueuse/core'
|
||||||
|
import { Badge, call, createResource } from 'frappe-ui'
|
||||||
|
import { ref, watch, onMounted } from 'vue'
|
||||||
|
|
||||||
|
const doctype = ref('CRM Lead')
|
||||||
|
const loading = ref(false)
|
||||||
|
const dirty = ref(false)
|
||||||
|
const preview = ref(false)
|
||||||
|
|
||||||
|
function getParams() {
|
||||||
|
return { doctype: doctype.value, type: 'Quick Entry' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const sections = createResource({
|
||||||
|
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||||
|
cache: ['quick-entry-sections', doctype.value],
|
||||||
|
params: getParams(),
|
||||||
|
onSuccess(data) {
|
||||||
|
sections.originalData = JSON.parse(JSON.stringify(data))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => sections?.data,
|
||||||
|
() => {
|
||||||
|
dirty.value =
|
||||||
|
JSON.stringify(sections?.data) !== JSON.stringify(sections?.originalData)
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(() => useDebounceFn(reload, 100)())
|
||||||
|
|
||||||
|
function reload() {
|
||||||
|
sections.params = getParams()
|
||||||
|
sections.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,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
loading.value = true
|
||||||
|
call(
|
||||||
|
'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.save_fields_layout',
|
||||||
|
{
|
||||||
|
doctype: doctype.value,
|
||||||
|
type: 'Quick Entry',
|
||||||
|
layout: JSON.stringify(_sections),
|
||||||
|
},
|
||||||
|
).then(() => {
|
||||||
|
loading.value = false
|
||||||
|
reload()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
220
frontend/src/components/Settings/QuickEntryLayoutBuilder.vue
Normal file
220
frontend/src/components/Settings/QuickEntryLayoutBuilder.vue
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Draggable :list="sections" item-key="label" class="flex flex-col">
|
||||||
|
<template #item="{ element: section }">
|
||||||
|
<div
|
||||||
|
class="py-2 first:pt-0"
|
||||||
|
:class="section.hideBorder ? '' : 'border-t first:border-t-0'"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between pb-2">
|
||||||
|
<div
|
||||||
|
class="flex h-7 max-w-fit cursor-pointer items-center gap-2 text-base font-semibold leading-5"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="!section.editingLabel"
|
||||||
|
:class="section.hideLabel ? 'text-gray-400' : ''"
|
||||||
|
>
|
||||||
|
{{ __(section.label) || __('Untitled') }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex gap-2 items-center">
|
||||||
|
<Input
|
||||||
|
v-model="section.label"
|
||||||
|
@keydown.enter="section.editingLabel = false"
|
||||||
|
@blur="section.editingLabel = false"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
v-if="section.editingLabel"
|
||||||
|
icon="check"
|
||||||
|
variant="ghost"
|
||||||
|
@click="section.editingLabel = false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Dropdown :options="getOptions(section)">
|
||||||
|
<template #default>
|
||||||
|
<Button variant="ghost">
|
||||||
|
<FeatherIcon name="more-horizontal" class="h-4" />
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Draggable
|
||||||
|
:list="section.fields"
|
||||||
|
group="fields"
|
||||||
|
item-key="label"
|
||||||
|
class="grid gap-2"
|
||||||
|
:class="
|
||||||
|
section.columns ? 'grid-cols-' + section.columns : 'grid-cols-3'
|
||||||
|
"
|
||||||
|
handle=".cursor-grab"
|
||||||
|
>
|
||||||
|
<template #item="{ element: field }">
|
||||||
|
<div
|
||||||
|
class="px-1.5 py-1 border rounded text-base text-gray-800 flex items-center justify-between gap-2"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<DragVerticalIcon class="h-3.5 cursor-grab" />
|
||||||
|
<div>{{ field.label }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
icon="x"
|
||||||
|
@click="
|
||||||
|
section.fields.splice(section.fields.indexOf(field), 1)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Draggable>
|
||||||
|
<Autocomplete
|
||||||
|
v-if="fields.data"
|
||||||
|
value=""
|
||||||
|
:options="fields.data"
|
||||||
|
@change="(e) => addField(section, e)"
|
||||||
|
>
|
||||||
|
<template #target="{ togglePopover }">
|
||||||
|
<div
|
||||||
|
class="grid gap-2 w-full"
|
||||||
|
:class="
|
||||||
|
section.columns
|
||||||
|
? 'grid-cols-' + section.columns
|
||||||
|
: 'grid-cols-3'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
class="mt-2 w-full !h-[38px] !border-gray-200"
|
||||||
|
variant="outline"
|
||||||
|
@click="togglePopover()"
|
||||||
|
:label="__('Add Field')"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<FeatherIcon name="plus" class="h-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #item-label="{ option }">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<div>{{ option.label }}</div>
|
||||||
|
<div class="text-gray-500 text-sm">
|
||||||
|
{{ `${option.fieldname} - ${option.fieldtype}` }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Autocomplete>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Draggable>
|
||||||
|
<div class="py-2 border-t">
|
||||||
|
<Button
|
||||||
|
class="w-full !h-[38px] !border-gray-200"
|
||||||
|
variant="outline"
|
||||||
|
:label="__('Add Section')"
|
||||||
|
@click="
|
||||||
|
sections.push({
|
||||||
|
label: __('New Section'),
|
||||||
|
opened: true,
|
||||||
|
fields: [],
|
||||||
|
})
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<FeatherIcon name="plus" class="h-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import Autocomplete from '@/components/frappe-ui/Autocomplete.vue'
|
||||||
|
import DragVerticalIcon from '@/components/Icons/DragVerticalIcon.vue'
|
||||||
|
import Draggable from 'vuedraggable'
|
||||||
|
import { Dropdown, createResource } from 'frappe-ui'
|
||||||
|
import { computed, watch } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
sections: Object,
|
||||||
|
doctype: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
const restrictedFieldTypes = [
|
||||||
|
'Table',
|
||||||
|
'Geolocation',
|
||||||
|
'Attach',
|
||||||
|
'Attach Image',
|
||||||
|
'HTML',
|
||||||
|
'Signature',
|
||||||
|
]
|
||||||
|
|
||||||
|
const params = computed(() => {
|
||||||
|
return {
|
||||||
|
doctype: props.doctype,
|
||||||
|
restricted_fieldtypes: restrictedFieldTypes,
|
||||||
|
as_array: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const fields = createResource({
|
||||||
|
url: 'crm.api.doc.get_fields_meta',
|
||||||
|
params: params.value,
|
||||||
|
cache: ['fieldsMeta', props.doctype],
|
||||||
|
auto: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
function addField(section, field) {
|
||||||
|
if (!field) return
|
||||||
|
section.fields.push(field)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOptions(section) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: 'Edit',
|
||||||
|
icon: 'edit',
|
||||||
|
onClick: () => (section.editingLabel = true),
|
||||||
|
condition: () => section.editable !== false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: section.hideLabel ? 'Show Label' : 'Hide Label',
|
||||||
|
icon: section.hideLabel ? 'eye' : 'eye-off',
|
||||||
|
onClick: () => (section.hideLabel = !section.hideLabel),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: section.hideBorder ? 'Show Border' : 'Hide Border',
|
||||||
|
icon: 'minus',
|
||||||
|
onClick: () => (section.hideBorder = !section.hideBorder),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Add Column',
|
||||||
|
icon: 'columns',
|
||||||
|
onClick: () =>
|
||||||
|
(section.columns = section.columns ? section.columns + 1 : 4),
|
||||||
|
condition: () => !section.columns || section.columns < 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Remove Column',
|
||||||
|
icon: 'columns',
|
||||||
|
onClick: () =>
|
||||||
|
(section.columns = section.columns ? section.columns - 1 : 2),
|
||||||
|
condition: () => !section.columns || section.columns > 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Remove Section',
|
||||||
|
icon: 'trash-2',
|
||||||
|
onClick: () => props.sections.splice(props.sections.indexOf(section), 1),
|
||||||
|
condition: () => section.editable !== false,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.doctype,
|
||||||
|
() => fields.fetch(params.value),
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
</script>
|
||||||
@ -40,10 +40,12 @@
|
|||||||
import ContactsIcon from '@/components/Icons/ContactsIcon.vue'
|
import ContactsIcon from '@/components/Icons/ContactsIcon.vue'
|
||||||
import WhatsAppIcon from '@/components/Icons/WhatsAppIcon.vue'
|
import WhatsAppIcon from '@/components/Icons/WhatsAppIcon.vue'
|
||||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||||
|
import RightSideLayoutIcon from '@/components/Icons/RightSideLayoutIcon.vue'
|
||||||
import ProfileSettings from '@/components/Settings/ProfileSettings.vue'
|
import ProfileSettings from '@/components/Settings/ProfileSettings.vue'
|
||||||
import WhatsAppSettings from '@/components/Settings/WhatsAppSettings.vue'
|
import WhatsAppSettings from '@/components/Settings/WhatsAppSettings.vue'
|
||||||
import TwilioSettings from '@/components/Settings/TwilioSettings.vue'
|
import TwilioSettings from '@/components/Settings/TwilioSettings.vue'
|
||||||
import FieldsLayout from '@/components/Settings/FieldsLayout.vue'
|
import SidebarFieldsLayout from '@/components/Settings/SidebarFieldsLayout.vue'
|
||||||
|
import QuickEntryLayout from '@/components/Settings/QuickEntryLayout.vue'
|
||||||
import SidebarLink from '@/components/SidebarLink.vue'
|
import SidebarLink from '@/components/SidebarLink.vue'
|
||||||
import { isWhatsappInstalled } from '@/composables/settings'
|
import { isWhatsappInstalled } from '@/composables/settings'
|
||||||
import { Dialog, FeatherIcon } from 'frappe-ui'
|
import { Dialog, FeatherIcon } from 'frappe-ui'
|
||||||
@ -84,9 +86,14 @@ const tabs = computed(() => {
|
|||||||
label: 'Customizations',
|
label: 'Customizations',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
label: 'Fields Layout',
|
label: 'Sidebar Fields Layout',
|
||||||
|
icon: RightSideLayoutIcon,
|
||||||
|
component: markRaw(SidebarFieldsLayout),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Quick Entry Layout',
|
||||||
icon: h(FeatherIcon, { name: 'grid' }),
|
icon: h(FeatherIcon, { name: 'grid' }),
|
||||||
component: markRaw(FieldsLayout),
|
component: markRaw(QuickEntryLayout),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -27,6 +27,10 @@
|
|||||||
@click="saveChanges"
|
@click="saveChanges"
|
||||||
/>
|
/>
|
||||||
<Button :label="__('Reset')" @click="reload" />
|
<Button :label="__('Reset')" @click="reload" />
|
||||||
|
<Button
|
||||||
|
:label="preview ? __('Hide Preview') : __('Show Preview')"
|
||||||
|
@click="preview = !preview"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Resizer
|
<Resizer
|
||||||
@ -37,13 +41,30 @@
|
|||||||
>
|
>
|
||||||
<div class="flex flex-1 flex-col justify-between overflow-hidden">
|
<div class="flex flex-1 flex-col justify-between overflow-hidden">
|
||||||
<div class="flex flex-col overflow-y-auto">
|
<div class="flex flex-col overflow-y-auto">
|
||||||
<SidebarLayoutBuilder :sections="sections.data" :doctype="doctype" />
|
<SidebarLayoutBuilder
|
||||||
|
v-if="!preview"
|
||||||
|
:sections="sections.data"
|
||||||
|
:doctype="doctype"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
v-for="(section, i) in sections.data"
|
||||||
|
:key="section.label"
|
||||||
|
class="flex flex-col p-3"
|
||||||
|
:class="{ 'border-b': i !== sections.data.length - 1 }"
|
||||||
|
>
|
||||||
|
<Section :is-opened="section.opened" :label="section.label">
|
||||||
|
<SectionFields :fields="section.fields" v-model="data" />
|
||||||
|
</Section>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Resizer>
|
</Resizer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import Section from '@/components/Section.vue'
|
||||||
|
import SectionFields from '@/components/SectionFields.vue'
|
||||||
import Resizer from '@/components/Resizer.vue'
|
import Resizer from '@/components/Resizer.vue'
|
||||||
import SidebarLayoutBuilder from '@/components/Settings/SidebarLayoutBuilder.vue'
|
import SidebarLayoutBuilder from '@/components/Settings/SidebarLayoutBuilder.vue'
|
||||||
import { useDebounceFn } from '@vueuse/core'
|
import { useDebounceFn } from '@vueuse/core'
|
||||||
@ -54,6 +75,8 @@ const parentRef = ref(null)
|
|||||||
const doctype = ref('CRM Lead')
|
const doctype = ref('CRM Lead')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const dirty = ref(false)
|
const dirty = ref(false)
|
||||||
|
const preview = ref(false)
|
||||||
|
const data = ref({})
|
||||||
|
|
||||||
function getParams() {
|
function getParams() {
|
||||||
return { doctype: doctype.value, type: 'Side Panel' }
|
return { doctype: doctype.value, type: 'Side Panel' }
|
||||||
@ -16,20 +16,27 @@
|
|||||||
<div v-if="!section.editingLabel">
|
<div v-if="!section.editingLabel">
|
||||||
{{ __(section.label) || __('Untitled') }}
|
{{ __(section.label) || __('Untitled') }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else class="flex gap-2 items-center">
|
||||||
<Input
|
<Input
|
||||||
v-model="section.label"
|
v-model="section.label"
|
||||||
@keydown.enter="section.editingLabel = false"
|
@keydown.enter="section.editingLabel = false"
|
||||||
@blur="section.editingLabel = false"
|
@blur="section.editingLabel = false"
|
||||||
@click.stop
|
@click.stop
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
v-if="section.editingLabel"
|
||||||
|
icon="check"
|
||||||
|
variant="ghost"
|
||||||
|
@click.stop="section.editingLabel = false"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
:icon="section.editingLabel ? 'check' : 'edit'"
|
v-if="!section.editingLabel"
|
||||||
|
icon="edit"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@click="section.editingLabel = !section.editingLabel"
|
@click="section.editingLabel = true"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
v-if="section.editable !== false"
|
v-if="section.editable !== false"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user