Merge pull request #485 from shariquerik/child-table-support

This commit is contained in:
Shariq Ansari 2024-12-29 21:35:05 +05:30 committed by GitHub
commit 2be808b511
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 1129 additions and 243 deletions

View File

@ -1,10 +1,11 @@
import frappe
import json import json
import frappe
from frappe import _ from frappe import _
from frappe.model.document import get_controller
from frappe.model import no_value_fields from frappe.model import no_value_fields
from pypika import Criterion from frappe.model.document import get_controller
from frappe.utils import make_filter_tuple from frappe.utils import make_filter_tuple
from pypika import Criterion
from crm.api.views import get_views from crm.api.views import get_views
from crm.fcrm.doctype.crm_form_script.crm_form_script import get_form_script from crm.fcrm.doctype.crm_form_script.crm_form_script import get_form_script
@ -557,6 +558,9 @@ def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False):
fields_meta = {} fields_meta = {}
for field in fields: for field in fields:
fields_meta[field.get("fieldname")] = field fields_meta[field.get("fieldname")] = field
if field.get("fieldtype") == "Table":
_fields = frappe.get_meta(field.get("options")).fields
fields_meta[field.get("fieldname")] = {"df": field, "fields": _fields}
return fields_meta return fields_meta
@ -672,7 +676,7 @@ def get_assigned_users(doctype, name, default_assigned_to=None):
@frappe.whitelist() @frappe.whitelist()
def get_fields(doctype: str, allow_all_fieldtypes: bool = False): def get_fields(doctype: str, allow_all_fieldtypes: bool = False):
not_allowed_fieldtypes = list(frappe.model.no_value_fields) + ["Read Only"] not_allowed_fieldtypes = [*list(frappe.model.no_value_fields), "Read Only"]
if allow_all_fieldtypes: if allow_all_fieldtypes:
not_allowed_fieldtypes = [] not_allowed_fieldtypes = []
fields = frappe.get_meta(doctype).fields fields = frappe.get_meta(doctype).fields

View File

@ -1,6 +1,5 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
import json
import frappe import frappe
from frappe import _ from frappe import _
@ -8,7 +7,9 @@ from frappe.desk.form.assign_to import add as assign
from frappe.model.document import Document from frappe.model.document import Document
from crm.fcrm.doctype.crm_service_level_agreement.utils import get_sla from crm.fcrm.doctype.crm_service_level_agreement.utils import get_sla
from crm.fcrm.doctype.crm_status_change_log.crm_status_change_log import add_status_change_log from crm.fcrm.doctype.crm_status_change_log.crm_status_change_log import (
add_status_change_log,
)
class CRMDeal(Document): class CRMDeal(Document):
@ -94,19 +95,26 @@ class CRMDeal(Document):
shared_with = [d.user for d in docshares] + [agent] shared_with = [d.user for d in docshares] + [agent]
for user in shared_with: for user in shared_with:
if user == agent and not frappe.db.exists("DocShare", {"user": agent, "share_name": self.name, "share_doctype": self.doctype}): if user == agent and not frappe.db.exists(
"DocShare",
{"user": agent, "share_name": self.name, "share_doctype": self.doctype},
):
frappe.share.add_docshare( frappe.share.add_docshare(
self.doctype, self.name, agent, write=1, flags={"ignore_share_permission": True} self.doctype,
self.name,
agent,
write=1,
flags={"ignore_share_permission": True},
) )
elif user != agent: elif user != agent:
frappe.share.remove(self.doctype, self.name, user) frappe.share.remove(self.doctype, self.name, user)
def set_sla(self): def set_sla(self):
""" """
Find an SLA to apply to the deal. Find an SLA to apply to the deal.
""" """
if self.sla: return if self.sla:
return
sla = get_sla(self) sla = get_sla(self)
if not sla: if not sla:
@ -129,48 +137,48 @@ class CRMDeal(Document):
def default_list_data(): def default_list_data():
columns = [ columns = [
{ {
'label': 'Organization', "label": "Organization",
'type': 'Link', "type": "Link",
'key': 'organization', "key": "organization",
'options': 'CRM Organization', "options": "CRM Organization",
'width': '11rem', "width": "11rem",
}, },
{ {
'label': 'Amount', "label": "Annual Revenue",
'type': 'Currency', "type": "Currency",
'key': 'annual_revenue', "key": "annual_revenue",
'align': 'right', "align": "right",
'width': '9rem', "width": "9rem",
}, },
{ {
'label': 'Status', "label": "Status",
'type': 'Select', "type": "Select",
'key': 'status', "key": "status",
'width': '10rem', "width": "10rem",
}, },
{ {
'label': 'Email', "label": "Email",
'type': 'Data', "type": "Data",
'key': 'email', "key": "email",
'width': '12rem', "width": "12rem",
}, },
{ {
'label': 'Mobile No', "label": "Mobile No",
'type': 'Data', "type": "Data",
'key': 'mobile_no', "key": "mobile_no",
'width': '11rem', "width": "11rem",
}, },
{ {
'label': 'Assigned To', "label": "Assigned To",
'type': 'Text', "type": "Text",
'key': '_assign', "key": "_assign",
'width': '10rem', "width": "10rem",
}, },
{ {
'label': 'Last Modified', "label": "Last Modified",
'type': 'Datetime', "type": "Datetime",
'key': 'modified', "key": "modified",
'width': '8rem', "width": "8rem",
}, },
] ]
rows = [ rows = [
@ -189,16 +197,17 @@ class CRMDeal(Document):
"modified", "modified",
"_assign", "_assign",
] ]
return {'columns': columns, 'rows': rows} return {"columns": columns, "rows": rows}
@staticmethod @staticmethod
def default_kanban_settings(): def default_kanban_settings():
return { return {
"column_field": "status", "column_field": "status",
"title_field": "organization", "title_field": "organization",
"kanban_fields": '["annual_revenue", "email", "mobile_no", "_assign", "modified"]' "kanban_fields": '["annual_revenue", "email", "mobile_no", "_assign", "modified"]',
} }
@frappe.whitelist() @frappe.whitelist()
def add_contact(deal, contact): def add_contact(deal, contact):
if not frappe.has_permission("CRM Deal", "write", deal): if not frappe.has_permission("CRM Deal", "write", deal):
@ -209,6 +218,7 @@ def add_contact(deal, contact):
deal.save() deal.save()
return True return True
@frappe.whitelist() @frappe.whitelist()
def remove_contact(deal, contact): def remove_contact(deal, contact):
if not frappe.has_permission("CRM Deal", "write", deal): if not frappe.has_permission("CRM Deal", "write", deal):
@ -219,6 +229,7 @@ def remove_contact(deal, contact):
deal.save() deal.save()
return True return True
@frappe.whitelist() @frappe.whitelist()
def set_primary_contact(deal, contact): def set_primary_contact(deal, contact):
if not frappe.has_permission("CRM Deal", "write", deal): if not frappe.has_permission("CRM Deal", "write", deal):
@ -229,11 +240,14 @@ def set_primary_contact(deal, contact):
deal.save() deal.save()
return True return True
def create_organization(doc): def create_organization(doc):
if not doc.get("organization_name"): if not doc.get("organization_name"):
return return
existing_organization = frappe.db.exists("CRM Organization", {"organization_name": doc.get("organization_name")}) existing_organization = frappe.db.exists(
"CRM Organization", {"organization_name": doc.get("organization_name")}
)
if existing_organization: if existing_organization:
return existing_organization return existing_organization
@ -250,6 +264,7 @@ def create_organization(doc):
organization.insert(ignore_permissions=True) organization.insert(ignore_permissions=True)
return organization.name return organization.name
def contact_exists(doc): def contact_exists(doc):
email_exist = frappe.db.exists("Contact Email", {"email_id": doc.get("email")}) email_exist = frappe.db.exists("Contact Email", {"email_id": doc.get("email")})
mobile_exist = frappe.db.exists("Contact Phone", {"phone": doc.get("mobile_no")}) mobile_exist = frappe.db.exists("Contact Phone", {"phone": doc.get("mobile_no")})
@ -262,6 +277,7 @@ def contact_exists(doc):
return False return False
def create_contact(doc): def create_contact(doc):
existing_contact = contact_exists(doc) existing_contact = contact_exists(doc)
if existing_contact: if existing_contact:
@ -288,18 +304,23 @@ def create_contact(doc):
return contact.name return contact.name
@frappe.whitelist() @frappe.whitelist()
def create_deal(args): def create_deal(args):
deal = frappe.new_doc("CRM Deal") deal = frappe.new_doc("CRM Deal")
contact = args.get("contact") contact = args.get("contact")
if not contact and (args.get("first_name") or args.get("last_name") or args.get("email") or args.get("mobile_no")): if not contact and (
args.get("first_name") or args.get("last_name") or args.get("email") or args.get("mobile_no")
):
contact = create_contact(args) contact = create_contact(args)
deal.update({ deal.update(
"organization": args.get("organization") or create_organization(args), {
"contacts": [{"contact": contact, "is_primary": 1}] if contact else [], "organization": args.get("organization") or create_organization(args),
}) "contacts": [{"contact": contact, "is_primary": 1}] if contact else [],
}
)
args.pop("organization", None) args.pop("organization", None)

View File

@ -27,7 +27,7 @@
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Type", "label": "Type",
"options": "Quick Entry\nSide Panel\nData Fields" "options": "Quick Entry\nSide Panel\nData Fields\nGrid Row"
}, },
{ {
"fieldname": "section_break_ttpm", "fieldname": "section_break_ttpm",
@ -46,7 +46,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2024-12-05 13:29:37.021412", "modified": "2024-12-29 12:58:54.280569",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "FCRM", "module": "FCRM",
"name": "CRM Fields Layout", "name": "CRM Fields Layout",

View File

@ -1,15 +1,16 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
import json
import frappe import frappe
from frappe import _ from frappe import _
from frappe.desk.form.assign_to import add as assign from frappe.desk.form.assign_to import add as assign
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import has_gravatar, validate_email_address from frappe.utils import has_gravatar, validate_email_address
from crm.fcrm.doctype.crm_service_level_agreement.utils import get_sla from crm.fcrm.doctype.crm_service_level_agreement.utils import get_sla
from crm.fcrm.doctype.crm_status_change_log.crm_status_change_log import add_status_change_log from crm.fcrm.doctype.crm_status_change_log.crm_status_change_log import (
add_status_change_log,
)
class CRMLead(Document): class CRMLead(Document):
@ -37,7 +38,15 @@ class CRMLead(Document):
def set_full_name(self): def set_full_name(self):
if self.first_name: if self.first_name:
self.lead_name = " ".join( self.lead_name = " ".join(
filter(None, [self.salutation, self.first_name, self.middle_name, self.last_name]) filter(
None,
[
self.salutation,
self.first_name,
self.middle_name,
self.last_name,
],
)
) )
def set_lead_name(self): def set_lead_name(self):
@ -92,9 +101,16 @@ class CRMLead(Document):
shared_with = [d.user for d in docshares] + [agent] shared_with = [d.user for d in docshares] + [agent]
for user in shared_with: for user in shared_with:
if user == agent and not frappe.db.exists("DocShare", {"user": agent, "share_name": self.name, "share_doctype": self.doctype}): if user == agent and not frappe.db.exists(
"DocShare",
{"user": agent, "share_name": self.name, "share_doctype": self.doctype},
):
frappe.share.add_docshare( frappe.share.add_docshare(
self.doctype, self.name, agent, write=1, flags={"ignore_share_permission": True} self.doctype,
self.name,
agent,
write=1,
flags={"ignore_share_permission": True},
) )
elif user != agent: elif user != agent:
frappe.share.remove(self.doctype, self.name, user) frappe.share.remove(self.doctype, self.name, user)
@ -188,8 +204,36 @@ class CRMLead(Document):
"lead_owner": "deal_owner", "lead_owner": "deal_owner",
} }
restricted_fieldtypes = ["Tab Break", "Section Break", "Column Break", "HTML", "Button", "Attach", "Table"] restricted_fieldtypes = [
restricted_map_fields = ["name", "naming_series", "creation", "owner", "modified", "modified_by", "idx", "docstatus", "status", "email", "mobile_no", "phone", "sla", "sla_status", "response_by", "first_response_time", "first_responded_on", "communication_status", "sla_creation"] "Tab Break",
"Section Break",
"Column Break",
"HTML",
"Button",
"Attach",
"Table",
]
restricted_map_fields = [
"name",
"naming_series",
"creation",
"owner",
"modified",
"modified_by",
"idx",
"docstatus",
"status",
"email",
"mobile_no",
"phone",
"sla",
"sla_status",
"response_by",
"first_response_time",
"first_responded_on",
"communication_status",
"sla_creation",
]
for field in self.meta.fields: for field in self.meta.fields:
if field.fieldtype in restricted_fieldtypes: if field.fieldtype in restricted_fieldtypes:
@ -222,7 +266,7 @@ class CRMLead(Document):
"sla_status": self.sla_status, "sla_status": self.sla_status,
"communication_status": self.communication_status, "communication_status": self.communication_status,
"first_response_time": self.first_response_time, "first_response_time": self.first_response_time,
"first_responded_on": self.first_responded_on "first_responded_on": self.first_responded_on,
} }
) )
@ -233,7 +277,8 @@ class CRMLead(Document):
""" """
Find an SLA to apply to the lead. Find an SLA to apply to the lead.
""" """
if self.sla: return if self.sla:
return
sla = get_sla(self) sla = get_sla(self)
if not sla: if not sla:
@ -263,47 +308,47 @@ class CRMLead(Document):
def default_list_data(): def default_list_data():
columns = [ columns = [
{ {
'label': 'Name', "label": "Name",
'type': 'Data', "type": "Data",
'key': 'lead_name', "key": "lead_name",
'width': '12rem', "width": "12rem",
}, },
{ {
'label': 'Organization', "label": "Organization",
'type': 'Link', "type": "Link",
'key': 'organization', "key": "organization",
'options': 'CRM Organization', "options": "CRM Organization",
'width': '10rem', "width": "10rem",
}, },
{ {
'label': 'Status', "label": "Status",
'type': 'Select', "type": "Select",
'key': 'status', "key": "status",
'width': '8rem', "width": "8rem",
}, },
{ {
'label': 'Email', "label": "Email",
'type': 'Data', "type": "Data",
'key': 'email', "key": "email",
'width': '12rem', "width": "12rem",
}, },
{ {
'label': 'Mobile No', "label": "Mobile No",
'type': 'Data', "type": "Data",
'key': 'mobile_no', "key": "mobile_no",
'width': '11rem', "width": "11rem",
}, },
{ {
'label': 'Assigned To', "label": "Assigned To",
'type': 'Text', "type": "Text",
'key': '_assign', "key": "_assign",
'width': '10rem', "width": "10rem",
}, },
{ {
'label': 'Last Modified', "label": "Last Modified",
'type': 'Datetime', "type": "Datetime",
'key': 'modified', "key": "modified",
'width': '8rem', "width": "8rem",
}, },
] ]
rows = [ rows = [
@ -323,20 +368,22 @@ class CRMLead(Document):
"_assign", "_assign",
"image", "image",
] ]
return {'columns': columns, 'rows': rows} return {"columns": columns, "rows": rows}
@staticmethod @staticmethod
def default_kanban_settings(): def default_kanban_settings():
return { return {
"column_field": "status", "column_field": "status",
"title_field": "lead_name", "title_field": "lead_name",
"kanban_fields": '["organization", "email", "mobile_no", "_assign", "modified"]' "kanban_fields": '["organization", "email", "mobile_no", "_assign", "modified"]',
} }
@frappe.whitelist() @frappe.whitelist()
def convert_to_deal(lead, doc=None): def convert_to_deal(lead, doc=None):
if not (doc and doc.flags.get("ignore_permissions")) and not frappe.has_permission("CRM Lead", "write", lead): if not (doc and doc.flags.get("ignore_permissions")) and not frappe.has_permission(
"CRM Lead", "write", lead
):
frappe.throw(_("Not allowed to convert Lead to Deal"), frappe.PermissionError) frappe.throw(_("Not allowed to convert Lead to Deal"), frappe.PermissionError)
lead = frappe.get_cached_doc("CRM Lead", lead) lead = frappe.get_cached_doc("CRM Lead", lead)

@ -1 +1 @@
Subproject commit 5a4f3c8d4f12efba37b9a83a51a59b53fa758be0 Subproject commit 46086c524bc218d989c68ca54cd13a37e693fab9

View File

@ -14,7 +14,7 @@
"@vueuse/core": "^10.3.0", "@vueuse/core": "^10.3.0",
"@vueuse/integrations": "^10.3.0", "@vueuse/integrations": "^10.3.0",
"feather-icons": "^4.28.0", "feather-icons": "^4.28.0",
"frappe-ui": "^0.1.91", "frappe-ui": "^0.1.94",
"gemoji": "^8.1.0", "gemoji": "^8.1.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mime": "^4.0.1", "mime": "^4.0.1",

View File

@ -31,7 +31,7 @@
<LoadingIndicator class="h-6 w-6" /> <LoadingIndicator class="h-6 w-6" />
<span>{{ __('Loading...') }}</span> <span>{{ __('Loading...') }}</span>
</div> </div>
<div v-else> <div v-else class="pb-8">
<FieldLayout <FieldLayout
v-if="tabs.data" v-if="tabs.data"
:tabs="tabs.data" :tabs="tabs.data"
@ -86,7 +86,7 @@ const data = createDocumentResource({
createToast({ createToast({
title: 'Data Updated', title: 'Data Updated',
icon: 'check', icon: 'check',
iconClasses: 'text-green-600', iconClasses: 'text-ink-green-3',
}) })
}, },
onError: (err) => { onError: (err) => {

View File

@ -0,0 +1,366 @@
<template>
<div class="flex flex-col text-base">
<div v-if="label" class="mb-1.5 text-sm text-ink-gray-5">
{{ __(label) }}
</div>
<div
v-if="fields?.length"
class="rounded border border-outline-gray-modals"
>
<!-- Header -->
<div
class="grid-header flex items-center rounded-t-[7px] bg-surface-gray-2 text-ink-gray-5 truncate"
>
<div
class="inline-flex items-center justify-center border-r border-outline-gray-2 h-8 p-2 w-12"
>
<Checkbox
class="cursor-pointer duration-300"
:modelValue="allRowsSelected"
@click.stop="toggleSelectAllRows($event.target.checked)"
/>
</div>
<div
class="inline-flex items-center justify-center border-r border-outline-gray-2 py-2 px-1 w-12"
>
{{ __('No') }}
</div>
<div
class="grid w-full truncate"
:style="{ gridTemplateColumns: gridTemplateColumns }"
>
<div
v-for="field in fields"
class="border-r border-outline-gray-2 p-2 truncate"
:key="field.name"
:title="field.label"
>
{{ __(field.label) }}
</div>
</div>
<div class="w-12">
<Button
class="flex w-full items-center justify-center rounded !bg-surface-gray-2 border-0"
variant="outline"
@click="showGridFieldsEditorModal = true"
>
<FeatherIcon name="settings" class="h-4 w-4 text-ink-gray-7" />
</Button>
</div>
</div>
<!-- Rows -->
<template v-if="rows.length">
<Draggable class="w-full" v-model="rows" group="rows" item-key="name">
<template #item="{ element: row, index }">
<div
class="grid-row flex cursor-pointer items-center border-b border-outline-gray-modals bg-surface-modals last:rounded-b last:border-b-0"
>
<div
class="inline-flex h-9.5 items-center justify-center border-r border-outline-gray-modals p-2 w-12"
>
<Checkbox
class="cursor-pointer duration-300"
:modelValue="selectedRows.has(row.name)"
@click.stop="toggleSelectRow(row)"
/>
</div>
<div
class="flex h-9.5 items-center justify-center border-r border-outline-gray-modals py-2 px-1 text-sm text-ink-gray-8 w-12"
>
{{ index + 1 }}
</div>
<div
class="grid w-full h-9.5"
:style="{ gridTemplateColumns: gridTemplateColumns }"
>
<div
class="border-r border-outline-gray-modals h-full"
v-for="field in fields"
:key="field.name"
>
<Link
v-if="field.type === 'Link'"
class="text-sm text-ink-gray-8"
v-model="row[field.name]"
:doctype="field.options"
:filters="field.filters"
/>
<div
v-else-if="field.type === 'Check'"
class="flex h-full justify-center items-center"
>
<Checkbox
class="cursor-pointer duration-300"
v-model="row[field.name]"
/>
</div>
<DatePicker
v-else-if="field.type === 'Date'"
v-model="row[field.name]"
icon-left=""
variant="outline"
:formatter="(date) => getFormat(date, '', true)"
input-class="border-none text-sm text-ink-gray-8"
/>
<DateTimePicker
v-else-if="field.type === 'Datetime'"
v-model="row[field.name]"
icon-left=""
variant="outline"
:formatter="(date) => getFormat(date, '', true, true)"
input-class="border-none text-sm text-ink-gray-8"
/>
<FormControl
v-else-if="
['Small Text', 'Text', 'Long Text', 'Code'].includes(
field.type,
)
"
rows="1"
type="textarea"
variant="outline"
v-model="row[field.name]"
/>
<FormControl
v-else-if="['Int'].includes(field.type)"
type="number"
variant="outline"
v-model="row[field.name]"
/>
<FormControl
v-else-if="field.type === 'Select'"
class="text-sm text-ink-gray-8"
type="select"
variant="outline"
v-model="row[field.name]"
:options="field.options"
/>
<FormControl
v-else
class="text-sm text-ink-gray-8"
type="text"
variant="outline"
v-model="row[field.name]"
:options="field.options"
/>
</div>
</div>
<div class="edit-row w-12">
<Button
class="flex w-full items-center justify-center rounded border-0"
variant="outline"
@click="showRowList[index] = true"
>
<EditIcon class="h-4 w-4 text-ink-gray-7" />
</Button>
</div>
<GridRowModal
v-if="showRowList[index]"
v-model="showRowList[index]"
v-model:showGridRowFieldsModal="showGridRowFieldsModal"
:index="index"
:data="row"
:doctype="doctype"
/>
</div>
</template>
</Draggable>
</template>
<div
v-else
class="flex flex-col items-center rounded p-5 text-sm text-ink-gray-5"
>
{{ __('No Data') }}
</div>
</div>
<div v-if="fields?.length" class="mt-2 flex flex-row gap-2">
<Button
v-if="showDeleteBtn"
:label="__('Delete')"
variant="solid"
theme="red"
@click="deleteRows"
/>
<Button :label="__('Add Row')" @click="addRow" />
</div>
</div>
<GridRowFieldsModal
v-if="showGridRowFieldsModal"
v-model="showGridRowFieldsModal"
:doctype="doctype"
/>
<GridFieldsEditorModal
v-if="showGridFieldsEditorModal"
v-model="showGridFieldsEditorModal"
:doctype="doctype"
:parentDoctype="parentDoctype"
/>
</template>
<script setup>
import GridFieldsEditorModal from '@/components/Controls/GridFieldsEditorModal.vue'
import GridRowFieldsModal from '@/components/Controls/GridRowFieldsModal.vue'
import GridRowModal from '@/components/Controls/GridRowModal.vue'
import EditIcon from '@/components/Icons/EditIcon.vue'
import Link from '@/components/Controls/Link.vue'
import { getRandom, getFormat } from '@/utils'
import { getMeta } from '@/stores/meta'
import {
FeatherIcon,
FormControl,
Checkbox,
DateTimePicker,
DatePicker,
} from 'frappe-ui'
import Draggable from 'vuedraggable'
import { ref, reactive, computed } from 'vue'
const props = defineProps({
label: {
type: String,
default: '',
},
doctype: {
type: String,
required: true,
},
parentDoctype: {
type: String,
required: true,
},
})
const { getGridSettings, getFields } = getMeta(props.doctype)
const rows = defineModel()
const showRowList = ref(new Array(rows.value.length).fill(false))
const selectedRows = reactive(new Set())
const showGridFieldsEditorModal = ref(false)
const showGridRowFieldsModal = ref(false)
const fields = computed(() => {
let gridSettings = getGridSettings(props.parentDoctype)
let gridFields = getFields()
if (gridSettings.length) {
let d = gridSettings.map((gs) =>
getFieldObj(gridFields.find((f) => f.fieldname === gs.fieldname)),
)
return d
}
return gridFields?.map((f) => getFieldObj(f)) || []
})
function getFieldObj(field) {
return {
label: field.label,
name: field.fieldname,
type: field.fieldtype,
options: field.options,
in_list_view: field.in_list_view,
}
}
const gridTemplateColumns = computed(() => {
if (!fields.value?.length) return '1fr'
// for the checkbox & sr no. columns
let gridSettings = getGridSettings(props.parentDoctype)
if (gridSettings.length) {
return gridSettings.map((gs) => `minmax(0, ${gs.columns || 2}fr)`).join(' ')
}
return fields.value.map(() => `minmax(0, 2fr)`).join(' ')
})
const allRowsSelected = computed(() => {
if (!rows.value.length) return false
return rows.value.length === selectedRows.size
})
const showDeleteBtn = computed(() => selectedRows.size > 0)
const toggleSelectAllRows = (iSelected) => {
if (iSelected) {
rows.value.forEach((row) => selectedRows.add(row.name))
} else {
selectedRows.clear()
}
}
const toggleSelectRow = (row) => {
if (selectedRows.has(row.name)) {
selectedRows.delete(row.name)
} else {
selectedRows.add(row.name)
}
}
const addRow = () => {
const newRow = {}
fields.value?.forEach((field) => {
if (field.type === 'Check') newRow[field.name] = false
else newRow[field.name] = ''
})
newRow.name = getRandom(10)
showRowList.value.push(false)
newRow['__islocal'] = true
rows.value.push(newRow)
}
const deleteRows = () => {
rows.value = rows.value.filter((row) => !selectedRows.has(row.name))
showRowList.value.pop()
selectedRows.clear()
}
</script>
<style scoped>
/* For Input fields */
:deep(.grid-row input:not([type='checkbox'])),
:deep(.grid-row textarea) {
border: none;
border-radius: 0;
height: 38px;
}
:deep(.grid-row input:focus),
:deep(.grid-row input:hover),
:deep(.grid-row textarea:focus),
:deep(.grid-row textarea:hover) {
box-shadow: none;
}
:deep(.grid-row input:focus-within) :deep(.grid-row textarea:focus-within) {
border: 1px solid var(--outline-gray-2);
}
/* For select field */
:deep(.grid-row select) {
border: none;
border-radius: 0;
height: 38px;
}
/* For Autocomplete */
:deep(.grid-row button) {
border: none;
border-radius: 0;
background-color: var(--surface-white);
height: 38px;
}
:deep(.grid-row .edit-row button) {
border-bottom-right-radius: 7px;
}
:deep(.grid-row button:focus) :deep(.grid-row button:hover) {
box-shadow: none;
background-color: var(--surface-white);
}
:deep(.grid-row button:focus-within) {
border: 1px solid var(--outline-gray-2);
}
</style>

View File

@ -0,0 +1,193 @@
<template>
<Dialog v-model="show">
<template #body-title>
<h3
class="flex items-center gap-2 text-2xl font-semibold leading-6 text-ink-gray-9"
>
<div>{{ __('Edit Grid Fields Layout') }}</div>
<Badge
v-if="dirty"
:label="__('Not Saved')"
variant="subtle"
theme="orange"
/>
</h3>
</template>
<template #body-content>
<div class="mt-4">
<div class="text-base text-ink-gray-8 mb-2">
{{ __('Fields Order') }}
</div>
<Draggable
v-if="oldFields?.length"
:list="fields"
@end="reorder"
group="fields"
item-key="fieldname"
class="flex flex-col gap-1"
>
<template #item="{ element: field }">
<div
class="px-1 py-0.5 bg-surface-gray-2 border border-outline-gray-modals rounded text-base text-ink-gray-8 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 class="flex items-center gap-2">
<TextInput
variant="outline"
type="number"
v-model="field.columns"
class="w-20"
/>
<Button variant="ghost" icon="x" @click="removeField(field)" />
</div>
</div>
</template>
</Draggable>
<Autocomplete
v-if="dropdownFields?.length"
value=""
:options="dropdownFields"
@change="(e) => addField(e)"
>
<template #target="{ togglePopover }">
<Button
class="w-full mt-2"
@click="togglePopover()"
:label="__('Add Field')"
>
<template #prefix>
<FeatherIcon name="plus" class="h-4" />
</template>
</Button>
</template>
<template #item-label="{ option }">
<div class="flex flex-col gap-1 text-ink-gray-9">
<div>{{ option.label }}</div>
<div class="text-ink-gray-4 text-sm">
{{ `${option.fieldname} - ${option.fieldtype}` }}
</div>
</div>
</template>
</Autocomplete>
<ErrorMessage class="mt-3" v-if="error" :message="error" />
</div>
</template>
<template #actions>
<div class="flex flex-col gap-2">
<Button
v-if="dirty"
class="w-full"
:label="__('Reset')"
@click="reset"
/>
<Button
class="w-full"
:label="__('Save')"
variant="solid"
@click="update"
:loading="loading"
:disabled="!dirty"
/>
</div>
</template>
</Dialog>
</template>
<script setup>
import DragVerticalIcon from '@/components/Icons/DragVerticalIcon.vue'
import Autocomplete from '@/components/frappe-ui/Autocomplete.vue'
import { getMeta } from '@/stores/meta'
import Draggable from 'vuedraggable'
import { Dialog, ErrorMessage } from 'frappe-ui'
import { ref, computed } from 'vue'
const props = defineProps({
doctype: String,
parentDoctype: String,
})
const { userSettings, getFields, getGridSettings, saveUserSettings } = getMeta(
props.doctype,
)
const show = defineModel()
const loading = ref(false)
const error = ref(null)
const dirty = computed(() => {
return JSON.stringify(fields.value) !== JSON.stringify(oldFields.value)
})
const oldFields = computed(() => {
let _fields = getFields()
let gridSettings = getGridSettings(props.parentDoctype)
if (gridSettings.length) {
return gridSettings.map((field) => {
let f = _fields.find((f) => f.fieldname === field.fieldname)
if (f) {
f.columns = field.columns
return fieldObj(f)
}
})
}
return _fields?.filter((field) => field.in_list_view).map((f) => fieldObj(f))
})
const fields = ref(JSON.parse(JSON.stringify(oldFields.value || [])))
const dropdownFields = computed(() => {
return getFields()?.filter(
(field) => !fields.value.find((f) => f.fieldname === field.fieldname),
)
})
function reset() {
fields.value = JSON.parse(JSON.stringify(oldFields.value || []))
}
function addField(field) {
fields.value.push(fieldObj(field))
}
function removeField(field) {
const index = fields.value.findIndex((f) => f.fieldname === field.fieldname)
fields.value.splice(index, 1)
}
function update() {
loading.value = true
let updateFields = fields.value.map((field) => {
return {
fieldname: field.fieldname,
columns: field.columns,
}
})
if (updateFields.length === 0) {
error.value = __('At least one field is required')
return
}
saveUserSettings(props.parentDoctype, 'GridView', updateFields, () => {
loading.value = false
show.value = false
userSettings[props.parentDoctype]['GridView'][props.doctype] = updateFields
})
}
function fieldObj(field) {
return {
label: field.label,
fieldname: field.fieldname,
fieldtype: field.fieldtype,
options: field.options,
in_list_view: field.in_list_view,
columns: field.columns || 2,
}
}
</script>

View File

@ -0,0 +1,125 @@
<template>
<Dialog v-model="show" :options="{ size: '3xl' }">
<template #body-title>
<h3
class="flex items-center gap-2 text-2xl font-semibold leading-6 text-ink-gray-9"
>
<div>{{ __('Edit Grid Row Fields Layout') }}</div>
<Badge
v-if="dirty"
:label="__('Not Saved')"
variant="subtle"
theme="orange"
/>
</h3>
</template>
<template #body-content>
<div class="flex flex-col gap-3">
<div class="flex justify-between gap-2">
<Button
:label="preview ? __('Hide preview') : __('Show preview')"
@click="preview = !preview"
/>
<div class="flex flex-row-reverse gap-2">
<Button
:loading="loading"
:label="__('Save')"
variant="solid"
@click="saveChanges"
/>
<Button :label="__('Reset')" @click="reload" />
</div>
</div>
<div v-if="tabs?.data">
<FieldLayoutEditor
v-if="!preview"
:tabs="tabs.data"
:doctype="_doctype"
/>
<FieldLayout v-else :tabs="tabs.data" :data="{}" :modal="true" />
</div>
</div>
</template>
</Dialog>
</template>
<script setup>
import FieldLayout from '@/components/FieldLayout.vue'
import FieldLayoutEditor from '@/components/FieldLayoutEditor.vue'
import { useDebounceFn } from '@vueuse/core'
import { capture } from '@/telemetry'
import { Dialog, Badge, call, createResource } from 'frappe-ui'
import { ref, watch, onMounted, nextTick } from 'vue'
const props = defineProps({
doctype: {
type: String,
default: 'CRM Lead',
},
})
const emit = defineEmits(['reload'])
const show = defineModel()
const _doctype = ref(props.doctype)
const loading = ref(false)
const dirty = ref(false)
const preview = ref(false)
function getParams() {
return { doctype: _doctype.value, type: 'Grid Row' }
}
const tabs = createResource({
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
cache: ['GridRowFieldsModal', _doctype.value],
params: getParams(),
onSuccess(data) {
tabs.originalData = JSON.parse(JSON.stringify(data))
},
})
watch(
() => tabs?.data,
() => {
dirty.value =
JSON.stringify(tabs?.data) !== JSON.stringify(tabs?.originalData)
},
{ deep: true },
)
onMounted(() => useDebounceFn(reload, 100)())
function reload() {
nextTick(() => {
tabs.params = getParams()
tabs.reload()
})
}
function saveChanges() {
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(
'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.save_fields_layout',
{
doctype: _doctype.value,
type: 'Grid Row',
layout: JSON.stringify(_tabs),
},
).then(() => {
loading.value = false
show.value = false
capture('data_fields_layout_builder', { doctype: _doctype.value })
emit('reload')
})
}
</script>

View File

@ -0,0 +1,62 @@
<template>
<Dialog v-model="show" :options="{ size: '4xl' }">
<template #body>
<div class="bg-surface-modal px-4 pb-6 pt-5 sm:px-6">
<div class="mb-5 flex items-center justify-between">
<div>
<h3 class="text-2xl font-semibold leading-6 text-ink-gray-9">
{{ __('Editing Row {0}', [index + 1]) }}
</h3>
</div>
<div class="flex items-center gap-1">
<Button
v-if="isManager()"
variant="ghost"
class="w-7"
@click="openGridRowFieldsModal"
>
<EditIcon class="h-4 w-4" />
</Button>
<Button variant="ghost" class="w-7" @click="show = false">
<FeatherIcon name="x" class="h-4 w-4" />
</Button>
</div>
</div>
<div>
<FieldLayout v-if="tabs.data" :tabs="tabs.data" :data="data" />
</div>
</div>
</template>
</Dialog>
</template>
<script setup>
import EditIcon from '@/components/Icons/EditIcon.vue'
import FieldLayout from '@/components/FieldLayout.vue'
import { usersStore } from '@/stores/users'
import { createResource } from 'frappe-ui'
import { nextTick } from 'vue'
const props = defineProps({
index: Number,
data: Object,
doctype: String,
})
const { isManager } = usersStore()
const show = defineModel()
const showGridRowFieldsModal = defineModel('showGridRowFieldsModal')
const tabs = createResource({
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
cache: ['GridRow', props.doctype],
params: { doctype: props.doctype, type: 'Grid Row' },
auto: true,
})
function openGridRowFieldsModal() {
showGridRowFieldsModal.value = true
nextTick(() => (show.value = false))
}
</script>

View File

@ -15,7 +15,7 @@
!hasTabs ? 'hidden' : modal ? 'border-outline-gray-modals' : '' !hasTabs ? 'hidden' : modal ? 'border-outline-gray-modals' : ''
" "
> >
<div :class="{ 'my-4 sm:my-6': hasTabs }"> <div class="overflow-hidden" :class="{ 'my-4 sm:my-6': hasTabs }">
<div <div
v-for="(section, i) in tab.sections" v-for="(section, i) in tab.sections"
:key="section.label" :key="section.label"
@ -76,6 +76,13 @@
v-model="data[field.name]" v-model="data[field.name]"
:disabled="true" :disabled="true"
/> />
<Grid
v-else-if="field.type === 'Table'"
v-model="data[field.name]"
:fields="field.fields"
:doctype="field.options"
:parentDoctype="doctype"
/>
<FormControl <FormControl
v-else-if="field.type === 'Select'" v-else-if="field.type === 'Select'"
type="select" type="select"
@ -178,7 +185,9 @@
/> />
<FormControl <FormControl
v-else-if=" v-else-if="
['Small Text', 'Text', 'Long Text'].includes(field.type) ['Small Text', 'Text', 'Long Text', 'Code'].includes(
field.type,
)
" "
type="textarea" type="textarea"
:placeholder="getPlaceholder(field)" :placeholder="getPlaceholder(field)"
@ -237,7 +246,8 @@ import EditIcon from '@/components/Icons/EditIcon.vue'
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue' import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
import UserAvatar from '@/components/UserAvatar.vue' import UserAvatar from '@/components/UserAvatar.vue'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import { getMeta } from '../stores/meta' import Grid from '@/components/Controls/Grid.vue'
import { getMeta } from '@/stores/meta'
import { usersStore } from '@/stores/users' import { usersStore } from '@/stores/users'
import { getFormat } from '@/utils' import { getFormat } from '@/utils'
import { flt } from '@/utils/numberFormat.js' import { flt } from '@/utils/numberFormat.js'

View File

@ -1,70 +1,70 @@
<template> <template>
<div class="flex flex-col gap-5.5"> <div class="flex flex-col gap-5.5">
<div <div
class="flex justify-between items-center gap-1 text-base bg-surface-gray-2 rounded py-2 px-2.5" class="flex items-center gap-2 text-base bg-surface-gray-2 rounded py-2 px-2.5"
> >
<div class="flex items-center gap-1"> <Draggable
<Draggable v-if="tabs.length && !tabs[tabIndex].no_tabs"
v-if="tabs.length && !tabs[tabIndex].no_tabs" :list="tabs"
:list="tabs" item-key="label"
item-key="label" class="flex items-center gap-2"
class="flex items-center gap-1" @end="(e) => (tabIndex = e.newIndex)"
@end="(e) => (tabIndex = e.newIndex)" >
> <template #item="{ element: tab, index: i }">
<template #item="{ element: tab, index: i }"> <div
<div class="flex items-center gap-2 cursor-pointer rounded"
class="cursor-pointer rounded" :class="[
:class="[ tabIndex == i
tabIndex == i ? 'text-ink-gray-9 bg-surface-white shadow-sm'
? 'text-ink-gray-9 bg-surface-white shadow-sm' : 'text-ink-gray-5 hover:text-ink-gray-9 hover:bg-surface-white hover:shadow-sm',
: 'text-ink-gray-5 hover:text-ink-gray-9 hover:bg-surface-white hover:shadow-sm', tab.editingLabel ? 'p-1' : 'px-2 py-1',
tab.editingLabel ? 'p-1' : 'px-2 py-1', ]"
]" @click="tabIndex = i"
@click="tabIndex = i" >
> <div @dblclick="() => (tab.editingLabel = true)">
<div @dblclick="() => (tab.editingLabel = true)"> <div v-if="!tab.editingLabel" class="flex items-center gap-2">
<div v-if="!tab.editingLabel" class="flex items-center gap-2"> {{ __(tab.label) || __('Untitled') }}
{{ __(tab.label) || __('Untitled') }} </div>
</div> <div v-else class="flex gap-1 items-center">
<div v-else class="flex gap-1 items-center"> <Input
<Input v-model="tab.label"
v-model="tab.label" @keydown.enter="tab.editingLabel = false"
@keydown.enter="tab.editingLabel = false" @blur="tab.editingLabel = false"
@blur="tab.editingLabel = false" @click.stop
@click.stop />
/> <Button
<Button v-if="tab.editingLabel"
v-if="tab.editingLabel" icon="check"
icon="check" variant="ghost"
variant="ghost" @click="tab.editingLabel = false"
@click="tab.editingLabel = false" />
/>
</div>
</div> </div>
</div> </div>
</template> <Dropdown
</Draggable> v-if="!tab.no_tabs && tabIndex == i"
<Button :options="getTabOptions(tab)"
variant="ghost" class="!h-4"
class="!h-6.5 !text-ink-gray-5 hover:!text-ink-gray-9" @click.stop
@click="addTab" >
:label="__('Add Tab')" <template #default>
> <Button variant="ghost" class="!p-1 !h-4">
<template #prefix> <FeatherIcon name="more-horizontal" class="h-4" />
<FeatherIcon name="plus" class="h-4" /> </Button>
</template> </template>
</Button> </Dropdown>
</div> </div>
<Dropdown
v-if="tabs.length && !tabs[tabIndex].no_tabs"
:options="getTabOptions(tabs[tabIndex])"
>
<template #default>
<Button variant="ghost">
<FeatherIcon name="more-horizontal" class="h-4" />
</Button>
</template> </template>
</Dropdown> </Draggable>
<Button
variant="ghost"
class="!h-6.5 !text-ink-gray-5 hover:!text-ink-gray-9"
@click="addTab"
:label="__('Add Tab')"
>
<template v-slot:[slotName]>
<FeatherIcon name="plus" class="h-4" />
</template>
</Button>
</div> </div>
<div v-show="tabIndex == i" v-for="(tab, i) in tabs" :key="tab.label"> <div v-show="tabIndex == i" v-for="(tab, i) in tabs" :key="tab.label">
<Draggable <Draggable
@ -126,9 +126,9 @@
<div <div
class="px-2.5 py-2 border border-outline-gray-2 rounded text-base bg-surface-modal text-ink-gray-8 flex items-center leading-4 justify-between gap-2" class="px-2.5 py-2 border border-outline-gray-2 rounded text-base bg-surface-modal text-ink-gray-8 flex items-center leading-4 justify-between gap-2"
> >
<div class="flex items-center gap-2"> <div class="flex items-center gap-2 truncate">
<DragVerticalIcon class="h-3.5 cursor-grab" /> <DragVerticalIcon class="h-3.5 cursor-grab" />
<div>{{ field.label }}</div> <div class="truncate">{{ field.label }}</div>
</div> </div>
<Button <Button
variant="ghost" variant="ghost"
@ -207,9 +207,14 @@ const props = defineProps({
}) })
const tabIndex = ref(0) const tabIndex = ref(0)
const slotName = computed(() => {
if (props.tabs.length == 1 && props.tabs[0].no_tabs) {
return 'prefix'
}
return 'default'
})
const restrictedFieldTypes = [ const restrictedFieldTypes = [
'Table',
'Geolocation', 'Geolocation',
'Attach', 'Attach',
'Attach Image', 'Attach Image',
@ -230,6 +235,34 @@ const fields = createResource({
params: params.value, params: params.value,
cache: ['fieldsMeta', props.doctype], cache: ['fieldsMeta', props.doctype],
auto: true, auto: true,
transform: (data) => {
let restrictedFields = [
'name',
'owner',
'creation',
'modified',
'modified_by',
'docstatus',
'_comments',
'_user_tags',
'_assign',
'_liked_by',
]
let existingFields = []
for (let tab of props.tabs) {
for (let section of tab.sections) {
existingFields = existingFields.concat(section.fields)
}
}
return data.filter((field) => {
return (
!existingFields.find((f) => f.name === field.fieldname) &&
!restrictedFields.includes(field.fieldname)
)
})
},
}) })
function addTab() { function addTab() {
@ -243,7 +276,12 @@ function addTab() {
function addField(section, field) { function addField(section, field) {
if (!field) return if (!field) return
section.fields.push(field) let newFieldObj = {
...field,
name: field.fieldname,
type: field.fieldtype,
}
section.fields.push(newFieldObj)
} }
function getTabOptions(tab) { function getTabOptions(tab) {

View File

@ -89,7 +89,7 @@
</Button> </Button>
</template> </template>
<template #item-label="{ option }"> <template #item-label="{ option }">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1 text-ink-gray-9">
<div>{{ option.label }}</div> <div>{{ option.label }}</div>
<div class="text-ink-gray-4 text-sm"> <div class="text-ink-gray-4 text-sm">
{{ `${option.fieldname} - ${option.fieldtype}` }} {{ `${option.fieldname} - ${option.fieldtype}` }}
@ -166,9 +166,6 @@ const fields = createResource({
params: { doctype: props.doctype, as_array: true }, params: { doctype: props.doctype, as_array: true },
cache: ['kanban_fields', props.doctype], cache: ['kanban_fields', props.doctype],
auto: true, auto: true,
onSuccess: (data) => {
data
},
}) })
const allFields = computed({ const allFields = computed({

View File

@ -16,18 +16,19 @@
<template #body-content> <template #body-content>
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<div class="flex justify-between gap-2"> <div class="flex justify-between gap-2">
<FormControl <Button
type="select"
class="w-1/4"
v-model="_doctype"
:options="['CRM Lead', 'CRM Deal']"
@change="reload"
/>
<Switch
v-model="preview"
:label="preview ? __('Hide preview') : __('Show preview')" :label="preview ? __('Hide preview') : __('Show preview')"
size="sm" @click="preview = !preview"
/> />
<div class="flex flex-row-reverse gap-2">
<Button
:loading="loading"
:label="__('Save')"
variant="solid"
@click="saveChanges"
/>
<Button :label="__('Reset')" @click="reload" />
</div>
</div> </div>
<div v-if="tabs?.data"> <div v-if="tabs?.data">
<FieldLayoutEditor <FieldLayoutEditor
@ -39,17 +40,6 @@
</div> </div>
</div> </div>
</template> </template>
<template #actions>
<div class="flex flex-row-reverse gap-2">
<Button
:loading="loading"
:label="__('Save')"
variant="solid"
@click="saveChanges"
/>
<Button :label="__('Reset')" @click="reload" />
</div>
</template>
</Dialog> </Dialog>
</template> </template>
<script setup> <script setup>
@ -57,7 +47,7 @@ import FieldLayout from '@/components/FieldLayout.vue'
import FieldLayoutEditor from '@/components/FieldLayoutEditor.vue' import FieldLayoutEditor from '@/components/FieldLayoutEditor.vue'
import { useDebounceFn } from '@vueuse/core' import { useDebounceFn } from '@vueuse/core'
import { capture } from '@/telemetry' import { capture } from '@/telemetry'
import { Dialog, Badge, Switch, call, createResource } from 'frappe-ui' import { Dialog, Badge, call, createResource } from 'frappe-ui'
import { ref, watch, onMounted, nextTick } from 'vue' import { ref, watch, onMounted, nextTick } from 'vue'
const props = defineProps({ const props = defineProps({

View File

@ -16,24 +16,19 @@
<template #body-content> <template #body-content>
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<div class="flex justify-between gap-2"> <div class="flex justify-between gap-2">
<FormControl <Button
type="select"
class="w-1/4"
v-model="_doctype"
:options="[
'CRM Lead',
'CRM Deal',
'Contact',
'CRM Organization',
'Address',
]"
@change="reload"
/>
<Switch
v-model="preview"
:label="preview ? __('Hide preview') : __('Show preview')" :label="preview ? __('Hide preview') : __('Show preview')"
size="sm" @click="preview = !preview"
/> />
<div class="flex flex-row-reverse gap-2">
<Button
:loading="loading"
:label="__('Save')"
variant="solid"
@click="saveChanges"
/>
<Button :label="__('Reset')" @click="reload" />
</div>
</div> </div>
<div v-if="tabs?.data"> <div v-if="tabs?.data">
<FieldLayoutEditor <FieldLayoutEditor
@ -45,17 +40,6 @@
</div> </div>
</div> </div>
</template> </template>
<template #actions>
<div class="flex flex-row-reverse gap-2">
<Button
:loading="loading"
:label="__('Save')"
variant="solid"
@click="saveChanges"
/>
<Button :label="__('Reset')" @click="reload" />
</div>
</template>
</Dialog> </Dialog>
</template> </template>
<script setup> <script setup>

View File

@ -16,18 +16,19 @@
<template #body-content> <template #body-content>
<div class="flex flex-col gap-5.5"> <div class="flex flex-col gap-5.5">
<div class="flex justify-between gap-2"> <div class="flex justify-between gap-2">
<FormControl <Button
type="select"
class="w-1/4"
v-model="_doctype"
:options="['CRM Lead', 'CRM Deal', 'Contact', 'CRM Organization']"
@change="reload"
/>
<Switch
v-model="preview"
:label="preview ? __('Hide preview') : __('Show preview')" :label="preview ? __('Hide preview') : __('Show preview')"
size="sm" @click="preview = !preview"
/> />
<div class="flex flex-row-reverse gap-2">
<Button
:loading="loading"
:label="__('Save')"
variant="solid"
@click="saveChanges"
/>
<Button :label="__('Reset')" @click="reload" />
</div>
</div> </div>
<div v-if="tabs.data?.[0]?.sections" class="flex gap-4"> <div v-if="tabs.data?.[0]?.sections" class="flex gap-4">
<SidePanelLayoutEditor <SidePanelLayoutEditor
@ -66,17 +67,6 @@
</div> </div>
</div> </div>
</template> </template>
<template #actions>
<div class="flex flex-row-reverse gap-2">
<Button
:loading="loading"
:label="__('Save')"
variant="solid"
@click="saveChanges"
/>
<Button :label="__('Reset')" @click="reload" />
</div>
</template>
</Dialog> </Dialog>
</template> </template>
<script setup> <script setup>
@ -85,7 +75,7 @@ import SidePanelLayout from '@/components/SidePanelLayout.vue'
import SidePanelLayoutEditor from '@/components/SidePanelLayoutEditor.vue' import SidePanelLayoutEditor from '@/components/SidePanelLayoutEditor.vue'
import { useDebounceFn } from '@vueuse/core' import { useDebounceFn } from '@vueuse/core'
import { capture } from '@/telemetry' import { capture } from '@/telemetry'
import { Dialog, Badge, Switch, call, createResource } from 'frappe-ui' import { Dialog, Badge, call, createResource } from 'frappe-ui'
import { ref, watch, onMounted, nextTick } from 'vue' import { ref, watch, onMounted, nextTick } from 'vue'
const props = defineProps({ const props = defineProps({

View File

@ -1,8 +1,9 @@
import { createResource } from 'frappe-ui' import { createResource } from 'frappe-ui'
import { formatCurrency, formatNumber } from '@/utils/numberFormat.js' import { formatCurrency, formatNumber } from '@/utils/numberFormat.js'
import { reactive } from 'vue' import { ref, reactive } from 'vue'
const doctypeMeta = reactive({}) const doctypeMeta = reactive({})
const userSettings = reactive({})
export function getMeta(doctype) { export function getMeta(doctype) {
const meta = createResource({ const meta = createResource({
@ -18,6 +19,8 @@ export function getMeta(doctype) {
for (let dtMeta of dtMetas) { for (let dtMeta of dtMetas) {
doctypeMeta[dtMeta.name] = dtMeta doctypeMeta[dtMeta.name] = dtMeta
} }
userSettings[doctype] = JSON.parse(res.user_settings)
}, },
}) })
@ -52,9 +55,54 @@ export function getMeta(doctype) {
return formatCurrency(doc[fieldname], '', currency, precision) return formatCurrency(doc[fieldname], '', currency, precision)
} }
function getGridSettings(parentDoctype, dt = null) {
dt = dt || doctype
if (!userSettings[parentDoctype]['GridView']?.[doctype]) return {}
return userSettings[parentDoctype]['GridView'][doctype]
}
function getFields(dt = null) {
dt = dt || doctype
return doctypeMeta[dt]?.fields.map((f) => {
if (f.fieldtype === 'Select' && typeof f.options === 'string') {
f.options = f.options.split('\n').map((option) => {
return {
label: option,
value: option,
}
})
}
return f
})
}
function saveUserSettings(parentDoctype, key, value, callback) {
let oldUserSettings = userSettings[parentDoctype]
let newUserSettings = JSON.parse(JSON.stringify(oldUserSettings))
newUserSettings[key][doctype] = value
if (JSON.stringify(oldUserSettings) !== JSON.stringify(newUserSettings)) {
return createResource({
url: 'frappe.model.utils.user_settings.save',
params: {
doctype: parentDoctype,
user_settings: JSON.stringify(newUserSettings),
},
auto: true,
onSuccess: () => callback?.(),
})
}
return callback?.()
}
return { return {
meta, meta,
doctypeMeta, doctypeMeta,
userSettings,
getFields,
getGridSettings,
saveUserSettings,
getFormattedFloat, getFormattedFloat,
getFormattedPercent, getFormattedPercent,
getFormattedCurrency, getFormattedCurrency,

View File

@ -305,3 +305,14 @@ export function isImage(extention) {
extention.toLowerCase(), extention.toLowerCase(),
) )
} }
export function getRandom(len) {
let text = ''
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
Array.from({ length: len }).forEach(() => {
text += possible.charAt(Math.floor(Math.random() * possible.length))
})
return text
}

View File

@ -2388,10 +2388,10 @@ fraction.js@^4.3.7:
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
frappe-ui@^0.1.91: frappe-ui@^0.1.94:
version "0.1.93" version "0.1.94"
resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.93.tgz#0443800e195cddcff88ba875989148a92a270206" resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.94.tgz#574a193ce51b47cb2747a5d5f9e9fe91b164e3a6"
integrity sha512-21VUcBBB9g1o7Iv/TW8ng3SdCxsm9G/g0rEdC9Y1Vqx1A1Ucf8VtVFECX2MgoDrLAwVOBKayU5BMaS1fuphcnA== integrity sha512-WV7nApCrDBqtKPPiVVxqFgA0JdWQz9kATZylfmMJQrSWszHCx0k0eriTSvT9+0vmtT0T9UYqLfXnhNj0lnqYNA==
dependencies: dependencies:
"@headlessui/vue" "^1.7.14" "@headlessui/vue" "^1.7.14"
"@popperjs/core" "^2.11.2" "@popperjs/core" "^2.11.2"