Merge pull request #212 from frappe/develop
chore: Merge develop to main
This commit is contained in:
commit
17ca40ea73
3
.gitignore
vendored
3
.gitignore
vendored
@ -7,5 +7,6 @@ dev-dist
|
|||||||
tags
|
tags
|
||||||
node_modules
|
node_modules
|
||||||
crm/public/frontend
|
crm/public/frontend
|
||||||
|
frontend/yarn.lock
|
||||||
crm/www/crm.html
|
crm/www/crm.html
|
||||||
build
|
build
|
||||||
|
|||||||
105
crm/api/doc.py
105
crm/api/doc.py
@ -89,7 +89,7 @@ def get_filterable_fields(doctype: str):
|
|||||||
"options": "User",
|
"options": "User",
|
||||||
},
|
},
|
||||||
{"fieldname": "_user_tags", "fieldtype": "Data", "label": "Tags"},
|
{"fieldname": "_user_tags", "fieldtype": "Data", "label": "Tags"},
|
||||||
{"fieldname": "_liked_by", "fieldtype": "Data", "label": "Liked By"},
|
{"fieldname": "_liked_by", "fieldtype": "Data", "label": "Like"},
|
||||||
{"fieldname": "_comments", "fieldtype": "Text", "label": "Comments"},
|
{"fieldname": "_comments", "fieldtype": "Text", "label": "Comments"},
|
||||||
{"fieldname": "_assign", "fieldtype": "Text", "label": "Assigned To"},
|
{"fieldname": "_assign", "fieldtype": "Text", "label": "Assigned To"},
|
||||||
{"fieldname": "creation", "fieldtype": "Datetime", "label": "Created On"},
|
{"fieldname": "creation", "fieldtype": "Datetime", "label": "Created On"},
|
||||||
@ -108,6 +108,54 @@ def get_filterable_fields(doctype: str):
|
|||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_group_by_fields(doctype: str):
|
||||||
|
allowed_fieldtypes = [
|
||||||
|
"Check",
|
||||||
|
"Data",
|
||||||
|
"Float",
|
||||||
|
"Int",
|
||||||
|
"Currency",
|
||||||
|
"Dynamic Link",
|
||||||
|
"Link",
|
||||||
|
"Select",
|
||||||
|
"Duration",
|
||||||
|
"Date",
|
||||||
|
"Datetime",
|
||||||
|
]
|
||||||
|
|
||||||
|
fields = frappe.get_meta(doctype).fields
|
||||||
|
fields = [field for field in fields if field.fieldtype not in no_value_fields and field.fieldtype in allowed_fieldtypes]
|
||||||
|
fields = [
|
||||||
|
{
|
||||||
|
"label": _(field.label),
|
||||||
|
"value": field.fieldname,
|
||||||
|
}
|
||||||
|
for field in fields
|
||||||
|
if field.label and field.fieldname
|
||||||
|
]
|
||||||
|
|
||||||
|
standard_fields = [
|
||||||
|
{"label": "Name", "value": "name"},
|
||||||
|
{"label": "Created On", "value": "creation"},
|
||||||
|
{"label": "Last Modified", "value": "modified"},
|
||||||
|
{"label": "Modified By", "value": "modified_by"},
|
||||||
|
{"label": "Owner", "value": "owner"},
|
||||||
|
{"label": "Liked By", "value": "_liked_by"},
|
||||||
|
{"label": "Assigned To", "value": "_assign"},
|
||||||
|
{"label": "Comments", "value": "_comments"},
|
||||||
|
{"label": "Created On", "value": "creation"},
|
||||||
|
{"label": "Modified On", "value": "modified"},
|
||||||
|
]
|
||||||
|
|
||||||
|
for field in standard_fields:
|
||||||
|
field["label"] = _(field["label"])
|
||||||
|
fields.append(field)
|
||||||
|
|
||||||
|
return fields
|
||||||
|
|
||||||
|
|
||||||
def get_fields_meta(DocField, doctype, allowed_fieldtypes, restricted_fields):
|
def get_fields_meta(DocField, doctype, allowed_fieldtypes, restricted_fields):
|
||||||
parent = "parent" if DocField._table_name == "tabDocField" else "dt"
|
parent = "parent" if DocField._table_name == "tabDocField" else "dt"
|
||||||
return (
|
return (
|
||||||
@ -159,12 +207,16 @@ def get_list_data(
|
|||||||
page_length_count=20,
|
page_length_count=20,
|
||||||
columns=None,
|
columns=None,
|
||||||
rows=None,
|
rows=None,
|
||||||
custom_view_name=None,
|
view=None,
|
||||||
default_filters=None,
|
default_filters=None,
|
||||||
):
|
):
|
||||||
custom_view = False
|
custom_view = False
|
||||||
filters = frappe._dict(filters)
|
filters = frappe._dict(filters)
|
||||||
|
|
||||||
|
custom_view_name = view.get('custom_view_name') if view else None
|
||||||
|
view_type = view.get('view_type') if view else None
|
||||||
|
group_by_field = view.get('group_by_field') if view else None
|
||||||
|
|
||||||
for key in filters:
|
for key in filters:
|
||||||
value = filters[key]
|
value = filters[key]
|
||||||
if isinstance(value, list):
|
if isinstance(value, list):
|
||||||
@ -197,8 +249,15 @@ def get_list_data(
|
|||||||
if not rows:
|
if not rows:
|
||||||
rows = ["name"]
|
rows = ["name"]
|
||||||
|
|
||||||
if not custom_view and frappe.db.exists("CRM View Settings", doctype):
|
default_view_filters = {
|
||||||
list_view_settings = frappe.get_doc("CRM View Settings", doctype)
|
"dt": doctype,
|
||||||
|
"type": view_type or 'list',
|
||||||
|
"is_default": 1,
|
||||||
|
"user": frappe.session.user,
|
||||||
|
}
|
||||||
|
|
||||||
|
if not custom_view and frappe.db.exists("CRM View Settings", default_view_filters):
|
||||||
|
list_view_settings = frappe.get_doc("CRM View Settings", default_view_filters)
|
||||||
columns = frappe.parse_json(list_view_settings.columns)
|
columns = frappe.parse_json(list_view_settings.columns)
|
||||||
rows = frappe.parse_json(list_view_settings.rows)
|
rows = frappe.parse_json(list_view_settings.rows)
|
||||||
is_default = False
|
is_default = False
|
||||||
@ -218,6 +277,10 @@ def get_list_data(
|
|||||||
if column.get("key") == "_liked_by" and column.get("width") == "10rem":
|
if column.get("key") == "_liked_by" and column.get("width") == "10rem":
|
||||||
column["width"] = "50px"
|
column["width"] = "50px"
|
||||||
|
|
||||||
|
# check if rows has group_by_field if not add it
|
||||||
|
if group_by_field and group_by_field not in rows:
|
||||||
|
rows.append(group_by_field)
|
||||||
|
|
||||||
data = frappe.get_list(
|
data = frappe.get_list(
|
||||||
doctype,
|
doctype,
|
||||||
fields=rows,
|
fields=rows,
|
||||||
@ -251,7 +314,7 @@ def get_list_data(
|
|||||||
},
|
},
|
||||||
{"label": "Assigned To", "type": "Text", "value": "_assign"},
|
{"label": "Assigned To", "type": "Text", "value": "_assign"},
|
||||||
{"label": "Owner", "type": "Link", "value": "owner", "options": "User"},
|
{"label": "Owner", "type": "Link", "value": "owner", "options": "User"},
|
||||||
{"label": "Liked By", "type": "Data", "value": "_liked_by"},
|
{"label": "Like", "type": "Data", "value": "_liked_by"},
|
||||||
]
|
]
|
||||||
|
|
||||||
for field in std_fields:
|
for field in std_fields:
|
||||||
@ -264,11 +327,43 @@ def get_list_data(
|
|||||||
if not is_default and custom_view_name:
|
if not is_default and custom_view_name:
|
||||||
is_default = frappe.db.get_value("CRM View Settings", custom_view_name, "load_default_columns")
|
is_default = frappe.db.get_value("CRM View Settings", custom_view_name, "load_default_columns")
|
||||||
|
|
||||||
|
if group_by_field and view_type == "group_by":
|
||||||
|
def get_options(type, options):
|
||||||
|
if type == "Select":
|
||||||
|
return [option for option in options.split("\n")]
|
||||||
|
else:
|
||||||
|
has_empty_values = any([not d.get(group_by_field) for d in data])
|
||||||
|
options = list(set([d.get(group_by_field) for d in data]))
|
||||||
|
options = [u for u in options if u]
|
||||||
|
if has_empty_values:
|
||||||
|
options.append("")
|
||||||
|
|
||||||
|
if order_by and group_by_field in order_by:
|
||||||
|
order_by_fields = order_by.split(",")
|
||||||
|
order_by_fields = [(field.split(" ")[0], field.split(" ")[1]) for field in order_by_fields]
|
||||||
|
if (group_by_field, "asc") in order_by_fields:
|
||||||
|
options.sort()
|
||||||
|
elif (group_by_field, "desc") in order_by_fields:
|
||||||
|
options.sort(reverse=True)
|
||||||
|
else:
|
||||||
|
options.sort()
|
||||||
|
return options
|
||||||
|
|
||||||
|
for field in fields:
|
||||||
|
if field.get("value") == group_by_field:
|
||||||
|
group_by_field = {
|
||||||
|
"label": field.get("label"),
|
||||||
|
"name": field.get("value"),
|
||||||
|
"type": field.get("type"),
|
||||||
|
"options": get_options(field.get("type"), field.get("options")),
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"data": data,
|
"data": data,
|
||||||
"columns": columns,
|
"columns": columns,
|
||||||
"rows": rows,
|
"rows": rows,
|
||||||
"fields": fields,
|
"fields": fields,
|
||||||
|
"group_by_field": group_by_field,
|
||||||
"page_length": page_length,
|
"page_length": page_length,
|
||||||
"page_length_count": page_length_count,
|
"page_length_count": page_length_count,
|
||||||
"is_default": is_default,
|
"is_default": is_default,
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
"user",
|
"user",
|
||||||
"is_default",
|
"is_default",
|
||||||
"column_break_zacm",
|
"column_break_zacm",
|
||||||
|
"type",
|
||||||
"dt",
|
"dt",
|
||||||
"route_name",
|
"route_name",
|
||||||
"pinned",
|
"pinned",
|
||||||
@ -21,7 +22,9 @@
|
|||||||
"filters_tab",
|
"filters_tab",
|
||||||
"filters",
|
"filters",
|
||||||
"order_by_tab",
|
"order_by_tab",
|
||||||
"order_by"
|
"order_by",
|
||||||
|
"group_by_tab",
|
||||||
|
"group_by_field"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@ -117,11 +120,28 @@
|
|||||||
"fieldname": "icon",
|
"fieldname": "icon",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Icon"
|
"label": "Icon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "list",
|
||||||
|
"fieldname": "type",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Type",
|
||||||
|
"options": "list\ngroup_by"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "group_by_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Group By"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "group_by_field",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Group By Field"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-05-20 17:24:18.662389",
|
"modified": "2024-06-01 16:58:34.952945",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "FCRM",
|
"module": "FCRM",
|
||||||
"name": "CRM View Settings",
|
"name": "CRM View Settings",
|
||||||
|
|||||||
@ -27,6 +27,7 @@ def create(view):
|
|||||||
doc = frappe.new_doc("CRM View Settings")
|
doc = frappe.new_doc("CRM View Settings")
|
||||||
doc.name = view.label
|
doc.name = view.label
|
||||||
doc.label = view.label
|
doc.label = view.label
|
||||||
|
doc.type = view.type or 'list'
|
||||||
doc.icon = view.icon
|
doc.icon = view.icon
|
||||||
doc.dt = view.doctype
|
doc.dt = view.doctype
|
||||||
doc.user = frappe.session.user
|
doc.user = frappe.session.user
|
||||||
@ -34,6 +35,7 @@ def create(view):
|
|||||||
doc.load_default_columns = view.load_default_columns or False
|
doc.load_default_columns = view.load_default_columns or False
|
||||||
doc.filters = json.dumps(view.filters)
|
doc.filters = json.dumps(view.filters)
|
||||||
doc.order_by = view.order_by
|
doc.order_by = view.order_by
|
||||||
|
doc.group_by_field = view.group_by_field
|
||||||
doc.columns = json.dumps(view.columns)
|
doc.columns = json.dumps(view.columns)
|
||||||
doc.rows = json.dumps(view.rows)
|
doc.rows = json.dumps(view.rows)
|
||||||
doc.insert()
|
doc.insert()
|
||||||
@ -53,11 +55,13 @@ def update(view):
|
|||||||
|
|
||||||
doc = frappe.get_doc("CRM View Settings", view.name)
|
doc = frappe.get_doc("CRM View Settings", view.name)
|
||||||
doc.label = view.label
|
doc.label = view.label
|
||||||
|
doc.type = view.type or 'list'
|
||||||
doc.icon = view.icon
|
doc.icon = view.icon
|
||||||
doc.route_name = view.route_name or ""
|
doc.route_name = view.route_name or ""
|
||||||
doc.load_default_columns = view.load_default_columns or False
|
doc.load_default_columns = view.load_default_columns or False
|
||||||
doc.filters = json.dumps(filters)
|
doc.filters = json.dumps(filters)
|
||||||
doc.order_by = view.order_by
|
doc.order_by = view.order_by
|
||||||
|
doc.group_by_field = view.group_by_field
|
||||||
doc.columns = json.dumps(columns)
|
doc.columns = json.dumps(columns)
|
||||||
doc.rows = json.dumps(rows)
|
doc.rows = json.dumps(rows)
|
||||||
doc.save()
|
doc.save()
|
||||||
@ -123,28 +127,38 @@ def create_or_update_default_view(view):
|
|||||||
|
|
||||||
doc = frappe.db.exists(
|
doc = frappe.db.exists(
|
||||||
"CRM View Settings",
|
"CRM View Settings",
|
||||||
{"dt": view.doctype, "is_default": True, "user": frappe.session.user},
|
{
|
||||||
|
"dt": view.doctype,
|
||||||
|
"type": view.type or 'list',
|
||||||
|
"is_default": True,
|
||||||
|
"user": frappe.session.user
|
||||||
|
},
|
||||||
)
|
)
|
||||||
if doc:
|
if doc:
|
||||||
doc = frappe.get_doc("CRM View Settings", doc)
|
doc = frappe.get_doc("CRM View Settings", doc)
|
||||||
doc.label = view.label
|
doc.label = view.label
|
||||||
|
doc.type = view.type or 'list'
|
||||||
doc.route_name = view.route_name or ""
|
doc.route_name = view.route_name or ""
|
||||||
doc.load_default_columns = view.load_default_columns or False
|
doc.load_default_columns = view.load_default_columns or False
|
||||||
doc.filters = json.dumps(filters)
|
doc.filters = json.dumps(filters)
|
||||||
doc.order_by = view.order_by
|
doc.order_by = view.order_by
|
||||||
|
doc.group_by_field = view.group_by_field
|
||||||
doc.columns = json.dumps(columns)
|
doc.columns = json.dumps(columns)
|
||||||
doc.rows = json.dumps(rows)
|
doc.rows = json.dumps(rows)
|
||||||
doc.save()
|
doc.save()
|
||||||
else:
|
else:
|
||||||
doc = frappe.new_doc("CRM View Settings")
|
doc = frappe.new_doc("CRM View Settings")
|
||||||
doc.name = view.label or 'List View'
|
label = 'Group By View' if view.type == 'group_by' else 'List View'
|
||||||
doc.label = view.label or 'List View'
|
doc.name = view.label or label
|
||||||
|
doc.label = view.label or label
|
||||||
|
doc.type = view.type or 'list'
|
||||||
doc.dt = view.doctype
|
doc.dt = view.doctype
|
||||||
doc.user = frappe.session.user
|
doc.user = frappe.session.user
|
||||||
doc.route_name = view.route_name or ""
|
doc.route_name = view.route_name or ""
|
||||||
doc.load_default_columns = view.load_default_columns or False
|
doc.load_default_columns = view.load_default_columns or False
|
||||||
doc.filters = json.dumps(filters)
|
doc.filters = json.dumps(filters)
|
||||||
doc.order_by = view.order_by
|
doc.order_by = view.order_by
|
||||||
|
doc.group_by_field = view.group_by_field
|
||||||
doc.columns = json.dumps(columns)
|
doc.columns = json.dumps(columns)
|
||||||
doc.rows = json.dumps(rows)
|
doc.rows = json.dumps(rows)
|
||||||
doc.is_default = True
|
doc.is_default = True
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
Subproject commit 1394a12b6de105649c8ca5beeead62a38ef1b18e
|
Subproject commit d7ad7bd0d09f25a446da984e6006479ea218acd0
|
||||||
@ -177,14 +177,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body class="sm:overscroll-y-none no-scrollbar">
|
<body class="sm:overscroll-y-none no-scrollbar">
|
||||||
<div id="app" class="h-full"></div>
|
<div id="app" class="h-full"></div>
|
||||||
<div id="modals"></div>
|
|
||||||
<div id="popovers"></div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
{% for key in boot %}
|
|
||||||
window["{{ key }}"] = {{ boot[key] | tojson }};
|
|
||||||
{% endfor %}
|
|
||||||
</script>
|
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -13,8 +13,9 @@
|
|||||||
"@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.55",
|
"frappe-ui": "^0.1.59",
|
||||||
"gemoji": "^8.1.0",
|
"gemoji": "^8.1.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"mime": "^4.0.1",
|
"mime": "^4.0.1",
|
||||||
"pinia": "^2.0.33",
|
"pinia": "^2.0.33",
|
||||||
"socket.io-client": "^4.7.2",
|
"socket.io-client": "^4.7.2",
|
||||||
|
|||||||
@ -10,11 +10,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { Dialogs } from '@/utils/dialogs'
|
import { Dialogs } from '@/utils/dialogs'
|
||||||
import { sessionStore as session } from '@/stores/session'
|
import { sessionStore as session } from '@/stores/session'
|
||||||
import { useScreenSize } from '@/composables'
|
|
||||||
import { Toasts } from 'frappe-ui'
|
import { Toasts } from 'frappe-ui'
|
||||||
import { computed, defineAsyncComponent } from 'vue'
|
import { computed, defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
const screenSize = useScreenSize()
|
|
||||||
const MobileLayout = defineAsyncComponent(() =>
|
const MobileLayout = defineAsyncComponent(() =>
|
||||||
import('./components/Layouts/MobileLayout.vue')
|
import('./components/Layouts/MobileLayout.vue')
|
||||||
)
|
)
|
||||||
@ -22,7 +20,7 @@ const DesktopLayout = defineAsyncComponent(() =>
|
|||||||
import('./components/Layouts/DesktopLayout.vue')
|
import('./components/Layouts/DesktopLayout.vue')
|
||||||
)
|
)
|
||||||
const Layout = computed(() => {
|
const Layout = computed(() => {
|
||||||
if (screenSize.width < 640) {
|
if (window.innerWidth < 640) {
|
||||||
return MobileLayout
|
return MobileLayout
|
||||||
} else {
|
} else {
|
||||||
return DesktopLayout
|
return DesktopLayout
|
||||||
|
|||||||
@ -924,7 +924,7 @@ import {
|
|||||||
import { globalStore } from '@/stores/global'
|
import { globalStore } from '@/stores/global'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { contactsStore } from '@/stores/contacts'
|
import { contactsStore } from '@/stores/contacts'
|
||||||
import { whatsappEnabled, callEnabled } from '@/stores/settings'
|
import { whatsappEnabled, callEnabled } from '@/composables/settings'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
|||||||
@ -2,7 +2,10 @@
|
|||||||
<NestedPopover>
|
<NestedPopover>
|
||||||
<template #target>
|
<template #target>
|
||||||
<Button :label="__('Columns')">
|
<Button :label="__('Columns')">
|
||||||
<template #prefix>
|
<template v-if="hideLabel">
|
||||||
|
<ColumnsIcon class="h-4" />
|
||||||
|
</template>
|
||||||
|
<template v-if="!hideLabel" #prefix>
|
||||||
<ColumnsIcon class="h-4" />
|
<ColumnsIcon class="h-4" />
|
||||||
</template>
|
</template>
|
||||||
</Button>
|
</Button>
|
||||||
@ -15,6 +18,7 @@
|
|||||||
<Draggable
|
<Draggable
|
||||||
:list="columns"
|
:list="columns"
|
||||||
@end="apply"
|
@end="apply"
|
||||||
|
:delay="isTouchScreenDevice() ? 200 : 0"
|
||||||
item-key="key"
|
item-key="key"
|
||||||
class="list-group"
|
class="list-group"
|
||||||
>
|
>
|
||||||
@ -98,17 +102,21 @@
|
|||||||
size="md"
|
size="md"
|
||||||
:label="__('Label')"
|
:label="__('Label')"
|
||||||
v-model="column.label"
|
v-model="column.label"
|
||||||
class="w-full"
|
class="sm:w-full w-52"
|
||||||
:placeholder="__('First Name')"
|
:placeholder="__('First Name')"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
type="text"
|
type="text"
|
||||||
size="md"
|
size="md"
|
||||||
:label="__('Width')"
|
:label="__('Width')"
|
||||||
class="w-full"
|
class="sm:w-full w-52"
|
||||||
v-model="column.width"
|
v-model="column.width"
|
||||||
placeholder="10rem"
|
placeholder="10rem"
|
||||||
:description="__('Width can be in number, pixel or rem (eg. 3, 30px, 10rem)')"
|
:description="
|
||||||
|
__(
|
||||||
|
'Width can be in number, pixel or rem (eg. 3, 30px, 10rem)'
|
||||||
|
)
|
||||||
|
"
|
||||||
:debounce="500"
|
:debounce="500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -140,6 +148,7 @@ import DragIcon from '@/components/Icons/DragIcon.vue'
|
|||||||
import ReloadIcon from '@/components/Icons/ReloadIcon.vue'
|
import ReloadIcon from '@/components/Icons/ReloadIcon.vue'
|
||||||
import NestedPopover from '@/components/NestedPopover.vue'
|
import NestedPopover from '@/components/NestedPopover.vue'
|
||||||
import Autocomplete from '@/components/frappe-ui/Autocomplete.vue'
|
import Autocomplete from '@/components/frappe-ui/Autocomplete.vue'
|
||||||
|
import { isTouchScreenDevice } from '@/utils'
|
||||||
import Draggable from 'vuedraggable'
|
import Draggable from 'vuedraggable'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { watchOnce } from '@vueuse/core'
|
import { watchOnce } from '@vueuse/core'
|
||||||
@ -149,6 +158,10 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
hideLabel: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update'])
|
const emit = defineEmits(['update'])
|
||||||
|
|||||||
@ -23,14 +23,14 @@
|
|||||||
>
|
>
|
||||||
<div class="flex items-center p-1 text-gray-500">
|
<div class="flex items-center p-1 text-gray-500">
|
||||||
<Button variant="ghost" class="h-7 w-7" @click="prevMonth">
|
<Button variant="ghost" class="h-7 w-7" @click="prevMonth">
|
||||||
<FeatherIcon stroke-width="2" name="chevron-left" class="h-4 w-4" />
|
<FeatherIcon :stroke-width="2" name="chevron-left" class="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<div class="flex-1 text-center text-base font-medium text-gray-700">
|
<div class="flex-1 text-center text-base font-medium text-gray-700">
|
||||||
{{ formatMonth }}
|
{{ formatMonth }}
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" class="h-7 w-7" @click="nextMonth">
|
<Button variant="ghost" class="h-7 w-7" @click="nextMonth">
|
||||||
<FeatherIcon
|
<FeatherIcon
|
||||||
stroke-width="2"
|
:stroke-width="2"
|
||||||
name="chevron-right"
|
name="chevron-right"
|
||||||
class="h-4 w-4"
|
class="h-4 w-4"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -22,14 +22,14 @@
|
|||||||
>
|
>
|
||||||
<div class="flex items-center p-1 text-gray-500">
|
<div class="flex items-center p-1 text-gray-500">
|
||||||
<Button variant="ghost" class="h-7 w-7" @click="prevMonth">
|
<Button variant="ghost" class="h-7 w-7" @click="prevMonth">
|
||||||
<FeatherIcon stroke-width="2" name="chevron-left" class="h-4 w-4" />
|
<FeatherIcon :stroke-width="2" name="chevron-left" class="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<div class="flex-1 text-center text-base font-medium text-gray-700">
|
<div class="flex-1 text-center text-base font-medium text-gray-700">
|
||||||
{{ formatMonth }}
|
{{ formatMonth }}
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" class="h-7 w-7" @click="nextMonth">
|
<Button variant="ghost" class="h-7 w-7" @click="nextMonth">
|
||||||
<FeatherIcon
|
<FeatherIcon
|
||||||
stroke-width="2"
|
:stroke-width="2"
|
||||||
name="chevron-right"
|
name="chevron-right"
|
||||||
class="h-4 w-4"
|
class="h-4 w-4"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -20,14 +20,14 @@
|
|||||||
>
|
>
|
||||||
<div class="flex items-center p-1 text-gray-500">
|
<div class="flex items-center p-1 text-gray-500">
|
||||||
<Button variant="ghost" class="h-7 w-7" @click="prevMonth">
|
<Button variant="ghost" class="h-7 w-7" @click="prevMonth">
|
||||||
<FeatherIcon stroke-width="2" name="chevron-left" class="h-4 w-4" />
|
<FeatherIcon :stroke-width="2" name="chevron-left" class="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<div class="flex-1 text-center text-base font-medium text-gray-700">
|
<div class="flex-1 text-center text-base font-medium text-gray-700">
|
||||||
{{ formatMonth }}
|
{{ formatMonth }}
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" class="h-7 w-7" @click="nextMonth">
|
<Button variant="ghost" class="h-7 w-7" @click="nextMonth">
|
||||||
<FeatherIcon
|
<FeatherIcon
|
||||||
stroke-width="2"
|
:stroke-width="2"
|
||||||
name="chevron-right"
|
name="chevron-right"
|
||||||
class="h-4 w-4"
|
class="h-4 w-4"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -33,7 +33,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, h } from 'vue'
|
import { computed, h } from 'vue'
|
||||||
import { Dropdown } from 'frappe-ui'
|
import { Dropdown } from 'frappe-ui'
|
||||||
import { isMobileView } from '@/stores/settings'
|
import { isMobileView } from '@/composables/settings'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
actions: {
|
actions: {
|
||||||
|
|||||||
@ -23,7 +23,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const files = import.meta.globEager('/src/index.css')
|
const files = import.meta.globEager('/src/index.css', { query: '?inline' })
|
||||||
const css = files['/src/index.css'].default
|
const css = files['/src/index.css'].default
|
||||||
|
|
||||||
const iframeRef = ref(null)
|
const iframeRef = ref(null)
|
||||||
|
|||||||
@ -1,44 +1,54 @@
|
|||||||
<template>
|
<template>
|
||||||
<NestedPopover>
|
<NestedPopover>
|
||||||
<template #target>
|
<template #target>
|
||||||
<Button
|
<div class="flex items-center">
|
||||||
:label="__('Filter')"
|
<Button
|
||||||
:class="filters?.size ? 'rounded-r-none' : ''"
|
:label="__('Filter')"
|
||||||
>
|
:class="filters?.size ? 'rounded-r-none' : ''"
|
||||||
<template #prefix><FilterIcon class="h-4" /></template>
|
>
|
||||||
<template v-if="filters?.size" #suffix>
|
<template #prefix><FilterIcon class="h-4" /></template>
|
||||||
<div
|
<template v-if="filters?.size" #suffix>
|
||||||
class="flex h-5 w-5 items-center justify-center rounded bg-gray-900 pt-[1px] text-2xs font-medium text-white"
|
<div
|
||||||
>
|
class="flex h-5 w-5 items-center justify-center rounded bg-gray-900 pt-[1px] text-2xs font-medium text-white"
|
||||||
{{ filters.size }}
|
>
|
||||||
|
{{ filters.size }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
<Tooltip v-if="filters?.size" :text="__('Clear all Filter')">
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
class="rounded-l-none border-l"
|
||||||
|
icon="x"
|
||||||
|
@click.stop="clearfilter(false)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</Tooltip>
|
||||||
</Button>
|
</div>
|
||||||
<Tooltip v-if="filters?.size" :text="__('Clear all Filter')">
|
|
||||||
<span>
|
|
||||||
<Button
|
|
||||||
class="rounded-l-none border-l"
|
|
||||||
icon="x"
|
|
||||||
@click.stop="clearfilter(false)"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
</template>
|
</template>
|
||||||
<template #body="{ close }">
|
<template #body="{ close }">
|
||||||
<div class="my-2 rounded-lg border border-gray-100 bg-white shadow-xl">
|
<div class="my-2 rounded-lg border border-gray-100 bg-white shadow-xl">
|
||||||
<div class="min-w-[400px] p-2">
|
<div class="min-w-72 p-2 sm:min-w-[400px]">
|
||||||
<div
|
<div
|
||||||
v-if="filters?.size"
|
v-if="filters?.size"
|
||||||
v-for="(f, i) in filters"
|
v-for="(f, i) in filters"
|
||||||
:key="i"
|
:key="i"
|
||||||
id="filter-list"
|
id="filter-list"
|
||||||
class="mb-3 flex items-center justify-between gap-2"
|
class="sm:mb-3 mb-4"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div v-if="isMobileView" class="flex flex-col gap-2">
|
||||||
<div class="w-13 pl-2 text-end text-base text-gray-600">
|
<div class="flex w-full items-center justify-between -mb-2">
|
||||||
{{ i == 0 ? __('Where') : __('And') }}
|
<div class="text-base text-gray-600">
|
||||||
|
{{ i == 0 ? __('Where') : __('And') }}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
class="flex"
|
||||||
|
variant="ghost"
|
||||||
|
icon="x"
|
||||||
|
@click="removeFilter(i)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div id="fieldname" class="!min-w-[140px]">
|
<div id="fieldname" class="w-full">
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
:value="f.field.fieldname"
|
:value="f.field.fieldname"
|
||||||
:options="filterableFields.data"
|
:options="filterableFields.data"
|
||||||
@ -55,16 +65,55 @@
|
|||||||
:placeholder="__('Equals')"
|
:placeholder="__('Equals')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div id="value" class="!min-w-[140px]">
|
<div id="value" class="w-full">
|
||||||
<component
|
<component
|
||||||
:is="getValSelect(f)"
|
:is="getValSelect(f)"
|
||||||
v-model="f.value"
|
v-model="f.value"
|
||||||
@change="(v) => updateValue(v, f)"
|
@change.stop="(v) => updateValue(v, f)"
|
||||||
:placeholder="__('John Doe')"
|
:placeholder="__('John Doe')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" icon="x" @click="removeFilter(i)" />
|
<div v-else class="flex items-center justify-between gap-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-13 pl-2 text-end text-base text-gray-600">
|
||||||
|
{{ i == 0 ? __('Where') : __('And') }}
|
||||||
|
</div>
|
||||||
|
<div id="fieldname" class="!min-w-[140px]">
|
||||||
|
<Autocomplete
|
||||||
|
:value="f.field.fieldname"
|
||||||
|
:options="filterableFields.data"
|
||||||
|
@change="(e) => updateFilter(e, i)"
|
||||||
|
:placeholder="__('First Name')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div id="operator">
|
||||||
|
<FormControl
|
||||||
|
type="select"
|
||||||
|
v-model="f.operator"
|
||||||
|
@change="(e) => updateOperator(e, f)"
|
||||||
|
:options="
|
||||||
|
getOperators(f.field.fieldtype, f.field.fieldname)
|
||||||
|
"
|
||||||
|
:placeholder="__('Equals')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div id="value" class="!min-w-[140px]">
|
||||||
|
<component
|
||||||
|
:is="getValSelect(f)"
|
||||||
|
v-model="f.value"
|
||||||
|
@change.stop="(v) => updateValue(v, f)"
|
||||||
|
:placeholder="__('John Doe')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
class="flex"
|
||||||
|
variant="ghost"
|
||||||
|
icon="x"
|
||||||
|
@click="removeFilter(i)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
@ -115,6 +164,7 @@ import Link from '@/components/Controls/Link.vue'
|
|||||||
import Autocomplete from '@/components/frappe-ui/Autocomplete.vue'
|
import Autocomplete from '@/components/frappe-ui/Autocomplete.vue'
|
||||||
import { FormControl, createResource, Tooltip } from 'frappe-ui'
|
import { FormControl, createResource, Tooltip } from 'frappe-ui'
|
||||||
import { h, computed, onMounted } from 'vue'
|
import { h, computed, onMounted } from 'vue'
|
||||||
|
import { isMobileView } from '@/composables/settings'
|
||||||
|
|
||||||
const typeCheck = ['Check']
|
const typeCheck = ['Check']
|
||||||
const typeLink = ['Link', 'Dynamic Link']
|
const typeLink = ['Link', 'Dynamic Link']
|
||||||
|
|||||||
77
frontend/src/components/GroupBy.vue
Normal file
77
frontend/src/components/GroupBy.vue
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<template>
|
||||||
|
<Autocomplete :options="options" value="" @change="(e) => setGroupBy(e)">
|
||||||
|
<template #target="{ togglePopover, isOpen }">
|
||||||
|
<Button
|
||||||
|
:label="
|
||||||
|
hideLabel
|
||||||
|
? groupByValue?.label
|
||||||
|
: __('Group By: ') + groupByValue?.label
|
||||||
|
"
|
||||||
|
@click="togglePopover()"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<DetailsIcon />
|
||||||
|
</template>
|
||||||
|
<template #suffix>
|
||||||
|
<FeatherIcon
|
||||||
|
:name="isOpen ? 'chevron-up' : 'chevron-down'"
|
||||||
|
class="h-4"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</Autocomplete>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import Autocomplete from '@/components/frappe-ui/Autocomplete.vue'
|
||||||
|
import DetailsIcon from '@/components/Icons/DetailsIcon.vue'
|
||||||
|
import { createResource } from 'frappe-ui'
|
||||||
|
import { ref, computed, onMounted, nextTick } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
doctype: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
hideLabel: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update'])
|
||||||
|
|
||||||
|
const list = defineModel()
|
||||||
|
|
||||||
|
const groupByValue = ref({
|
||||||
|
label: '',
|
||||||
|
value: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const groupByOptions = createResource({
|
||||||
|
url: 'crm.api.doc.get_group_by_fields',
|
||||||
|
cache: ['groupByOptions', props.doctype],
|
||||||
|
params: {
|
||||||
|
doctype: props.doctype,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (groupByOptions.data?.length) return
|
||||||
|
groupByOptions.fetch()
|
||||||
|
})
|
||||||
|
|
||||||
|
function setGroupBy(data) {
|
||||||
|
groupByValue.value = data
|
||||||
|
nextTick(() => emit('update', data.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = computed(() => {
|
||||||
|
if (!groupByOptions.data) return []
|
||||||
|
if (!list.value?.data?.group_by_field) return groupByOptions.data
|
||||||
|
groupByValue.value = list.value.data.group_by_field
|
||||||
|
return groupByOptions.data.filter(
|
||||||
|
(option) => option !== groupByValue.value.value
|
||||||
|
)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
20
frontend/src/components/Icons/AscendingIcon.vue
Normal file
20
frontend/src/components/Icons/AscendingIcon.vue
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<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"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="lucide lucide-arrow-down-a-z"
|
||||||
|
>
|
||||||
|
<path d="m3 16 4 4 4-4" />
|
||||||
|
<path d="M7 20V4" />
|
||||||
|
<path d="M20 8h-5" />
|
||||||
|
<path d="M15 10V6.5a2.5 2.5 0 0 1 5 0V10" />
|
||||||
|
<path d="M15 14h5l-5 6h5" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
20
frontend/src/components/Icons/DesendingIcon.vue
Normal file
20
frontend/src/components/Icons/DesendingIcon.vue
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<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"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="lucide lucide-arrow-up-z-a"
|
||||||
|
>
|
||||||
|
<path d="m3 8 4-4 4 4" />
|
||||||
|
<path d="M7 4v16" />
|
||||||
|
<path d="M15 4h5l-5 6h5" />
|
||||||
|
<path d="M15 20v-3.5a2.5 2.5 0 0 1 5 0V20" />
|
||||||
|
<path d="M20 18h-5" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
@ -74,7 +74,7 @@
|
|||||||
</Section>
|
</Section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isMobileView" class="m-2 flex flex-col gap-1">
|
<div class="m-2 flex flex-col gap-1">
|
||||||
<SidebarLink
|
<SidebarLink
|
||||||
:label="isSidebarCollapsed ? __('Expand') : __('Collapse')"
|
:label="isSidebarCollapsed ? __('Expand') : __('Collapse')"
|
||||||
:isCollapsed="isSidebarCollapsed"
|
:isCollapsed="isSidebarCollapsed"
|
||||||
@ -116,7 +116,6 @@ import { notificationsStore } from '@/stores/notifications'
|
|||||||
import { FeatherIcon } from 'frappe-ui'
|
import { FeatherIcon } from 'frappe-ui'
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
import { computed, h } from 'vue'
|
import { computed, h } from 'vue'
|
||||||
import { isMobileView } from '@/stores/settings'
|
|
||||||
|
|
||||||
const { getPinnedViews, getPublicViews } = viewsStore()
|
const { getPinnedViews, getPublicViews } = viewsStore()
|
||||||
const { toggle: toggleNotificationPanel } = notificationsStore()
|
const { toggle: toggleNotificationPanel } = notificationsStore()
|
||||||
@ -200,6 +199,7 @@ function parseView(views) {
|
|||||||
icon: getIcon(view.route_name, view.icon),
|
icon: getIcon(view.route_name, view.icon),
|
||||||
to: {
|
to: {
|
||||||
name: view.route_name,
|
name: view.route_name,
|
||||||
|
params: { viewType: view.type || 'list' },
|
||||||
query: { view: view.name },
|
query: { view: view.name },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,8 +9,9 @@
|
|||||||
resizeColumn: options.resizeColumn,
|
resizeColumn: options.resizeColumn,
|
||||||
}"
|
}"
|
||||||
row-key="name"
|
row-key="name"
|
||||||
|
v-bind="$attrs"
|
||||||
>
|
>
|
||||||
<ListHeader class="mx-5" @columnWidthUpdated="emit('columnWidthUpdated')">
|
<ListHeader class="sm:mx-5 mx-3" @columnWidthUpdated="emit('columnWidthUpdated')">
|
||||||
<ListHeaderItem
|
<ListHeaderItem
|
||||||
v-for="column in columns"
|
v-for="column in columns"
|
||||||
:key="column.key"
|
:key="column.key"
|
||||||
@ -28,9 +29,8 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</ListHeaderItem>
|
</ListHeaderItem>
|
||||||
</ListHeader>
|
</ListHeader>
|
||||||
<ListRows id="list-rows">
|
<ListRows class="mx-3 sm:mx-5" id="list-rows">
|
||||||
<ListRow
|
<ListRow
|
||||||
class="mx-5"
|
|
||||||
v-for="row in rows"
|
v-for="row in rows"
|
||||||
:key="row.name"
|
:key="row.name"
|
||||||
v-slot="{ idx, column, item }"
|
v-slot="{ idx, column, item }"
|
||||||
@ -140,7 +140,7 @@
|
|||||||
</ListSelectBanner>
|
</ListSelectBanner>
|
||||||
</ListView>
|
</ListView>
|
||||||
<ListFooter
|
<ListFooter
|
||||||
class="border-t px-5 py-2"
|
class="border-t sm:px-5 px-3 py-2"
|
||||||
v-model="pageLengthCount"
|
v-model="pageLengthCount"
|
||||||
:options="{
|
:options="{
|
||||||
rowCount: options.rowCount,
|
rowCount: options.rowCount,
|
||||||
|
|||||||
@ -14,7 +14,10 @@
|
|||||||
}"
|
}"
|
||||||
row-key="name"
|
row-key="name"
|
||||||
>
|
>
|
||||||
<ListHeader class="mx-5" @columnWidthUpdated="emit('columnWidthUpdated')">
|
<ListHeader
|
||||||
|
class="mx-3 sm:mx-5"
|
||||||
|
@columnWidthUpdated="emit('columnWidthUpdated')"
|
||||||
|
>
|
||||||
<ListHeaderItem
|
<ListHeaderItem
|
||||||
v-for="column in columns"
|
v-for="column in columns"
|
||||||
:key="column.key"
|
:key="column.key"
|
||||||
@ -32,9 +35,8 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</ListHeaderItem>
|
</ListHeaderItem>
|
||||||
</ListHeader>
|
</ListHeader>
|
||||||
<ListRows id="list-rows">
|
<ListRows class="mx-3 sm:mx-5" id="list-rows">
|
||||||
<ListRow
|
<ListRow
|
||||||
class="mx-5"
|
|
||||||
v-for="row in rows"
|
v-for="row in rows"
|
||||||
:key="row.name"
|
:key="row.name"
|
||||||
v-slot="{ idx, column, item }"
|
v-slot="{ idx, column, item }"
|
||||||
@ -136,7 +138,7 @@
|
|||||||
</ListView>
|
</ListView>
|
||||||
<ListFooter
|
<ListFooter
|
||||||
v-if="pageLengthCount"
|
v-if="pageLengthCount"
|
||||||
class="border-t px-5 py-2"
|
class="border-t px-3 py-2 sm:px-5"
|
||||||
v-model="pageLengthCount"
|
v-model="pageLengthCount"
|
||||||
:options="{
|
:options="{
|
||||||
rowCount: options.rowCount,
|
rowCount: options.rowCount,
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
}"
|
}"
|
||||||
row-key="name"
|
row-key="name"
|
||||||
>
|
>
|
||||||
<ListHeader class="mx-5" @columnWidthUpdated="emit('columnWidthUpdated')">
|
<ListHeader class="sm:mx-5 mx-3" @columnWidthUpdated="emit('columnWidthUpdated')">
|
||||||
<ListHeaderItem
|
<ListHeaderItem
|
||||||
v-for="column in columns"
|
v-for="column in columns"
|
||||||
:key="column.key"
|
:key="column.key"
|
||||||
@ -29,18 +29,74 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</ListHeaderItem>
|
</ListHeaderItem>
|
||||||
</ListHeader>
|
</ListHeader>
|
||||||
<ListRows id="list-rows">
|
<ListRows :rows="rows" v-slot="{ idx, column, item }">
|
||||||
<ListRow
|
<div v-if="column.key === '_assign'" class="flex items-center">
|
||||||
class="mx-5"
|
<MultipleAvatar
|
||||||
v-for="row in rows"
|
:avatars="item"
|
||||||
:key="row.name"
|
size="sm"
|
||||||
v-slot="{ idx, column, item }"
|
@click="
|
||||||
:row="row"
|
(event) =>
|
||||||
>
|
emit('applyFilter', {
|
||||||
<div v-if="column.key === '_assign'" class="flex items-center">
|
event,
|
||||||
<MultipleAvatar
|
idx,
|
||||||
:avatars="item"
|
column,
|
||||||
size="sm"
|
item,
|
||||||
|
firstColumn: columns[0],
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ListRowItem v-else :item="item">
|
||||||
|
<template #prefix>
|
||||||
|
<div v-if="column.key === 'status'">
|
||||||
|
<IndicatorIcon :class="item.color" />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="column.key === 'organization'">
|
||||||
|
<Avatar
|
||||||
|
v-if="item.label"
|
||||||
|
class="flex items-center"
|
||||||
|
:image="item.logo"
|
||||||
|
:label="item.label"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="column.key === 'deal_owner'">
|
||||||
|
<Avatar
|
||||||
|
v-if="item.full_name"
|
||||||
|
class="flex items-center"
|
||||||
|
:image="item.user_image"
|
||||||
|
:label="item.full_name"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="column.key === 'mobile_no'">
|
||||||
|
<PhoneIcon class="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="column.key === '_liked_by'">
|
||||||
|
<Button
|
||||||
|
v-if="column.key == '_liked_by'"
|
||||||
|
variant="ghosted"
|
||||||
|
:class="isLiked(item) ? 'fill-red-500' : 'fill-white'"
|
||||||
|
@click.stop.prevent="
|
||||||
|
() => emit('likeDoc', { name: row.name, liked: isLiked(item) })
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<HeartIcon class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #default="{ label }">
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
[
|
||||||
|
'modified',
|
||||||
|
'creation',
|
||||||
|
'first_response_time',
|
||||||
|
'first_responded_on',
|
||||||
|
'response_by',
|
||||||
|
].includes(column.key)
|
||||||
|
"
|
||||||
|
class="truncate text-base"
|
||||||
@click="
|
@click="
|
||||||
(event) =>
|
(event) =>
|
||||||
emit('applyFilter', {
|
emit('applyFilter', {
|
||||||
@ -51,60 +107,21 @@
|
|||||||
firstColumn: columns[0],
|
firstColumn: columns[0],
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
/>
|
>
|
||||||
</div>
|
<Tooltip :text="item.label">
|
||||||
<ListRowItem v-else :item="item">
|
<div>{{ item.timeAgo }}</div>
|
||||||
<template #prefix>
|
</Tooltip>
|
||||||
<div v-if="column.key === 'status'">
|
</div>
|
||||||
<IndicatorIcon :class="item.color" />
|
<div
|
||||||
</div>
|
v-else-if="column.key === 'sla_status'"
|
||||||
<div v-else-if="column.key === 'organization'">
|
class="truncate text-base"
|
||||||
<Avatar
|
>
|
||||||
v-if="item.label"
|
<Badge
|
||||||
class="flex items-center"
|
v-if="item.value"
|
||||||
:image="item.logo"
|
:variant="'subtle'"
|
||||||
:label="item.label"
|
:theme="item.color"
|
||||||
size="sm"
|
size="md"
|
||||||
/>
|
:label="item.value"
|
||||||
</div>
|
|
||||||
<div v-else-if="column.key === 'deal_owner'">
|
|
||||||
<Avatar
|
|
||||||
v-if="item.full_name"
|
|
||||||
class="flex items-center"
|
|
||||||
:image="item.user_image"
|
|
||||||
:label="item.full_name"
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="column.key === 'mobile_no'">
|
|
||||||
<PhoneIcon class="h-4 w-4" />
|
|
||||||
</div>
|
|
||||||
<div v-else-if="column.key === '_liked_by'">
|
|
||||||
<Button
|
|
||||||
v-if="column.key == '_liked_by'"
|
|
||||||
variant="ghosted"
|
|
||||||
:class="isLiked(item) ? 'fill-red-500' : 'fill-white'"
|
|
||||||
@click.stop.prevent="
|
|
||||||
() =>
|
|
||||||
emit('likeDoc', { name: row.name, liked: isLiked(item) })
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<HeartIcon class="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #default="{ label }">
|
|
||||||
<div
|
|
||||||
v-if="
|
|
||||||
[
|
|
||||||
'modified',
|
|
||||||
'creation',
|
|
||||||
'first_response_time',
|
|
||||||
'first_responded_on',
|
|
||||||
'response_by',
|
|
||||||
].includes(column.key)
|
|
||||||
"
|
|
||||||
class="truncate text-base"
|
|
||||||
@click="
|
@click="
|
||||||
(event) =>
|
(event) =>
|
||||||
emit('applyFilter', {
|
emit('applyFilter', {
|
||||||
@ -115,60 +132,34 @@
|
|||||||
firstColumn: columns[0],
|
firstColumn: columns[0],
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
>
|
/>
|
||||||
<Tooltip :text="item.label">
|
</div>
|
||||||
<div>{{ item.timeAgo }}</div>
|
<div v-else-if="column.type === 'Check'">
|
||||||
</Tooltip>
|
<FormControl
|
||||||
</div>
|
type="checkbox"
|
||||||
<div
|
:modelValue="item"
|
||||||
v-else-if="column.key === 'sla_status'"
|
:disabled="true"
|
||||||
class="truncate text-base"
|
class="text-gray-900"
|
||||||
>
|
/>
|
||||||
<Badge
|
</div>
|
||||||
v-if="item.value"
|
<div
|
||||||
:variant="'subtle'"
|
v-else
|
||||||
:theme="item.color"
|
class="truncate text-base"
|
||||||
size="md"
|
@click="
|
||||||
:label="item.value"
|
(event) =>
|
||||||
@click="
|
emit('applyFilter', {
|
||||||
(event) =>
|
event,
|
||||||
emit('applyFilter', {
|
idx,
|
||||||
event,
|
column,
|
||||||
idx,
|
item,
|
||||||
column,
|
firstColumn: columns[0],
|
||||||
item,
|
})
|
||||||
firstColumn: columns[0],
|
"
|
||||||
})
|
>
|
||||||
"
|
{{ label }}
|
||||||
/>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
<div v-else-if="column.type === 'Check'">
|
</ListRowItem>
|
||||||
<FormControl
|
|
||||||
type="checkbox"
|
|
||||||
:modelValue="item"
|
|
||||||
:disabled="true"
|
|
||||||
class="text-gray-900"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="truncate text-base"
|
|
||||||
@click="
|
|
||||||
(event) =>
|
|
||||||
emit('applyFilter', {
|
|
||||||
event,
|
|
||||||
idx,
|
|
||||||
column,
|
|
||||||
item,
|
|
||||||
firstColumn: columns[0],
|
|
||||||
})
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{ label }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</ListRowItem>
|
|
||||||
</ListRow>
|
|
||||||
</ListRows>
|
</ListRows>
|
||||||
<ListSelectBanner>
|
<ListSelectBanner>
|
||||||
<template #actions="{ selections, unselectAll }">
|
<template #actions="{ selections, unselectAll }">
|
||||||
@ -182,7 +173,7 @@
|
|||||||
</ListView>
|
</ListView>
|
||||||
<ListFooter
|
<ListFooter
|
||||||
v-if="pageLengthCount"
|
v-if="pageLengthCount"
|
||||||
class="border-t px-5 py-2"
|
class="border-t sm:px-5 px-3 py-2"
|
||||||
v-model="pageLengthCount"
|
v-model="pageLengthCount"
|
||||||
:options="{
|
:options="{
|
||||||
rowCount: options.rowCount,
|
rowCount: options.rowCount,
|
||||||
@ -199,13 +190,12 @@ import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
|||||||
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
||||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||||
import ListBulkActions from '@/components/ListBulkActions.vue'
|
import ListBulkActions from '@/components/ListBulkActions.vue'
|
||||||
|
import ListRows from '@/components/ListViews/ListRows.vue'
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
ListView,
|
ListView,
|
||||||
ListHeader,
|
ListHeader,
|
||||||
ListHeaderItem,
|
ListHeaderItem,
|
||||||
ListRows,
|
|
||||||
ListRow,
|
|
||||||
ListRowItem,
|
ListRowItem,
|
||||||
ListSelectBanner,
|
ListSelectBanner,
|
||||||
ListFooter,
|
ListFooter,
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
}"
|
}"
|
||||||
row-key="name"
|
row-key="name"
|
||||||
>
|
>
|
||||||
<ListHeader class="mx-5" @columnWidthUpdated="emit('columnWidthUpdated')">
|
<ListHeader class="sm:mx-5 mx-3" @columnWidthUpdated="emit('columnWidthUpdated')">
|
||||||
<ListHeaderItem
|
<ListHeaderItem
|
||||||
v-for="column in columns"
|
v-for="column in columns"
|
||||||
:key="column.key"
|
:key="column.key"
|
||||||
@ -28,9 +28,8 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</ListHeaderItem>
|
</ListHeaderItem>
|
||||||
</ListHeader>
|
</ListHeader>
|
||||||
<ListRows id="list-rows">
|
<ListRows class="mx-3 sm:mx-5" id="list-rows">
|
||||||
<ListRow
|
<ListRow
|
||||||
class="mx-5"
|
|
||||||
v-for="row in rows"
|
v-for="row in rows"
|
||||||
:key="row.name"
|
:key="row.name"
|
||||||
v-slot="{ idx, column, item }"
|
v-slot="{ idx, column, item }"
|
||||||
@ -129,7 +128,7 @@
|
|||||||
</ListSelectBanner>
|
</ListSelectBanner>
|
||||||
</ListView>
|
</ListView>
|
||||||
<ListFooter
|
<ListFooter
|
||||||
class="border-t px-5 py-2"
|
class="border-t sm:px-5 px-3 py-2"
|
||||||
v-model="pageLengthCount"
|
v-model="pageLengthCount"
|
||||||
:options="{
|
:options="{
|
||||||
rowCount: options.rowCount,
|
rowCount: options.rowCount,
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
}"
|
}"
|
||||||
row-key="name"
|
row-key="name"
|
||||||
>
|
>
|
||||||
<ListHeader class="mx-5" @columnWidthUpdated="emit('columnWidthUpdated')">
|
<ListHeader class="sm:mx-5 mx-3" @columnWidthUpdated="emit('columnWidthUpdated')">
|
||||||
<ListHeaderItem
|
<ListHeaderItem
|
||||||
v-for="column in columns"
|
v-for="column in columns"
|
||||||
:key="column.key"
|
:key="column.key"
|
||||||
@ -29,18 +29,71 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</ListHeaderItem>
|
</ListHeaderItem>
|
||||||
</ListHeader>
|
</ListHeader>
|
||||||
<ListRows id="list-rows">
|
<ListRows :rows="rows" v-slot="{ idx, column, item }">
|
||||||
<ListRow
|
<div v-if="column.key === '_assign'" class="flex items-center">
|
||||||
class="mx-5"
|
<MultipleAvatar
|
||||||
v-for="row in rows"
|
:avatars="item"
|
||||||
:key="row.name"
|
size="sm"
|
||||||
v-slot="{ idx, column, item }"
|
@click="
|
||||||
:row="row"
|
(event) =>
|
||||||
>
|
emit('applyFilter', {
|
||||||
<div v-if="column.key === '_assign'" class="flex items-center">
|
event,
|
||||||
<MultipleAvatar
|
idx,
|
||||||
:avatars="item"
|
column,
|
||||||
size="sm"
|
item,
|
||||||
|
firstColumn: columns[0],
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ListRowItem v-else :item="item">
|
||||||
|
<template #prefix>
|
||||||
|
<div v-if="column.key === 'status'">
|
||||||
|
<IndicatorIcon :class="item.color" />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="column.key === 'lead_name'">
|
||||||
|
<Avatar
|
||||||
|
v-if="item.label"
|
||||||
|
class="flex items-center"
|
||||||
|
:image="item.image"
|
||||||
|
:label="item.image_label"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="column.key === 'organization'">
|
||||||
|
<Avatar
|
||||||
|
v-if="item.label"
|
||||||
|
class="flex items-center"
|
||||||
|
:image="item.logo"
|
||||||
|
:label="item.label"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="column.key === 'lead_owner'">
|
||||||
|
<Avatar
|
||||||
|
v-if="item.full_name"
|
||||||
|
class="flex items-center"
|
||||||
|
:image="item.user_image"
|
||||||
|
:label="item.full_name"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="column.key === 'mobile_no'">
|
||||||
|
<PhoneIcon class="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #default="{ label }">
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
[
|
||||||
|
'modified',
|
||||||
|
'creation',
|
||||||
|
'first_response_time',
|
||||||
|
'first_responded_on',
|
||||||
|
'response_by',
|
||||||
|
].includes(column.key)
|
||||||
|
"
|
||||||
|
class="truncate text-base"
|
||||||
@click="
|
@click="
|
||||||
(event) =>
|
(event) =>
|
||||||
emit('applyFilter', {
|
emit('applyFilter', {
|
||||||
@ -51,56 +104,37 @@
|
|||||||
firstColumn: columns[0],
|
firstColumn: columns[0],
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
/>
|
>
|
||||||
</div>
|
<Tooltip :text="item.label">
|
||||||
<ListRowItem v-else :item="item">
|
<div>{{ item.timeAgo }}</div>
|
||||||
<template #prefix>
|
</Tooltip>
|
||||||
<div v-if="column.key === 'status'">
|
</div>
|
||||||
<IndicatorIcon :class="item.color" />
|
<div v-else-if="column.key === '_liked_by'">
|
||||||
</div>
|
<Button
|
||||||
<div v-else-if="column.key === 'lead_name'">
|
v-if="column.key == '_liked_by'"
|
||||||
<Avatar
|
variant="ghosted"
|
||||||
v-if="item.label"
|
:class="isLiked(item) ? 'fill-red-500' : 'fill-white'"
|
||||||
class="flex items-center"
|
@click.stop.prevent="
|
||||||
:image="item.image"
|
() =>
|
||||||
:label="item.image_label"
|
emit('likeDoc', {
|
||||||
size="sm"
|
name: row.name,
|
||||||
/>
|
liked: isLiked(item),
|
||||||
</div>
|
})
|
||||||
<div v-else-if="column.key === 'organization'">
|
|
||||||
<Avatar
|
|
||||||
v-if="item.label"
|
|
||||||
class="flex items-center"
|
|
||||||
:image="item.logo"
|
|
||||||
:label="item.label"
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="column.key === 'lead_owner'">
|
|
||||||
<Avatar
|
|
||||||
v-if="item.full_name"
|
|
||||||
class="flex items-center"
|
|
||||||
:image="item.user_image"
|
|
||||||
:label="item.full_name"
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="column.key === 'mobile_no'">
|
|
||||||
<PhoneIcon class="h-4 w-4" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #default="{ label }">
|
|
||||||
<div
|
|
||||||
v-if="
|
|
||||||
[
|
|
||||||
'modified',
|
|
||||||
'creation',
|
|
||||||
'first_response_time',
|
|
||||||
'first_responded_on',
|
|
||||||
'response_by',
|
|
||||||
].includes(column.key)
|
|
||||||
"
|
"
|
||||||
class="truncate text-base"
|
>
|
||||||
|
<HeartIcon class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="column.key === 'sla_status'"
|
||||||
|
class="truncate text-base"
|
||||||
|
>
|
||||||
|
<Badge
|
||||||
|
v-if="item.value"
|
||||||
|
:variant="'subtle'"
|
||||||
|
:theme="item.color"
|
||||||
|
size="md"
|
||||||
|
:label="item.value"
|
||||||
@click="
|
@click="
|
||||||
(event) =>
|
(event) =>
|
||||||
emit('applyFilter', {
|
emit('applyFilter', {
|
||||||
@ -111,73 +145,34 @@
|
|||||||
firstColumn: columns[0],
|
firstColumn: columns[0],
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
>
|
/>
|
||||||
<Tooltip :text="item.label">
|
</div>
|
||||||
<div>{{ item.timeAgo }}</div>
|
<div v-else-if="column.type === 'Check'">
|
||||||
</Tooltip>
|
<FormControl
|
||||||
</div>
|
type="checkbox"
|
||||||
<div v-else-if="column.key === '_liked_by'">
|
:modelValue="item"
|
||||||
<Button
|
:disabled="true"
|
||||||
v-if="column.key == '_liked_by'"
|
class="text-gray-900"
|
||||||
variant="ghosted"
|
/>
|
||||||
:class="isLiked(item) ? 'fill-red-500' : 'fill-white'"
|
</div>
|
||||||
@click.stop.prevent="
|
<div
|
||||||
() =>
|
v-else
|
||||||
emit('likeDoc', { name: row.name, liked: isLiked(item) })
|
class="truncate text-base"
|
||||||
"
|
@click="
|
||||||
>
|
(event) =>
|
||||||
<HeartIcon class="h-4 w-4" />
|
emit('applyFilter', {
|
||||||
</Button>
|
event,
|
||||||
</div>
|
idx,
|
||||||
<div
|
column,
|
||||||
v-else-if="column.key === 'sla_status'"
|
item,
|
||||||
class="truncate text-base"
|
firstColumn: columns[0],
|
||||||
>
|
})
|
||||||
<Badge
|
"
|
||||||
v-if="item.value"
|
>
|
||||||
:variant="'subtle'"
|
{{ label }}
|
||||||
:theme="item.color"
|
</div>
|
||||||
size="md"
|
</template>
|
||||||
:label="item.value"
|
</ListRowItem>
|
||||||
@click="
|
|
||||||
(event) =>
|
|
||||||
emit('applyFilter', {
|
|
||||||
event,
|
|
||||||
idx,
|
|
||||||
column,
|
|
||||||
item,
|
|
||||||
firstColumn: columns[0],
|
|
||||||
})
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="column.type === 'Check'">
|
|
||||||
<FormControl
|
|
||||||
type="checkbox"
|
|
||||||
:modelValue="item"
|
|
||||||
:disabled="true"
|
|
||||||
class="text-gray-900"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="truncate text-base"
|
|
||||||
@click="
|
|
||||||
(event) =>
|
|
||||||
emit('applyFilter', {
|
|
||||||
event,
|
|
||||||
idx,
|
|
||||||
column,
|
|
||||||
item,
|
|
||||||
firstColumn: columns[0],
|
|
||||||
})
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{ label }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</ListRowItem>
|
|
||||||
</ListRow>
|
|
||||||
</ListRows>
|
</ListRows>
|
||||||
<ListSelectBanner>
|
<ListSelectBanner>
|
||||||
<template #actions="{ selections, unselectAll }">
|
<template #actions="{ selections, unselectAll }">
|
||||||
@ -191,7 +186,7 @@
|
|||||||
</ListView>
|
</ListView>
|
||||||
<ListFooter
|
<ListFooter
|
||||||
v-if="pageLengthCount"
|
v-if="pageLengthCount"
|
||||||
class="border-t px-5 py-2"
|
class="border-t sm:px-5 px-3 py-2"
|
||||||
v-model="pageLengthCount"
|
v-model="pageLengthCount"
|
||||||
:options="{
|
:options="{
|
||||||
rowCount: options.rowCount,
|
rowCount: options.rowCount,
|
||||||
@ -208,13 +203,12 @@ import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
|||||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||||
import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
||||||
import ListBulkActions from '@/components/ListBulkActions.vue'
|
import ListBulkActions from '@/components/ListBulkActions.vue'
|
||||||
|
import ListRows from '@/components/ListViews/ListRows.vue'
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
ListView,
|
ListView,
|
||||||
ListHeader,
|
ListHeader,
|
||||||
ListHeaderItem,
|
ListHeaderItem,
|
||||||
ListRows,
|
|
||||||
ListRow,
|
|
||||||
ListSelectBanner,
|
ListSelectBanner,
|
||||||
ListRowItem,
|
ListRowItem,
|
||||||
ListFooter,
|
ListFooter,
|
||||||
|
|||||||
66
frontend/src/components/ListViews/ListRows.vue
Normal file
66
frontend/src/components/ListViews/ListRows.vue
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mx-3 mt-2 h-full overflow-y-auto sm:mx-5" v-if="showGroupedRows">
|
||||||
|
<div v-for="group in reactivieRows" :key="group.group">
|
||||||
|
<ListGroupHeader :group="group">
|
||||||
|
<div
|
||||||
|
class="my-2 flex items-center gap-2 text-base font-medium text-gray-800"
|
||||||
|
>
|
||||||
|
<div>{{ __(group.label) }} -</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<component v-if="group.icon" :is="group.icon" />
|
||||||
|
<div v-if="group.group == ' '" class="text-gray-500">
|
||||||
|
{{ __('Empty') }}
|
||||||
|
</div>
|
||||||
|
<div v-else>{{ group.group }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ListGroupHeader>
|
||||||
|
<ListGroupRows :group="group" id="list-rows">
|
||||||
|
<ListRow
|
||||||
|
v-for="row in group.rows"
|
||||||
|
:key="row.name"
|
||||||
|
v-slot="{ idx, column, item }"
|
||||||
|
:row="row"
|
||||||
|
>
|
||||||
|
<slot v-bind="{ idx, column, item }" />
|
||||||
|
</ListRow>
|
||||||
|
</ListGroupRows>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ListRows class="mx-3 sm:mx-5" v-else id="list-rows">
|
||||||
|
<ListRow
|
||||||
|
v-for="row in reactivieRows"
|
||||||
|
:key="row.name"
|
||||||
|
v-slot="{ idx, column, item }"
|
||||||
|
:row="row"
|
||||||
|
>
|
||||||
|
<slot v-bind="{ idx, column, item }" />
|
||||||
|
</ListRow>
|
||||||
|
</ListRows>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ListRows, ListRow, ListGroupHeader, ListGroupRows } from 'frappe-ui'
|
||||||
|
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
rows: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const reactivieRows = ref(props.rows)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.rows,
|
||||||
|
(val) => (reactivieRows.value = val)
|
||||||
|
)
|
||||||
|
|
||||||
|
let showGroupedRows = computed(() => {
|
||||||
|
return props.rows.every(
|
||||||
|
(row) => row.group && row.rows && Array.isArray(row.rows)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@ -13,7 +13,7 @@
|
|||||||
}"
|
}"
|
||||||
row-key="name"
|
row-key="name"
|
||||||
>
|
>
|
||||||
<ListHeader class="mx-5" @columnWidthUpdated="emit('columnWidthUpdated')">
|
<ListHeader class="sm:mx-5 mx-3" @columnWidthUpdated="emit('columnWidthUpdated')">
|
||||||
<ListHeaderItem
|
<ListHeaderItem
|
||||||
v-for="column in columns"
|
v-for="column in columns"
|
||||||
:key="column.key"
|
:key="column.key"
|
||||||
@ -31,9 +31,8 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</ListHeaderItem>
|
</ListHeaderItem>
|
||||||
</ListHeader>
|
</ListHeader>
|
||||||
<ListRows id="list-rows">
|
<ListRows class="mx-3 sm:mx-5" id="list-rows">
|
||||||
<ListRow
|
<ListRow
|
||||||
class="mx-5"
|
|
||||||
v-for="row in rows"
|
v-for="row in rows"
|
||||||
:key="row.name"
|
:key="row.name"
|
||||||
v-slot="{ idx, column, item }"
|
v-slot="{ idx, column, item }"
|
||||||
@ -122,7 +121,7 @@
|
|||||||
</ListSelectBanner>
|
</ListSelectBanner>
|
||||||
</ListView>
|
</ListView>
|
||||||
<ListFooter
|
<ListFooter
|
||||||
class="border-t px-5 py-2"
|
class="border-t sm:px-5 px-3 py-2"
|
||||||
v-model="pageLengthCount"
|
v-model="pageLengthCount"
|
||||||
:options="{
|
:options="{
|
||||||
rowCount: options.rowCount,
|
rowCount: options.rowCount,
|
||||||
|
|||||||
@ -10,7 +10,10 @@
|
|||||||
}"
|
}"
|
||||||
row-key="name"
|
row-key="name"
|
||||||
>
|
>
|
||||||
<ListHeader class="mx-5" @columnWidthUpdated="emit('columnWidthUpdated')">
|
<ListHeader
|
||||||
|
class="mx-3 sm:mx-5"
|
||||||
|
@columnWidthUpdated="emit('columnWidthUpdated')"
|
||||||
|
>
|
||||||
<ListHeaderItem
|
<ListHeaderItem
|
||||||
v-for="column in columns"
|
v-for="column in columns"
|
||||||
:key="column.key"
|
:key="column.key"
|
||||||
@ -28,9 +31,8 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</ListHeaderItem>
|
</ListHeaderItem>
|
||||||
</ListHeader>
|
</ListHeader>
|
||||||
<ListRows id="list-rows">
|
<ListRows class="mx-3 sm:mx-5" id="list-rows">
|
||||||
<ListRow
|
<ListRow
|
||||||
class="mx-5"
|
|
||||||
v-for="row in rows"
|
v-for="row in rows"
|
||||||
:key="row.name"
|
:key="row.name"
|
||||||
v-slot="{ idx, column, item }"
|
v-slot="{ idx, column, item }"
|
||||||
@ -135,7 +137,7 @@
|
|||||||
</ListSelectBanner>
|
</ListSelectBanner>
|
||||||
</ListView>
|
</ListView>
|
||||||
<ListFooter
|
<ListFooter
|
||||||
class="border-t px-5 py-2"
|
class="border-t px-3 py-2 sm:px-5"
|
||||||
v-model="pageLengthCount"
|
v-model="pageLengthCount"
|
||||||
:options="{
|
:options="{
|
||||||
rowCount: options.rowCount,
|
rowCount: options.rowCount,
|
||||||
|
|||||||
@ -12,5 +12,5 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import CallUI from '@/components/CallUI.vue'
|
import CallUI from '@/components/CallUI.vue'
|
||||||
import { mobileSidebarOpened as sidebarOpened } from '@/stores/settings'
|
import { mobileSidebarOpened as sidebarOpened } from '@/composables/settings'
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -103,7 +103,7 @@ import SidebarLink from '@/components/SidebarLink.vue'
|
|||||||
import { viewsStore } from '@/stores/views'
|
import { viewsStore } from '@/stores/views'
|
||||||
import { notificationsStore } from '@/stores/notifications'
|
import { notificationsStore } from '@/stores/notifications'
|
||||||
import { computed, h } from 'vue'
|
import { computed, h } from 'vue'
|
||||||
import { mobileSidebarOpened as sidebarOpened } from '@/stores/settings'
|
import { mobileSidebarOpened as sidebarOpened } from '@/composables/settings'
|
||||||
|
|
||||||
const { getPinnedViews, getPublicViews } = viewsStore()
|
const { getPinnedViews, getPublicViews } = viewsStore()
|
||||||
|
|
||||||
@ -184,6 +184,7 @@ function parseView(views) {
|
|||||||
icon: getIcon(view.route_name, view.icon),
|
icon: getIcon(view.route_name, view.icon),
|
||||||
to: {
|
to: {
|
||||||
name: view.route_name,
|
name: view.route_name,
|
||||||
|
params: { viewType: view.type || 'list' },
|
||||||
query: { view: view.name },
|
query: { view: view.name },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -124,7 +124,7 @@ import { useRouter } from 'vue-router'
|
|||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
name: {
|
name: {
|
||||||
type: Object,
|
type: String,
|
||||||
default: {},
|
default: {},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@ -60,7 +60,7 @@
|
|||||||
type="textarea"
|
type="textarea"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
ref="content"
|
ref="content"
|
||||||
rows="10"
|
:rows="10"
|
||||||
v-model="_emailTemplate.response_html"
|
v-model="_emailTemplate.response_html"
|
||||||
:placeholder="
|
:placeholder="
|
||||||
__(
|
__(
|
||||||
|
|||||||
@ -75,6 +75,7 @@ const duplicateMode = ref(false)
|
|||||||
const _view = ref({
|
const _view = ref({
|
||||||
name: '',
|
name: '',
|
||||||
label: '',
|
label: '',
|
||||||
|
type: 'list',
|
||||||
icon: '',
|
icon: '',
|
||||||
filters: {},
|
filters: {},
|
||||||
order_by: 'modified desc',
|
order_by: 'modified desc',
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
:class="[field.hidden && 'hidden']"
|
:class="[field.hidden && 'hidden']"
|
||||||
class="flex items-center gap-2 px-3 leading-5 first:mt-3"
|
class="flex items-center gap-2 px-3 leading-5 first:mt-3"
|
||||||
>
|
>
|
||||||
<Tooltip :text="__(field.label)" hoverDelay="1">
|
<Tooltip :text="__(field.label)" :hoverDelay="1">
|
||||||
<div class="sm:w-[106px] w-36 shrink-0 truncate text-sm text-gray-600">
|
<div class="sm:w-[106px] w-36 shrink-0 truncate text-sm text-gray-600">
|
||||||
<span>{{ __(field.label) }}</span>
|
<span>{{ __(field.label) }}</span>
|
||||||
<span class="text-red-500">{{ field.reqd ? ' *' : '' }}</span>
|
<span class="text-red-500">{{ field.reqd ? ' *' : '' }}</span>
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</slot>
|
</slot>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip :text="label" placement="right" :disabled="isCollapsed" hoverDelay="1.5">
|
<Tooltip :text="label" placement="right" :disabled="isCollapsed" :hoverDelay="1.5">
|
||||||
<span
|
<span
|
||||||
class="flex-1 flex-shrink-0 truncate text-base duration-300 ease-in-out"
|
class="flex-1 flex-shrink-0 truncate text-base duration-300 ease-in-out"
|
||||||
:class="
|
:class="
|
||||||
@ -43,7 +43,7 @@
|
|||||||
import { Tooltip } from 'frappe-ui'
|
import { Tooltip } from 'frappe-ui'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { isMobileView, mobileSidebarOpened } from '@/stores/settings'
|
import { isMobileView, mobileSidebarOpened } from '@/composables/settings'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|||||||
@ -1,8 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<NestedPopover>
|
<Autocomplete
|
||||||
<template #target>
|
v-if="!sortValues?.size"
|
||||||
<Button :label="__('Sort')" ref="sortButtonRef">
|
:options="options"
|
||||||
<template #prefix><SortIcon class="h-4" /></template>
|
value=""
|
||||||
|
:placeholder="__('First Name')"
|
||||||
|
@change="(e) => setSort(e)"
|
||||||
|
>
|
||||||
|
<template #target="{ togglePopover }">
|
||||||
|
<Button :label="__('Sort')" @click="togglePopover()">
|
||||||
|
<template v-if="hideLabel">
|
||||||
|
<SortIcon class="h-4" />
|
||||||
|
</template>
|
||||||
|
<template v-if="!hideLabel && !sortValues?.size" #prefix>
|
||||||
|
<SortIcon class="h-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</Autocomplete>
|
||||||
|
<NestedPopover v-else>
|
||||||
|
<template #target="{ open }">
|
||||||
|
<Button v-if="sortValues.size > 1" :label="__('Sort')">
|
||||||
|
<template v-if="hideLabel">
|
||||||
|
<SortIcon class="h-4" />
|
||||||
|
</template>
|
||||||
|
<template v-if="!hideLabel" #prefix><SortIcon class="h-4" /></template>
|
||||||
<template v-if="sortValues?.size" #suffix>
|
<template v-if="sortValues?.size" #suffix>
|
||||||
<div
|
<div
|
||||||
class="flex h-5 w-5 items-center justify-center rounded bg-gray-900 pt-[1px] text-2xs font-medium text-white"
|
class="flex h-5 w-5 items-center justify-center rounded bg-gray-900 pt-[1px] text-2xs font-medium text-white"
|
||||||
@ -11,10 +32,43 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Button>
|
</Button>
|
||||||
|
<div v-else class="flex items-center justify-center">
|
||||||
|
<Button
|
||||||
|
v-if="sortValues.size"
|
||||||
|
class="rounded-r-none border-r"
|
||||||
|
@click.stop="
|
||||||
|
() => {
|
||||||
|
Array.from(sortValues)[0].direction =
|
||||||
|
Array.from(sortValues)[0].direction == 'asc' ? 'desc' : 'asc'
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<AscendingIcon
|
||||||
|
v-if="Array.from(sortValues)[0].direction == 'asc'"
|
||||||
|
class="h-4"
|
||||||
|
/>
|
||||||
|
<DesendingIcon v-else class="h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
:label="getSortLabel()"
|
||||||
|
:class="sortValues.size ? 'rounded-l-none' : ''"
|
||||||
|
>
|
||||||
|
<template v-if="!hideLabel && !sortValues?.size" #prefix>
|
||||||
|
<SortIcon class="h-4" />
|
||||||
|
</template>
|
||||||
|
<template v-if="sortValues?.size" #suffix>
|
||||||
|
<FeatherIcon
|
||||||
|
:name="open ? 'chevron-up' : 'chevron-down'"
|
||||||
|
class="h-4 text-gray-600"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #body="{ close }">
|
<template #body="{ close }">
|
||||||
<div class="my-2 rounded-lg border border-gray-100 bg-white shadow-xl">
|
<div class="my-2 rounded-lg border border-gray-100 bg-white shadow-xl">
|
||||||
<div class="min-w-[352px] p-2">
|
<div class="min-w-60 p-2">
|
||||||
<div
|
<div
|
||||||
v-if="sortValues?.size"
|
v-if="sortValues?.size"
|
||||||
id="sort-list"
|
id="sort-list"
|
||||||
@ -23,34 +77,51 @@
|
|||||||
<div
|
<div
|
||||||
v-for="(sort, i) in sortValues"
|
v-for="(sort, i) in sortValues"
|
||||||
:key="sort.fieldname"
|
:key="sort.fieldname"
|
||||||
class="flex items-center gap-2"
|
class="flex items-center gap-1"
|
||||||
>
|
>
|
||||||
<div class="handle flex h-7 w-7 items-center justify-center">
|
<div class="handle flex h-7 w-7 items-center justify-center">
|
||||||
<DragIcon class="h-4 w-4 cursor-grab text-gray-600" />
|
<DragIcon class="h-4 w-4 cursor-grab text-gray-600" />
|
||||||
</div>
|
</div>
|
||||||
<Autocomplete
|
<div class="flex">
|
||||||
class="!w-32"
|
<Button
|
||||||
:value="sort.fieldname"
|
size="md"
|
||||||
:options="sortOptions.data"
|
class="rounded-r-none border-r"
|
||||||
@change="(e) => updateSort(e, i)"
|
@click="
|
||||||
:placeholder="__('First Name')"
|
() => {
|
||||||
/>
|
sort.direction = sort.direction == 'asc' ? 'desc' : 'asc'
|
||||||
<FormControl
|
apply()
|
||||||
class="!w-32"
|
}
|
||||||
type="select"
|
"
|
||||||
v-model="sort.direction"
|
>
|
||||||
:options="[
|
<AscendingIcon v-if="sort.direction == 'asc'" class="h-4" />
|
||||||
{ label: __('Ascending'), value: 'asc' },
|
<DesendingIcon v-else class="h-4" />
|
||||||
{ label: __('Descending'), value: 'desc' },
|
</Button>
|
||||||
]"
|
<Autocomplete
|
||||||
@change="
|
class="!w-32"
|
||||||
(e) => {
|
:value="sort.fieldname"
|
||||||
sort.direction = e.target.value
|
:options="sortOptions.data"
|
||||||
apply()
|
@change="(e) => updateSort(e, i)"
|
||||||
}
|
:placeholder="__('First Name')"
|
||||||
"
|
>
|
||||||
:placeholder="__('Ascending')"
|
<template
|
||||||
/>
|
#target="{ togglePopover, selectedValue, displayValue }"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
class="flex w-full items-center justify-between rounded-l-none !text-gray-600"
|
||||||
|
size="md"
|
||||||
|
@click="togglePopover()"
|
||||||
|
>
|
||||||
|
{{ displayValue(selectedValue) }}
|
||||||
|
<template #suffix>
|
||||||
|
<FeatherIcon
|
||||||
|
name="chevron-down"
|
||||||
|
class="h-4 text-gray-600"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</Autocomplete>
|
||||||
|
</div>
|
||||||
<Button variant="ghost" icon="x" @click="removeSort(i)" />
|
<Button variant="ghost" icon="x" @click="removeSort(i)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -95,26 +166,30 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import AscendingIcon from '@/components/Icons/AscendingIcon.vue'
|
||||||
|
import DesendingIcon from '@/components/Icons/DesendingIcon.vue'
|
||||||
import NestedPopover from '@/components/NestedPopover.vue'
|
import NestedPopover from '@/components/NestedPopover.vue'
|
||||||
import SortIcon from '@/components/Icons/SortIcon.vue'
|
import SortIcon from '@/components/Icons/SortIcon.vue'
|
||||||
import DragIcon from '@/components/Icons/DragIcon.vue'
|
import DragIcon from '@/components/Icons/DragIcon.vue'
|
||||||
import { useSortable } from '@vueuse/integrations/useSortable'
|
|
||||||
import Autocomplete from '@/components/frappe-ui/Autocomplete.vue'
|
import Autocomplete from '@/components/frappe-ui/Autocomplete.vue'
|
||||||
|
import { useSortable } from '@vueuse/integrations/useSortable'
|
||||||
import { createResource } from 'frappe-ui'
|
import { createResource } from 'frappe-ui'
|
||||||
import { computed, ref, nextTick, onMounted } from 'vue'
|
import { computed, nextTick, onMounted } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
doctype: {
|
doctype: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
hideLabel: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update'])
|
const emit = defineEmits(['update'])
|
||||||
const list = defineModel()
|
const list = defineModel()
|
||||||
|
|
||||||
const sortButtonRef = ref(null)
|
|
||||||
|
|
||||||
const sortOptions = createResource({
|
const sortOptions = createResource({
|
||||||
url: 'crm.api.doc.sort_options',
|
url: 'crm.api.doc.sort_options',
|
||||||
cache: ['sortOptions', props.doctype],
|
cache: ['sortOptions', props.doctype],
|
||||||
@ -161,6 +236,16 @@ const sortSortable = useSortable('#sort-list', sortValues, {
|
|||||||
onEnd: () => apply(),
|
onEnd: () => apply(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function getSortLabel() {
|
||||||
|
if (!sortValues.value.size) return __('Sort')
|
||||||
|
let values = Array.from(sortValues.value)
|
||||||
|
let label = sortOptions.data?.find(
|
||||||
|
(option) => option.value === values[0].fieldname
|
||||||
|
)?.label
|
||||||
|
|
||||||
|
return label || sort.fieldname
|
||||||
|
}
|
||||||
|
|
||||||
function setSort(data) {
|
function setSort(data) {
|
||||||
sortValues.value.add({ fieldname: data.value, direction: 'asc' })
|
sortValues.value.add({ fieldname: data.value, direction: 'asc' })
|
||||||
restartSort()
|
restartSort()
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="isMobileView"
|
v-if="isMobileView"
|
||||||
class="flex flex-col justify-between gap-2 px-5 py-4"
|
class="flex flex-col justify-between gap-2 sm:px-5 px-3 py-4"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2 overflow-x-auto">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Dropdown :options="viewsDropdownOptions">
|
<Dropdown :options="viewsDropdownOptions">
|
||||||
<template #default="{ open }">
|
<template #default="{ open }">
|
||||||
@ -12,7 +12,12 @@
|
|||||||
<div v-if="isEmoji(currentView.icon)">
|
<div v-if="isEmoji(currentView.icon)">
|
||||||
{{ currentView.icon }}
|
{{ currentView.icon }}
|
||||||
</div>
|
</div>
|
||||||
<FeatherIcon v-else :name="currentView.icon" class="h-4" />
|
<FeatherIcon
|
||||||
|
v-else-if="typeof currentView.icon == 'string'"
|
||||||
|
:name="currentView.icon"
|
||||||
|
class="h-4"
|
||||||
|
/>
|
||||||
|
<component v-else :is="currentView.icon" class="h-4" />
|
||||||
</template>
|
</template>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
<FeatherIcon
|
<FeatherIcon
|
||||||
@ -36,19 +41,35 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2 overflow-x-auto">
|
||||||
<Filter
|
|
||||||
v-model="list"
|
|
||||||
:doctype="doctype"
|
|
||||||
:default_filters="filters"
|
|
||||||
@update="updateFilter"
|
|
||||||
/>
|
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<SortBy v-model="list" :doctype="doctype" @update="updateSort" />
|
<Filter
|
||||||
|
v-model="list"
|
||||||
|
:doctype="doctype"
|
||||||
|
:default_filters="filters"
|
||||||
|
@update="updateFilter"
|
||||||
|
/>
|
||||||
|
<GroupBy
|
||||||
|
v-if="route.params.viewType === 'group_by'"
|
||||||
|
v-model="list"
|
||||||
|
:doctype="doctype"
|
||||||
|
:hideLabel="isMobileView"
|
||||||
|
@update="updateGroupBy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<SortBy
|
||||||
|
v-model="list"
|
||||||
|
:doctype="doctype"
|
||||||
|
@update="updateSort"
|
||||||
|
:hideLabel="isMobileView"
|
||||||
|
/>
|
||||||
<ColumnSettings
|
<ColumnSettings
|
||||||
v-if="!options.hideColumnsButton"
|
v-if="!options.hideColumnsButton"
|
||||||
v-model="list"
|
v-model="list"
|
||||||
:doctype="doctype"
|
:doctype="doctype"
|
||||||
|
:hideLabel="isMobileView"
|
||||||
@update="(isDefault) => updateColumns(isDefault)"
|
@update="(isDefault) => updateColumns(isDefault)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -69,7 +90,12 @@
|
|||||||
<Button :label="__(currentView.label)">
|
<Button :label="__(currentView.label)">
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<div v-if="isEmoji(currentView.icon)">{{ currentView.icon }}</div>
|
<div v-if="isEmoji(currentView.icon)">{{ currentView.icon }}</div>
|
||||||
<FeatherIcon v-else :name="currentView.icon" class="h-4" />
|
<FeatherIcon
|
||||||
|
v-else-if="typeof currentView.icon == 'string'"
|
||||||
|
:name="currentView.icon"
|
||||||
|
class="h-4"
|
||||||
|
/>
|
||||||
|
<component v-else :is="currentView.icon" class="h-4" />
|
||||||
</template>
|
</template>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
<FeatherIcon
|
<FeatherIcon
|
||||||
@ -117,6 +143,12 @@
|
|||||||
<RefreshIcon class="h-4 w-4" />
|
<RefreshIcon class="h-4 w-4" />
|
||||||
</template>
|
</template>
|
||||||
</Button>
|
</Button>
|
||||||
|
<GroupBy
|
||||||
|
v-if="route.params.viewType === 'group_by'"
|
||||||
|
v-model="list"
|
||||||
|
:doctype="doctype"
|
||||||
|
@update="updateGroupBy"
|
||||||
|
/>
|
||||||
<Filter
|
<Filter
|
||||||
v-model="list"
|
v-model="list"
|
||||||
:doctype="doctype"
|
:doctype="doctype"
|
||||||
@ -162,7 +194,11 @@
|
|||||||
afterCreate: async (v) => {
|
afterCreate: async (v) => {
|
||||||
await reloadView()
|
await reloadView()
|
||||||
viewUpdated = false
|
viewUpdated = false
|
||||||
router.push({ name: route.name, query: { view: v.name } })
|
router.push({
|
||||||
|
name: route.name,
|
||||||
|
params: { viewType: v.type || 'list' },
|
||||||
|
query: { view: v.name },
|
||||||
|
})
|
||||||
},
|
},
|
||||||
afterUpdate: () => {
|
afterUpdate: () => {
|
||||||
viewUpdated = false
|
viewUpdated = false
|
||||||
@ -212,6 +248,7 @@
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import DetailsIcon from '@/components/Icons/DetailsIcon.vue'
|
||||||
import QuickFilterField from '@/components/QuickFilterField.vue'
|
import QuickFilterField from '@/components/QuickFilterField.vue'
|
||||||
import RefreshIcon from '@/components/Icons/RefreshIcon.vue'
|
import RefreshIcon from '@/components/Icons/RefreshIcon.vue'
|
||||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||||
@ -221,6 +258,7 @@ import UnpinIcon from '@/components/Icons/UnpinIcon.vue'
|
|||||||
import ViewModal from '@/components/Modals/ViewModal.vue'
|
import ViewModal from '@/components/Modals/ViewModal.vue'
|
||||||
import SortBy from '@/components/SortBy.vue'
|
import SortBy from '@/components/SortBy.vue'
|
||||||
import Filter from '@/components/Filter.vue'
|
import Filter from '@/components/Filter.vue'
|
||||||
|
import GroupBy from '@/components/GroupBy.vue'
|
||||||
import FadedScrollableDiv from '@/components/FadedScrollableDiv.vue'
|
import FadedScrollableDiv from '@/components/FadedScrollableDiv.vue'
|
||||||
import ColumnSettings from '@/components/ColumnSettings.vue'
|
import ColumnSettings from '@/components/ColumnSettings.vue'
|
||||||
import { globalStore } from '@/stores/global'
|
import { globalStore } from '@/stores/global'
|
||||||
@ -228,10 +266,11 @@ import { viewsStore } from '@/stores/views'
|
|||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { isEmoji } from '@/utils'
|
import { isEmoji } from '@/utils'
|
||||||
import { createResource, Dropdown, call, FeatherIcon } from 'frappe-ui'
|
import { createResource, Dropdown, call, FeatherIcon } from 'frappe-ui'
|
||||||
import { computed, ref, onMounted, watch, h } from 'vue'
|
import { computed, ref, onMounted, watch, h, markRaw } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { useDebounceFn } from '@vueuse/core'
|
import { useDebounceFn } from '@vueuse/core'
|
||||||
import { isMobileView } from '@/stores/settings'
|
import { isMobileView } from '@/composables/settings'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
doctype: {
|
doctype: {
|
||||||
@ -247,6 +286,7 @@ const props = defineProps({
|
|||||||
default: {
|
default: {
|
||||||
hideColumnsButton: false,
|
hideColumnsButton: false,
|
||||||
defaultViewName: '',
|
defaultViewName: '',
|
||||||
|
allowedViews: ['list'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -268,17 +308,35 @@ const defaultParams = ref('')
|
|||||||
const viewUpdated = ref(false)
|
const viewUpdated = ref(false)
|
||||||
const showViewModal = ref(false)
|
const showViewModal = ref(false)
|
||||||
|
|
||||||
|
function getViewType() {
|
||||||
|
let viewType = route.params.viewType || 'list'
|
||||||
|
let types = {
|
||||||
|
list: {
|
||||||
|
label: __('List View'),
|
||||||
|
icon: 'list',
|
||||||
|
},
|
||||||
|
group_by: {
|
||||||
|
label: __('Group By View'),
|
||||||
|
icon: markRaw(DetailsIcon),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return types[viewType]
|
||||||
|
}
|
||||||
|
|
||||||
const currentView = computed(() => {
|
const currentView = computed(() => {
|
||||||
let _view = getView(route.query.view)
|
let _view = getView(route.query.view, route.params.viewType, props.doctype)
|
||||||
return {
|
return {
|
||||||
label: _view?.label || props.options?.defaultViewName || 'List View',
|
label:
|
||||||
icon: _view?.icon || 'list',
|
_view?.label || props.options?.defaultViewName || getViewType().label,
|
||||||
|
icon: _view?.icon || getViewType().icon,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const view = ref({
|
const view = ref({
|
||||||
name: '',
|
name: '',
|
||||||
label: '',
|
label: '',
|
||||||
|
type: 'list',
|
||||||
icon: '',
|
icon: '',
|
||||||
filters: {},
|
filters: {},
|
||||||
order_by: 'modified desc',
|
order_by: 'modified desc',
|
||||||
@ -308,7 +366,7 @@ watch(updatedPageCount, (value) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function getParams() {
|
function getParams() {
|
||||||
let _view = getView(route.query.view, props.doctype)
|
let _view = getView(route.query.view, route.params.viewType, props.doctype)
|
||||||
const filters = (_view?.filters && JSON.parse(_view.filters)) || {}
|
const filters = (_view?.filters && JSON.parse(_view.filters)) || {}
|
||||||
const order_by = _view?.order_by || 'modified desc'
|
const order_by = _view?.order_by || 'modified desc'
|
||||||
const columns = _view?.columns || ''
|
const columns = _view?.columns || ''
|
||||||
@ -318,9 +376,11 @@ function getParams() {
|
|||||||
view.value = {
|
view.value = {
|
||||||
name: _view.name,
|
name: _view.name,
|
||||||
label: _view.label,
|
label: _view.label,
|
||||||
|
type: _view.type || 'list',
|
||||||
icon: _view.icon,
|
icon: _view.icon,
|
||||||
filters: _view.filters,
|
filters: _view.filters,
|
||||||
order_by: _view.order_by,
|
order_by: _view.order_by,
|
||||||
|
group_by_field: _view.group_by_field,
|
||||||
columns: _view.columns,
|
columns: _view.columns,
|
||||||
rows: _view.rows,
|
rows: _view.rows,
|
||||||
route_name: _view.route_name,
|
route_name: _view.route_name,
|
||||||
@ -331,13 +391,15 @@ function getParams() {
|
|||||||
} else {
|
} else {
|
||||||
view.value = {
|
view.value = {
|
||||||
name: '',
|
name: '',
|
||||||
label: '',
|
label: getViewType().label,
|
||||||
|
type: route.params.viewType || 'list',
|
||||||
icon: '',
|
icon: '',
|
||||||
filters: {},
|
filters: {},
|
||||||
order_by: 'modified desc',
|
order_by: 'modified desc',
|
||||||
|
group_by_field: 'owner',
|
||||||
columns: '',
|
columns: '',
|
||||||
rows: '',
|
rows: '',
|
||||||
route_name: '',
|
route_name: route.name,
|
||||||
load_default_columns: true,
|
load_default_columns: true,
|
||||||
pinned: false,
|
pinned: false,
|
||||||
public: false,
|
public: false,
|
||||||
@ -352,7 +414,11 @@ function getParams() {
|
|||||||
rows: rows,
|
rows: rows,
|
||||||
page_length: pageLength.value,
|
page_length: pageLength.value,
|
||||||
page_length_count: pageLengthCount.value,
|
page_length_count: pageLengthCount.value,
|
||||||
custom_view_name: _view?.name || '',
|
view: {
|
||||||
|
custom_view_name: _view?.name || '',
|
||||||
|
view_type: _view?.type || route.params.viewType || 'list',
|
||||||
|
group_by_field: _view?.group_by_field || 'owner',
|
||||||
|
},
|
||||||
default_filters: props.filters,
|
default_filters: props.filters,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -360,15 +426,9 @@ function getParams() {
|
|||||||
list.value = createResource({
|
list.value = createResource({
|
||||||
url: 'crm.api.doc.get_list_data',
|
url: 'crm.api.doc.get_list_data',
|
||||||
params: getParams(),
|
params: getParams(),
|
||||||
cache: [props.doctype, route.query.view],
|
cache: [props.doctype, route.query.view, route.params.viewType],
|
||||||
transform(data) {
|
|
||||||
return {
|
|
||||||
...data,
|
|
||||||
params: getParams(),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
let cv = getView(route.query.view)
|
let cv = getView(route.query.view, route.params.viewType, props.doctype)
|
||||||
let params = list.value.params ? list.value.params : getParams()
|
let params = list.value.params ? list.value.params : getParams()
|
||||||
defaultParams.value = {
|
defaultParams.value = {
|
||||||
doctype: props.doctype,
|
doctype: props.doctype,
|
||||||
@ -378,7 +438,11 @@ list.value = createResource({
|
|||||||
page_length_count: params.page_length_count,
|
page_length_count: params.page_length_count,
|
||||||
columns: data.columns,
|
columns: data.columns,
|
||||||
rows: data.rows,
|
rows: data.rows,
|
||||||
custom_view_name: cv?.name || '',
|
view: {
|
||||||
|
custom_view_name: cv?.name || '',
|
||||||
|
view_type: cv?.type || route.params.viewType || 'list',
|
||||||
|
group_by_field: params?.view?.group_by_field || 'owner',
|
||||||
|
},
|
||||||
default_filters: props.filters,
|
default_filters: props.filters,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -412,23 +476,37 @@ async function exportRows() {
|
|||||||
export_type.value = 'Excel'
|
export_type.value = 'Excel'
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultViews = [
|
let defaultViews = []
|
||||||
{
|
let allowedViews = props.options.allowedViews || ['list']
|
||||||
|
|
||||||
|
if (allowedViews.includes('list')) {
|
||||||
|
defaultViews.push({
|
||||||
label: __(props.options?.defaultViewName) || __('List View'),
|
label: __(props.options?.defaultViewName) || __('List View'),
|
||||||
icon: 'list',
|
icon: 'list',
|
||||||
onClick() {
|
onClick() {
|
||||||
viewUpdated.value = false
|
viewUpdated.value = false
|
||||||
router.push({ name: route.name })
|
router.push({ name: route.name })
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
]
|
}
|
||||||
|
if (allowedViews.includes('group_by')) {
|
||||||
|
defaultViews.push({
|
||||||
|
label: __(props.options?.defaultViewName) || __('Group By View'),
|
||||||
|
icon: markRaw(DetailsIcon),
|
||||||
|
onClick() {
|
||||||
|
viewUpdated.value = false
|
||||||
|
router.push({ name: route.name, params: { viewType: 'group_by' } })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function getIcon(icon) {
|
function getIcon(icon, type) {
|
||||||
if (isEmoji(icon)) {
|
if (isEmoji(icon)) {
|
||||||
return h('div', icon)
|
return h('div', icon)
|
||||||
} else {
|
} else if (!icon && type === 'group_by') {
|
||||||
return icon || 'list'
|
return markRaw(DetailsIcon)
|
||||||
}
|
}
|
||||||
|
return icon || 'list'
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewsDropdownOptions = computed(() => {
|
const viewsDropdownOptions = computed(() => {
|
||||||
@ -443,14 +521,19 @@ const viewsDropdownOptions = computed(() => {
|
|||||||
if (list.value?.data?.views) {
|
if (list.value?.data?.views) {
|
||||||
list.value.data.views.forEach((view) => {
|
list.value.data.views.forEach((view) => {
|
||||||
view.label = __(view.label)
|
view.label = __(view.label)
|
||||||
view.icon = getIcon(view.icon)
|
view.type = view.type || 'list'
|
||||||
|
view.icon = getIcon(view.icon, view.type)
|
||||||
view.filters =
|
view.filters =
|
||||||
typeof view.filters == 'string'
|
typeof view.filters == 'string'
|
||||||
? JSON.parse(view.filters)
|
? JSON.parse(view.filters)
|
||||||
: view.filters
|
: view.filters
|
||||||
view.onClick = () => {
|
view.onClick = () => {
|
||||||
viewUpdated.value = false
|
viewUpdated.value = false
|
||||||
router.push({ ...route, query: { view: view.name } })
|
router.push({
|
||||||
|
name: route.name,
|
||||||
|
params: { viewType: view.type },
|
||||||
|
query: { view: view.name },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
let publicViews = list.value.data.views.filter((v) => v.public)
|
let publicViews = list.value.data.views.filter((v) => v.public)
|
||||||
@ -491,7 +574,15 @@ const quickFilterList = computed(() => {
|
|||||||
if (list.value.params?.filters[filter.name]) {
|
if (list.value.params?.filters[filter.name]) {
|
||||||
let value = list.value.params.filters[filter.name]
|
let value = list.value.params.filters[filter.name]
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
filter['value'] = value[1].replace(/%/g, '')
|
if (
|
||||||
|
(['Check', 'Select', 'Link', 'Date', 'Datetime'].includes(
|
||||||
|
filter.type
|
||||||
|
) &&
|
||||||
|
value[0]?.toLowerCase() == 'like') ||
|
||||||
|
value[0]?.toLowerCase() != 'like'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
filter['value'] = value[1]?.replace(/%/g, '')
|
||||||
} else {
|
} else {
|
||||||
filter['value'] = value.replace(/%/g, '')
|
filter['value'] = value.replace(/%/g, '')
|
||||||
}
|
}
|
||||||
@ -555,6 +646,21 @@ function updateSort(order_by) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateGroupBy(group_by_field) {
|
||||||
|
viewUpdated.value = true
|
||||||
|
if (!defaultParams.value) {
|
||||||
|
defaultParams.value = getParams()
|
||||||
|
}
|
||||||
|
list.value.params = defaultParams.value
|
||||||
|
list.value.params.view.group_by_field = group_by_field
|
||||||
|
view.value.group_by_field = group_by_field
|
||||||
|
list.value.reload()
|
||||||
|
|
||||||
|
if (!route.query.view) {
|
||||||
|
create_or_update_default_view()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateColumns(obj) {
|
function updateColumns(obj) {
|
||||||
if (!obj) {
|
if (!obj) {
|
||||||
obj = {
|
obj = {
|
||||||
@ -601,10 +707,12 @@ function create_or_update_default_view() {
|
|||||||
reloadView()
|
reloadView()
|
||||||
view.value = {
|
view.value = {
|
||||||
label: view.value.label,
|
label: view.value.label,
|
||||||
|
type: view.value.type || 'list',
|
||||||
icon: view.value.icon,
|
icon: view.value.icon,
|
||||||
name: view.value.name,
|
name: view.value.name,
|
||||||
filters: defaultParams.value.filters,
|
filters: defaultParams.value.filters,
|
||||||
order_by: defaultParams.value.order_by,
|
order_by: defaultParams.value.order_by,
|
||||||
|
group_by_field: defaultParams.value.view.group_by_field,
|
||||||
columns: defaultParams.value.columns,
|
columns: defaultParams.value.columns,
|
||||||
rows: defaultParams.value.rows,
|
rows: defaultParams.value.rows,
|
||||||
route_name: route.name,
|
route_name: route.name,
|
||||||
@ -708,7 +816,10 @@ const viewActions = computed(() => {
|
|||||||
const viewModalObj = ref({})
|
const viewModalObj = ref({})
|
||||||
|
|
||||||
function duplicateView() {
|
function duplicateView() {
|
||||||
let label = __(getView(route.query.view)?.label) || __('List View')
|
let label =
|
||||||
|
__(
|
||||||
|
getView(route.query.view, route.params.viewType, props.doctype)?.label
|
||||||
|
) || getViewType().label
|
||||||
view.value.name = ''
|
view.value.name = ''
|
||||||
view.value.label = label + __(' (New)')
|
view.value.label = label + __(' (New)')
|
||||||
viewModalObj.value = view.value
|
viewModalObj.value = view.value
|
||||||
@ -716,9 +827,9 @@ function duplicateView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function editView() {
|
function editView() {
|
||||||
let cView = getView(route.query.view)
|
let cView = getView(route.query.view, route.params.viewType, props.doctype)
|
||||||
view.value.name = route.query.view
|
view.value.name = route.query.view
|
||||||
view.value.label = __(cView?.label) || __('List View')
|
view.value.label = __(cView?.label) || getViewType().label
|
||||||
view.value.icon = cView?.icon || ''
|
view.value.icon = cView?.icon || ''
|
||||||
viewModalObj.value = view.value
|
viewModalObj.value = view.value
|
||||||
showViewModal.value = true
|
showViewModal.value = true
|
||||||
@ -762,10 +873,12 @@ function cancelChanges() {
|
|||||||
function saveView() {
|
function saveView() {
|
||||||
view.value = {
|
view.value = {
|
||||||
label: view.value.label,
|
label: view.value.label,
|
||||||
|
type: view.value.type || 'list',
|
||||||
icon: view.value.icon,
|
icon: view.value.icon,
|
||||||
name: view.value.name,
|
name: view.value.name,
|
||||||
filters: defaultParams.value.filters,
|
filters: defaultParams.value.filters,
|
||||||
order_by: defaultParams.value.order_by,
|
order_by: defaultParams.value.order_by,
|
||||||
|
group_by_field: defaultParams.value.view.group_by_field,
|
||||||
columns: defaultParams.value.columns,
|
columns: defaultParams.value.columns,
|
||||||
rows: defaultParams.value.rows,
|
rows: defaultParams.value.rows,
|
||||||
route_name: route.name,
|
route_name: route.name,
|
||||||
@ -830,19 +943,16 @@ defineExpose({ applyFilter, applyLikeFilter, likeDoc })
|
|||||||
|
|
||||||
// Watchers
|
// Watchers
|
||||||
watch(
|
watch(
|
||||||
() => getView(route.query.view),
|
() => getView(route.query.view, route.params.viewType, props.doctype),
|
||||||
(value, old_value) => {
|
(value, old_value) => {
|
||||||
if (JSON.stringify(value) === JSON.stringify(old_value)) return
|
if (_.isEqual(value, old_value)) return
|
||||||
reload()
|
reload()
|
||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
watch([() => route, () => route.params.viewType], (value, old_value) => {
|
||||||
() => route,
|
if (value[0] === old_value[0] && value[1] === value[0]) return
|
||||||
(value, old_value) => {
|
reload()
|
||||||
if (value === old_value) return
|
})
|
||||||
reload()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="reply?.message"
|
v-if="reply?.message"
|
||||||
class="flex items-center justify-around gap-2 sm:px-10 px-4 pt-2"
|
class="flex items-center justify-around gap-2 px-4 pt-2 sm:px-10"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mb-1 ml-13 flex-1 cursor-pointer rounded border-0 border-l-4 border-green-500 bg-gray-100 p-2 text-base text-gray-600"
|
class="mb-1 ml-13 flex-1 cursor-pointer rounded border-0 border-l-4 border-green-500 bg-gray-100 p-2 text-base text-gray-600"
|
||||||
@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
<Button variant="ghost" icon="x" @click="reply = {}" />
|
<Button variant="ghost" icon="x" @click="reply = {}" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-end gap-2 sm:px-10 px-4 py-2.5">
|
<div class="flex items-end gap-2 px-4 py-2.5 sm:px-10" v-bind="$attrs">
|
||||||
<div class="flex h-8 items-center gap-2">
|
<div class="flex h-8 items-center gap-2">
|
||||||
<FileUploader @success="(file) => uploadFile(file)">
|
<FileUploader @success="(file) => uploadFile(file)">
|
||||||
<template v-slot="{ openFileSelector }">
|
<template v-slot="{ openFileSelector }">
|
||||||
|
|||||||
@ -2,7 +2,16 @@
|
|||||||
<Combobox v-model="selectedValue" nullable v-slot="{ open: isComboboxOpen }">
|
<Combobox v-model="selectedValue" nullable v-slot="{ open: isComboboxOpen }">
|
||||||
<Popover class="w-full" v-model:show="showOptions">
|
<Popover class="w-full" v-model:show="showOptions">
|
||||||
<template #target="{ open: openPopover, togglePopover }">
|
<template #target="{ open: openPopover, togglePopover }">
|
||||||
<slot name="target" v-bind="{ open: openPopover, togglePopover }">
|
<slot
|
||||||
|
name="target"
|
||||||
|
v-bind="{
|
||||||
|
open: openPopover,
|
||||||
|
togglePopover,
|
||||||
|
isOpen: showOptions,
|
||||||
|
selectedValue,
|
||||||
|
displayValue,
|
||||||
|
}"
|
||||||
|
>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<button
|
<button
|
||||||
class="flex w-full items-center justify-between focus:outline-none"
|
class="flex w-full items-center justify-between focus:outline-none"
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
import { onMounted, onUnmounted, reactive } from 'vue'
|
|
||||||
|
|
||||||
export function useScreenSize() {
|
|
||||||
const size = reactive({
|
|
||||||
width: window.innerWidth,
|
|
||||||
height: window.innerHeight,
|
|
||||||
})
|
|
||||||
|
|
||||||
const onResize = () => {
|
|
||||||
size.width = window.innerWidth
|
|
||||||
size.height = window.innerHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
window.addEventListener('resize', onResize)
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('resize', onResize)
|
|
||||||
})
|
|
||||||
|
|
||||||
return size
|
|
||||||
}
|
|
||||||
@ -1,5 +1,4 @@
|
|||||||
import { createResource } from 'frappe-ui'
|
import { createResource } from 'frappe-ui'
|
||||||
import { useScreenSize } from '@/composables'
|
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
export const whatsappEnabled = ref(false)
|
export const whatsappEnabled = ref(false)
|
||||||
@ -23,5 +22,4 @@ createResource({
|
|||||||
|
|
||||||
export const mobileSidebarOpened = ref(false)
|
export const mobileSidebarOpened = ref(false)
|
||||||
|
|
||||||
const screenSize = useScreenSize()
|
export const isMobileView = computed(() => window.innerWidth < 768)
|
||||||
export const isMobileView = computed(() => screenSize.width < 768)
|
|
||||||
@ -50,11 +50,7 @@
|
|||||||
<span>{{ __('No {0} Found', [__('Logs')]) }}</span>
|
<span>{{ __('No {0} Found', [__('Logs')]) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CallLogModal
|
<CallLogModal v-model="showCallLogModal" :name="selectedCallLog" />
|
||||||
v-model="showCallLogModal"
|
|
||||||
v-model:reloadCallLogs="callLogs"
|
|
||||||
:name="selectedCallLog"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|||||||
@ -238,7 +238,7 @@ import { globalStore } from '@/stores/global.js'
|
|||||||
import { usersStore } from '@/stores/users.js'
|
import { usersStore } from '@/stores/users.js'
|
||||||
import { organizationsStore } from '@/stores/organizations.js'
|
import { organizationsStore } from '@/stores/organizations.js'
|
||||||
import { statusesStore } from '@/stores/statuses'
|
import { statusesStore } from '@/stores/statuses'
|
||||||
import { callEnabled } from '@/stores/settings'
|
import { callEnabled } from '@/composables/settings'
|
||||||
import { ref, computed, h } from 'vue'
|
import { ref, computed, h } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
|||||||
@ -321,7 +321,7 @@ import {
|
|||||||
import { globalStore } from '@/stores/global'
|
import { globalStore } from '@/stores/global'
|
||||||
import { organizationsStore } from '@/stores/organizations'
|
import { organizationsStore } from '@/stores/organizations'
|
||||||
import { statusesStore } from '@/stores/statuses'
|
import { statusesStore } from '@/stores/statuses'
|
||||||
import { whatsappEnabled, callEnabled } from '@/stores/settings'
|
import { whatsappEnabled, callEnabled } from '@/composables/settings'
|
||||||
import {
|
import {
|
||||||
createResource,
|
createResource,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
|
|||||||
@ -24,6 +24,9 @@
|
|||||||
v-model:resizeColumn="triggerResize"
|
v-model:resizeColumn="triggerResize"
|
||||||
v-model:updatedPageCount="updatedPageCount"
|
v-model:updatedPageCount="updatedPageCount"
|
||||||
doctype="CRM Deal"
|
doctype="CRM Deal"
|
||||||
|
:options="{
|
||||||
|
allowedViews: ['list', 'group_by'],
|
||||||
|
}"
|
||||||
/>
|
/>
|
||||||
<DealsListView
|
<DealsListView
|
||||||
ref="dealsListView"
|
ref="dealsListView"
|
||||||
@ -61,6 +64,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import CustomActions from '@/components/CustomActions.vue'
|
import CustomActions from '@/components/CustomActions.vue'
|
||||||
|
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
||||||
import DealsIcon from '@/components/Icons/DealsIcon.vue'
|
import DealsIcon from '@/components/Icons/DealsIcon.vue'
|
||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
import DealsListView from '@/components/ListViews/DealsListView.vue'
|
import DealsListView from '@/components/ListViews/DealsListView.vue'
|
||||||
@ -77,7 +81,8 @@ import {
|
|||||||
formatTime,
|
formatTime,
|
||||||
} from '@/utils'
|
} from '@/utils'
|
||||||
import { Breadcrumbs } from 'frappe-ui'
|
import { Breadcrumbs } from 'frappe-ui'
|
||||||
import { ref, computed } from 'vue'
|
import { useRoute } from 'vue-router'
|
||||||
|
import { ref, computed, h } from 'vue'
|
||||||
|
|
||||||
const breadcrumbs = [{ label: __('Deals'), route: { name: 'Deals' } }]
|
const breadcrumbs = [{ label: __('Deals'), route: { name: 'Deals' } }]
|
||||||
|
|
||||||
@ -85,6 +90,8 @@ const { getUser } = usersStore()
|
|||||||
const { getOrganization } = organizationsStore()
|
const { getOrganization } = organizationsStore()
|
||||||
const { getDealStatus } = statusesStore()
|
const { getDealStatus } = statusesStore()
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
const dealsListView = ref(null)
|
const dealsListView = ref(null)
|
||||||
const showDealModal = ref(false)
|
const showDealModal = ref(false)
|
||||||
|
|
||||||
@ -98,7 +105,49 @@ const viewControls = ref(null)
|
|||||||
// Rows
|
// Rows
|
||||||
const rows = computed(() => {
|
const rows = computed(() => {
|
||||||
if (!deals.value?.data?.data) return []
|
if (!deals.value?.data?.data) return []
|
||||||
return deals.value.data.data.map((deal) => {
|
if (route.params.viewType === 'group_by') {
|
||||||
|
if (!deals.value?.data.group_by_field?.name) return []
|
||||||
|
return getGroupedByRows(
|
||||||
|
deals.value?.data.data,
|
||||||
|
deals.value?.data.group_by_field
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return parseRows(deals.value?.data.data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function getGroupedByRows(listRows, groupByField) {
|
||||||
|
let groupedRows = []
|
||||||
|
|
||||||
|
groupByField.options?.forEach((option) => {
|
||||||
|
let filteredRows = []
|
||||||
|
|
||||||
|
if (!option) {
|
||||||
|
filteredRows = listRows.filter((row) => !row[groupByField.name])
|
||||||
|
} else {
|
||||||
|
filteredRows = listRows.filter((row) => row[groupByField.name] == option)
|
||||||
|
}
|
||||||
|
|
||||||
|
let groupDetail = {
|
||||||
|
label: groupByField.label,
|
||||||
|
group: option || __(' '),
|
||||||
|
collapsed: false,
|
||||||
|
rows: parseRows(filteredRows),
|
||||||
|
}
|
||||||
|
if (groupByField.name == 'status') {
|
||||||
|
groupDetail.icon = () =>
|
||||||
|
h(IndicatorIcon, {
|
||||||
|
class: getDealStatus(option)?.iconColorClass,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
groupedRows.push(groupDetail)
|
||||||
|
})
|
||||||
|
|
||||||
|
return groupedRows || listRows
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRows(rows) {
|
||||||
|
return rows.map((deal) => {
|
||||||
let _rows = {}
|
let _rows = {}
|
||||||
deals.value.data.rows.forEach((row) => {
|
deals.value.data.rows.forEach((row) => {
|
||||||
_rows[row] = deal[row]
|
_rows[row] = deal[row]
|
||||||
@ -174,5 +223,5 @@ const rows = computed(() => {
|
|||||||
})
|
})
|
||||||
return _rows
|
return _rows
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -293,7 +293,7 @@ import { globalStore } from '@/stores/global'
|
|||||||
import { contactsStore } from '@/stores/contacts'
|
import { contactsStore } from '@/stores/contacts'
|
||||||
import { organizationsStore } from '@/stores/organizations'
|
import { organizationsStore } from '@/stores/organizations'
|
||||||
import { statusesStore } from '@/stores/statuses'
|
import { statusesStore } from '@/stores/statuses'
|
||||||
import { whatsappEnabled, callEnabled } from '@/stores/settings'
|
import { whatsappEnabled, callEnabled } from '@/composables/settings'
|
||||||
import {
|
import {
|
||||||
createResource,
|
createResource,
|
||||||
FileUploader,
|
FileUploader,
|
||||||
|
|||||||
@ -25,6 +25,9 @@
|
|||||||
v-model:updatedPageCount="updatedPageCount"
|
v-model:updatedPageCount="updatedPageCount"
|
||||||
doctype="CRM Lead"
|
doctype="CRM Lead"
|
||||||
:filters="{ converted: 0 }"
|
:filters="{ converted: 0 }"
|
||||||
|
:options="{
|
||||||
|
allowedViews: ['list', 'group_by'],
|
||||||
|
}"
|
||||||
/>
|
/>
|
||||||
<LeadsListView
|
<LeadsListView
|
||||||
ref="leadsListView"
|
ref="leadsListView"
|
||||||
@ -62,6 +65,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import CustomActions from '@/components/CustomActions.vue'
|
import CustomActions from '@/components/CustomActions.vue'
|
||||||
|
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
||||||
import LeadsIcon from '@/components/Icons/LeadsIcon.vue'
|
import LeadsIcon from '@/components/Icons/LeadsIcon.vue'
|
||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
import LeadsListView from '@/components/ListViews/LeadsListView.vue'
|
import LeadsListView from '@/components/ListViews/LeadsListView.vue'
|
||||||
@ -78,8 +82,8 @@ import {
|
|||||||
createToast,
|
createToast,
|
||||||
} from '@/utils'
|
} from '@/utils'
|
||||||
import { createResource, Breadcrumbs } from 'frappe-ui'
|
import { createResource, Breadcrumbs } from 'frappe-ui'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { ref, computed, reactive } from 'vue'
|
import { ref, computed, reactive, h } from 'vue'
|
||||||
|
|
||||||
const breadcrumbs = [{ label: __('Leads'), route: { name: 'Leads' } }]
|
const breadcrumbs = [{ label: __('Leads'), route: { name: 'Leads' } }]
|
||||||
|
|
||||||
@ -88,6 +92,7 @@ const { getOrganization } = organizationsStore()
|
|||||||
const { getLeadStatus } = statusesStore()
|
const { getLeadStatus } = statusesStore()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
const leadsListView = ref(null)
|
const leadsListView = ref(null)
|
||||||
const showLeadModal = ref(false)
|
const showLeadModal = ref(false)
|
||||||
@ -102,7 +107,49 @@ const viewControls = ref(null)
|
|||||||
// Rows
|
// Rows
|
||||||
const rows = computed(() => {
|
const rows = computed(() => {
|
||||||
if (!leads.value?.data?.data) return []
|
if (!leads.value?.data?.data) return []
|
||||||
return leads.value?.data.data.map((lead) => {
|
if (route.params.viewType === 'group_by') {
|
||||||
|
if (!leads.value?.data.group_by_field?.name) return []
|
||||||
|
return getGroupedByRows(
|
||||||
|
leads.value?.data.data,
|
||||||
|
leads.value?.data.group_by_field
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return parseRows(leads.value?.data.data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function getGroupedByRows(listRows, groupByField) {
|
||||||
|
let groupedRows = []
|
||||||
|
|
||||||
|
groupByField.options?.forEach((option) => {
|
||||||
|
let filteredRows = []
|
||||||
|
|
||||||
|
if (!option) {
|
||||||
|
filteredRows = listRows.filter((row) => !row[groupByField.name])
|
||||||
|
} else {
|
||||||
|
filteredRows = listRows.filter((row) => row[groupByField.name] == option)
|
||||||
|
}
|
||||||
|
|
||||||
|
let groupDetail = {
|
||||||
|
label: groupByField.label,
|
||||||
|
group: option || __(' '),
|
||||||
|
collapsed: false,
|
||||||
|
rows: parseRows(filteredRows),
|
||||||
|
}
|
||||||
|
if (groupByField.name == 'status') {
|
||||||
|
groupDetail.icon = () =>
|
||||||
|
h(IndicatorIcon, {
|
||||||
|
class: getLeadStatus(option)?.iconColorClass,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
groupedRows.push(groupDetail)
|
||||||
|
})
|
||||||
|
|
||||||
|
return groupedRows || listRows
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRows(rows) {
|
||||||
|
return rows.map((lead) => {
|
||||||
let _rows = {}
|
let _rows = {}
|
||||||
leads.value?.data.rows.forEach((row) => {
|
leads.value?.data.rows.forEach((row) => {
|
||||||
_rows[row] = lead[row]
|
_rows[row] = lead[row]
|
||||||
@ -182,7 +229,7 @@ const rows = computed(() => {
|
|||||||
})
|
})
|
||||||
return _rows
|
return _rows
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
|
||||||
let newLead = reactive({
|
let newLead = reactive({
|
||||||
salutation: '',
|
salutation: '',
|
||||||
|
|||||||
@ -277,7 +277,7 @@ import {
|
|||||||
import { globalStore } from '@/stores/global'
|
import { globalStore } from '@/stores/global'
|
||||||
import { organizationsStore } from '@/stores/organizations'
|
import { organizationsStore } from '@/stores/organizations'
|
||||||
import { statusesStore } from '@/stores/statuses'
|
import { statusesStore } from '@/stores/statuses'
|
||||||
import { whatsappEnabled, callEnabled, isMobileView } from '@/stores/settings'
|
import { whatsappEnabled, callEnabled, isMobileView } from '@/composables/settings'
|
||||||
import {
|
import {
|
||||||
createResource,
|
createResource,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
|
|||||||
@ -195,7 +195,7 @@ import { globalStore } from '@/stores/global'
|
|||||||
import { contactsStore } from '@/stores/contacts'
|
import { contactsStore } from '@/stores/contacts'
|
||||||
import { organizationsStore } from '@/stores/organizations'
|
import { organizationsStore } from '@/stores/organizations'
|
||||||
import { statusesStore } from '@/stores/statuses'
|
import { statusesStore } from '@/stores/statuses'
|
||||||
import { whatsappEnabled, callEnabled, isMobileView } from '@/stores/settings'
|
import { whatsappEnabled, callEnabled, isMobileView } from '@/composables/settings'
|
||||||
import {
|
import {
|
||||||
createResource,
|
createResource,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
|
|||||||
@ -22,7 +22,7 @@
|
|||||||
<div class="flex-1 overflow-y-auto">
|
<div class="flex-1 overflow-y-auto">
|
||||||
<div
|
<div
|
||||||
v-if="notes.data?.data?.length"
|
v-if="notes.data?.data?.length"
|
||||||
class="grid sm:grid-cols-4 grid-cols-1 gap-4 px-5 pb-3"
|
class="grid grid-cols-1 gap-2 px-3 pb-2 sm:grid-cols-4 sm:gap-4 sm:px-5 sm:pb-3"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="note in notes.data.data"
|
v-for="note in notes.data.data"
|
||||||
@ -75,7 +75,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<ListFooter
|
<ListFooter
|
||||||
v-if="notes.data?.data?.length"
|
v-if="notes.data?.data?.length"
|
||||||
class="border-t px-5 py-2"
|
class="border-t px-3 py-2 sm:px-5"
|
||||||
v-model="notes.data.page_length_count"
|
v-model="notes.data.page_length_count"
|
||||||
:options="{
|
:options="{
|
||||||
rowCount: notes.data.row_count,
|
rowCount: notes.data.row_count,
|
||||||
|
|||||||
@ -14,25 +14,27 @@ const routes = [
|
|||||||
component: () => import('@/pages/MobileNotification.vue'),
|
component: () => import('@/pages/MobileNotification.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/leads',
|
alias: '/leads',
|
||||||
|
path: '/leads/view/:viewType?',
|
||||||
name: 'Leads',
|
name: 'Leads',
|
||||||
component: () => import('@/pages/Leads.vue'),
|
component: () => import('@/pages/Leads.vue'),
|
||||||
meta: { scrollPos: { top: 0, left: 0 } },
|
meta: { scrollPos: { top: 0, left: 0 } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/leads/:leadId/:tabName?',
|
path: '/leads/:leadId',
|
||||||
name: 'Lead',
|
name: 'Lead',
|
||||||
component: () => import(`@/pages/${handleMobileView('Lead')}.vue`),
|
component: () => import(`@/pages/${handleMobileView('Lead')}.vue`),
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/deals',
|
alias: '/deals',
|
||||||
|
path: '/deals/view/:viewType?',
|
||||||
name: 'Deals',
|
name: 'Deals',
|
||||||
component: () => import('@/pages/Deals.vue'),
|
component: () => import('@/pages/Deals.vue'),
|
||||||
meta: { scrollPos: { top: 0, left: 0 } },
|
meta: { scrollPos: { top: 0, left: 0 } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/deals/:dealId/:tabName?',
|
path: '/deals/:dealId',
|
||||||
name: 'Deal',
|
name: 'Deal',
|
||||||
component: () => import(`@/pages/${handleMobileView('Deal')}.vue`),
|
component: () => import(`@/pages/${handleMobileView('Deal')}.vue`),
|
||||||
props: true,
|
props: true,
|
||||||
|
|||||||
@ -20,6 +20,7 @@ export const viewsStore = defineStore('crm-views', (doctype) => {
|
|||||||
publicViews.value = []
|
publicViews.value = []
|
||||||
for (let view of views) {
|
for (let view of views) {
|
||||||
viewsByName[view.name] = view
|
viewsByName[view.name] = view
|
||||||
|
view.type = view.type || 'list'
|
||||||
if (view.pinned) {
|
if (view.pinned) {
|
||||||
pinnedViews.value?.push(view)
|
pinnedViews.value?.push(view)
|
||||||
}
|
}
|
||||||
@ -27,16 +28,17 @@ export const viewsStore = defineStore('crm-views', (doctype) => {
|
|||||||
publicViews.value?.push(view)
|
publicViews.value?.push(view)
|
||||||
}
|
}
|
||||||
if (view.is_default && view.dt) {
|
if (view.is_default && view.dt) {
|
||||||
defaultView.value[view.dt] = view
|
defaultView.value[view.dt + ' ' + view.type] = view
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return views
|
return views
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
function getView(view, doctype = null) {
|
function getView(view, type, doctype = null) {
|
||||||
|
type = type || 'list'
|
||||||
if (!view && doctype) {
|
if (!view && doctype) {
|
||||||
return defaultView.value?.[doctype] || null
|
return defaultView.value[doctype + ' ' + type] || null
|
||||||
}
|
}
|
||||||
return viewsByName[view]
|
return viewsByName[view]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -212,3 +212,7 @@ export function isEmoji(str) {
|
|||||||
const emojiList = gemoji.map((emoji) => emoji.emoji)
|
const emojiList = gemoji.map((emoji) => emoji.emoji)
|
||||||
return emojiList.includes(str)
|
return emojiList.includes(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isTouchScreenDevice() {
|
||||||
|
return "ontouchstart" in document.documentElement;
|
||||||
|
}
|
||||||
|
|||||||
@ -24,8 +24,9 @@ export default defineConfig({
|
|||||||
display: 'standalone',
|
display: 'standalone',
|
||||||
name: 'Frappe CRM',
|
name: 'Frappe CRM',
|
||||||
short_name: 'Frappe CRM',
|
short_name: 'Frappe CRM',
|
||||||
start_url: "/crm",
|
start_url: '/crm',
|
||||||
description: 'Modern & 100% Open-source CRM tool to supercharge your sales operations',
|
description:
|
||||||
|
'Modern & 100% Open-source CRM tool to supercharge your sales operations',
|
||||||
icons: [
|
icons: [
|
||||||
{
|
{
|
||||||
src: '/assets/crm/manifest/manifest-icon-192.maskable.png',
|
src: '/assets/crm/manifest/manifest-icon-192.maskable.png',
|
||||||
@ -54,6 +55,25 @@ export default defineConfig({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
{
|
||||||
|
name: 'transform-index.html',
|
||||||
|
transformIndexHtml(html, context) {
|
||||||
|
if (!context.server) {
|
||||||
|
return html.replace(
|
||||||
|
/<\/body>/,
|
||||||
|
`
|
||||||
|
<script>
|
||||||
|
{% for key in boot %}
|
||||||
|
window["{{ key }}"] = {{ boot[key] | tojson }};
|
||||||
|
{% endfor %}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return html
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user