Merge pull request #623 from frappe/develop
chore: Merge develop to main
This commit is contained in:
commit
cc50319941
@ -15,12 +15,10 @@ def login():
|
|||||||
frappe.local.response["location"] = frappe.local.response["redirect_to"]
|
frappe.local.response["location"] = frappe.local.response["redirect_to"]
|
||||||
|
|
||||||
|
|
||||||
def validate_reset_password(user):
|
def validate_reset_password(doc, event):
|
||||||
if frappe.conf.demo_username and frappe.session.user == frappe.conf.demo_username:
|
if frappe.conf.demo_username and frappe.session.user == frappe.conf.demo_username:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Password cannot be reset by Demo User {}").format(
|
_("Password cannot be reset by Demo User {}").format(frappe.bold(frappe.conf.demo_username)),
|
||||||
frappe.bold(frappe.conf.demo_username)
|
|
||||||
),
|
|
||||||
frappe.PermissionError,
|
frappe.PermissionError,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -28,9 +26,6 @@ def validate_reset_password(user):
|
|||||||
def validate_user(doc, event):
|
def validate_user(doc, event):
|
||||||
if frappe.conf.demo_username and frappe.session.user == frappe.conf.demo_username and doc.new_password:
|
if frappe.conf.demo_username and frappe.session.user == frappe.conf.demo_username and doc.new_password:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Password cannot be reset by Demo User {}").format(
|
_("Password cannot be reset by Demo User {}").format(frappe.bold(frappe.conf.demo_username)),
|
||||||
frappe.bold(frappe.conf.demo_username)
|
|
||||||
),
|
|
||||||
frappe.PermissionError,
|
frappe.PermissionError,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import json
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||||
from frappe.model import no_value_fields
|
from frappe.model import no_value_fields
|
||||||
from frappe.model.document import get_controller
|
from frappe.model.document import get_controller
|
||||||
from frappe.utils import make_filter_tuple
|
from frappe.utils import make_filter_tuple
|
||||||
@ -178,23 +179,39 @@ def get_doctype_fields_meta(DocField, doctype, allowed_fieldtypes, restricted_fi
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_quick_filters(doctype: str):
|
def get_quick_filters(doctype: str, cached: bool = True):
|
||||||
meta = frappe.get_meta(doctype)
|
meta = frappe.get_meta(doctype, cached)
|
||||||
fields = [field for field in meta.fields if field.in_standard_filter]
|
|
||||||
quick_filters = []
|
quick_filters = []
|
||||||
|
|
||||||
|
if global_settings := frappe.db.exists("CRM Global Settings", {"dt": doctype, "type": "Quick Filters"}):
|
||||||
|
_quick_filters = frappe.db.get_value("CRM Global Settings", global_settings, "json")
|
||||||
|
_quick_filters = json.loads(_quick_filters) or []
|
||||||
|
|
||||||
|
fields = []
|
||||||
|
|
||||||
|
for filter in _quick_filters:
|
||||||
|
if filter == "name":
|
||||||
|
fields.append({"label": "Name", "fieldname": "name", "fieldtype": "Data"})
|
||||||
|
else:
|
||||||
|
field = next((f for f in meta.fields if f.fieldname == filter), None)
|
||||||
|
if field:
|
||||||
|
fields.append(field)
|
||||||
|
|
||||||
|
else:
|
||||||
|
fields = [field for field in meta.fields if field.in_standard_filter]
|
||||||
|
|
||||||
for field in fields:
|
for field in fields:
|
||||||
options = field.options
|
options = field.get("options")
|
||||||
if field.fieldtype == "Select" and options and isinstance(options, str):
|
if field.get("fieldtype") == "Select" and options and isinstance(options, str):
|
||||||
options = options.split("\n")
|
options = options.split("\n")
|
||||||
options = [{"label": option, "value": option} for option in options]
|
options = [{"label": option, "value": option} for option in options]
|
||||||
if not any([not option.get("value") for option in options]):
|
if not any([not option.get("value") for option in options]):
|
||||||
options.insert(0, {"label": "", "value": ""})
|
options.insert(0, {"label": "", "value": ""})
|
||||||
quick_filters.append(
|
quick_filters.append(
|
||||||
{
|
{
|
||||||
"label": _(field.label),
|
"label": _(field.get("label")),
|
||||||
"fieldname": field.fieldname,
|
"fieldname": field.get("fieldname"),
|
||||||
"fieldtype": field.fieldtype,
|
"fieldtype": field.get("fieldtype"),
|
||||||
"options": options,
|
"options": options,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -205,6 +222,55 @@ def get_quick_filters(doctype: str):
|
|||||||
return quick_filters
|
return quick_filters
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def update_quick_filters(quick_filters: str, old_filters: str, doctype: str):
|
||||||
|
quick_filters = json.loads(quick_filters)
|
||||||
|
old_filters = json.loads(old_filters)
|
||||||
|
|
||||||
|
new_filters = [filter for filter in quick_filters if filter not in old_filters]
|
||||||
|
removed_filters = [filter for filter in old_filters if filter not in quick_filters]
|
||||||
|
|
||||||
|
# update or create global quick filter settings
|
||||||
|
create_update_global_settings(doctype, quick_filters)
|
||||||
|
|
||||||
|
# remove old filters
|
||||||
|
for filter in removed_filters:
|
||||||
|
update_in_standard_filter(filter, doctype, 0)
|
||||||
|
|
||||||
|
# add new filters
|
||||||
|
for filter in new_filters:
|
||||||
|
update_in_standard_filter(filter, doctype, 1)
|
||||||
|
|
||||||
|
|
||||||
|
def create_update_global_settings(doctype, quick_filters):
|
||||||
|
if global_settings := frappe.db.exists("CRM Global Settings", {"dt": doctype, "type": "Quick Filters"}):
|
||||||
|
frappe.db.set_value("CRM Global Settings", global_settings, "json", json.dumps(quick_filters))
|
||||||
|
else:
|
||||||
|
# create CRM Global Settings doc
|
||||||
|
doc = frappe.new_doc("CRM Global Settings")
|
||||||
|
doc.dt = doctype
|
||||||
|
doc.type = "Quick Filters"
|
||||||
|
doc.json = json.dumps(quick_filters)
|
||||||
|
doc.insert()
|
||||||
|
|
||||||
|
|
||||||
|
def update_in_standard_filter(fieldname, doctype, value):
|
||||||
|
if property_name := frappe.db.exists(
|
||||||
|
"Property Setter",
|
||||||
|
{"doc_type": doctype, "field_name": fieldname, "property": "in_standard_filter"},
|
||||||
|
):
|
||||||
|
frappe.db.set_value("Property Setter", property_name, "value", value)
|
||||||
|
else:
|
||||||
|
make_property_setter(
|
||||||
|
doctype,
|
||||||
|
fieldname,
|
||||||
|
"in_standard_filter",
|
||||||
|
value,
|
||||||
|
"Check",
|
||||||
|
validate_fields_for_doctype=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_data(
|
def get_data(
|
||||||
doctype: str,
|
doctype: str,
|
||||||
@ -382,7 +448,7 @@ def get_data(
|
|||||||
all_count = frappe.get_list(
|
all_count = frappe.get_list(
|
||||||
doctype,
|
doctype,
|
||||||
filters=convert_filter_to_tuple(doctype, new_filters),
|
filters=convert_filter_to_tuple(doctype, new_filters),
|
||||||
fields="count(*) as total_count"
|
fields="count(*) as total_count",
|
||||||
)[0].total_count
|
)[0].total_count
|
||||||
|
|
||||||
kc["all_count"] = all_count
|
kc["all_count"] = all_count
|
||||||
@ -485,9 +551,9 @@ def get_data(
|
|||||||
"page_length_count": page_length_count,
|
"page_length_count": page_length_count,
|
||||||
"is_default": is_default,
|
"is_default": is_default,
|
||||||
"views": get_views(doctype),
|
"views": get_views(doctype),
|
||||||
"total_count": frappe.get_list(
|
"total_count": frappe.get_list(doctype, filters=filters, fields="count(*) as total_count")[
|
||||||
doctype, filters=filters, fields="count(*) as total_count"
|
0
|
||||||
)[0].total_count,
|
].total_count,
|
||||||
"row_count": len(data),
|
"row_count": len(data),
|
||||||
"form_script": get_form_script(doctype),
|
"form_script": get_form_script(doctype),
|
||||||
"list_script": get_form_script(doctype, "List"),
|
"list_script": get_form_script(doctype, "List"),
|
||||||
|
|||||||
0
crm/fcrm/doctype/crm_global_settings/__init__.py
Normal file
0
crm/fcrm/doctype/crm_global_settings/__init__.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
// frappe.ui.form.on("CRM Global Settings", {
|
||||||
|
// refresh(frm) {
|
||||||
|
|
||||||
|
// },
|
||||||
|
// });
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"autoname": "format:{type}-{dt}",
|
||||||
|
"creation": "2025-02-28 14:37:10.002433",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"dt",
|
||||||
|
"column_break_kipp",
|
||||||
|
"type",
|
||||||
|
"section_break_vass",
|
||||||
|
"json"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"default": "DocType",
|
||||||
|
"fieldname": "dt",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "DocType",
|
||||||
|
"options": "DocType",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_kipp",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "type",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Type",
|
||||||
|
"options": "Quick Filters\nSidebar Items",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_vass",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "json",
|
||||||
|
"fieldtype": "JSON",
|
||||||
|
"label": "JSON"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2025-02-28 14:55:33.801215",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "FCRM",
|
||||||
|
"name": "CRM Global Settings",
|
||||||
|
"naming_rule": "Expression",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"sort_field": "creation",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class CRMGlobalSettings(Document):
|
||||||
|
pass
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||||
|
|
||||||
|
|
||||||
|
# On IntegrationTestCase, the doctype test records and all
|
||||||
|
# link-field test record dependencies are recursively loaded
|
||||||
|
# Use these module variables to add/remove to/from that list
|
||||||
|
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||||
|
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||||
|
|
||||||
|
|
||||||
|
class UnitTestCRMGlobalSettings(UnitTestCase):
|
||||||
|
"""
|
||||||
|
Unit tests for CRMGlobalSettings.
|
||||||
|
Use this class for testing individual functions and methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class IntegrationTestCRMGlobalSettings(IntegrationTestCase):
|
||||||
|
"""
|
||||||
|
Integration tests for CRMGlobalSettings.
|
||||||
|
Use this class for testing interactions between multiple components.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
@ -132,7 +132,6 @@ before_uninstall = "crm.uninstall.before_uninstall"
|
|||||||
override_doctype_class = {
|
override_doctype_class = {
|
||||||
"Contact": "crm.overrides.contact.CustomContact",
|
"Contact": "crm.overrides.contact.CustomContact",
|
||||||
"Email Template": "crm.overrides.email_template.CustomEmailTemplate",
|
"Email Template": "crm.overrides.email_template.CustomEmailTemplate",
|
||||||
"User": "crm.overrides.user.CustomUser",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Document Events
|
# Document Events
|
||||||
@ -161,6 +160,7 @@ doc_events = {
|
|||||||
},
|
},
|
||||||
"User": {
|
"User": {
|
||||||
"before_validate": ["crm.api.demo.validate_user"],
|
"before_validate": ["crm.api.demo.validate_user"],
|
||||||
|
"validate_reset_password": ["crm.api.demo.validate_reset_password"],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
# import frappe
|
|
||||||
from frappe import _
|
|
||||||
from frappe.core.doctype.user.user import User
|
|
||||||
from crm.api.demo import validate_reset_password
|
|
||||||
|
|
||||||
|
|
||||||
class CustomUser(User):
|
|
||||||
def validate_reset_password(self):
|
|
||||||
# restrict demo user to reset password
|
|
||||||
validate_reset_password(self)
|
|
||||||
@ -1,5 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<Popover placement="right-start" class="flex w-full">
|
<Popover
|
||||||
|
placement="right-start"
|
||||||
|
trigger="hover"
|
||||||
|
:hoverDelay="0.1"
|
||||||
|
:leaveDelay="0.1"
|
||||||
|
>
|
||||||
<template #target="{ togglePopover }">
|
<template #target="{ togglePopover }">
|
||||||
<button
|
<button
|
||||||
:class="[
|
:class="[
|
||||||
@ -19,19 +24,19 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #body>
|
<template #body>
|
||||||
<div
|
<div
|
||||||
class="grid grid-cols-3 justify-between mx-3 p-2 min-w-40 rounded-lg bg-surface-modal shadow-2xl ring-1 ring-black ring-opacity-5 focus:outline-none"
|
class="flex w-fit mx-2 min-w-32 max-w-48 flex-col rounded-lg border border-outline-gray-2 bg-surface-white p-1.5 text-sm text-ink-gray-8 shadow-xl auto-fill-[100px]"
|
||||||
>
|
>
|
||||||
<div v-for="app in apps.data" :key="app.name">
|
<a
|
||||||
<a
|
:href="app.route"
|
||||||
:href="app.route"
|
v-for="app in apps.data"
|
||||||
class="flex flex-col gap-1.5 rounded justify-center items-center py-2 px-1 hover:bg-surface-gray-2"
|
key="name"
|
||||||
>
|
class="flex items-center gap-2 rounded p-1.5 hover:bg-surface-gray-2"
|
||||||
<img class="size-8" :src="app.logo" />
|
>
|
||||||
<div class="text-sm text-ink-gray-7" @click="app.onClick">
|
<img class="size-6" :src="app.logo" />
|
||||||
{{ app.title }}
|
<span class="max-w-18 w-full truncate">
|
||||||
</div>
|
{{ app.title }}
|
||||||
</a>
|
</span>
|
||||||
</div>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
@ -94,7 +94,7 @@ import { usersStore } from '@/stores/users'
|
|||||||
import { TextEditorBubbleMenu, TextEditor, FileUploader } from 'frappe-ui'
|
import { TextEditorBubbleMenu, TextEditor, FileUploader } from 'frappe-ui'
|
||||||
import { capture } from '@/telemetry'
|
import { capture } from '@/telemetry'
|
||||||
import { EditorContent } from '@tiptap/vue-3'
|
import { EditorContent } from '@tiptap/vue-3'
|
||||||
import { ref, computed, defineModel } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
placeholder: {
|
placeholder: {
|
||||||
|
|||||||
@ -183,7 +183,7 @@ import { capture } from '@/telemetry'
|
|||||||
import { validateEmail } from '@/utils'
|
import { validateEmail } from '@/utils'
|
||||||
import Paragraph from '@tiptap/extension-paragraph'
|
import Paragraph from '@tiptap/extension-paragraph'
|
||||||
import { EditorContent } from '@tiptap/vue-3'
|
import { EditorContent } from '@tiptap/vue-3'
|
||||||
import { ref, computed, defineModel, nextTick } from 'vue'
|
import { ref, computed, nextTick } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
placeholder: {
|
placeholder: {
|
||||||
|
|||||||
16
frontend/src/components/Icons/ExportIcon.vue
Normal file
16
frontend/src/components/Icons/ExportIcon.vue
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8 10V2M8 2L5.33333 4.66663M8 2L10.6667 4.66663M11.8571 6.99996H13C13.5523 6.99996 14 7.44767 14 7.99996V13C14 13.5522 13.5523 14 13 14H3C2.44772 14 2 13.5522 2 13V7.99996C2 7.44767 2.44772 6.99996 3 6.99996H4.14286"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
20
frontend/src/components/Icons/QuickFilterIcon.vue
Normal file
20
frontend/src/components/Icons/QuickFilterIcon.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.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="lucide lucide-list-filter-plus"
|
||||||
|
>
|
||||||
|
<path d="M10 18h4" />
|
||||||
|
<path d="M11 6H3" />
|
||||||
|
<path d="M15 6h6" />
|
||||||
|
<path d="M18 9V3" />
|
||||||
|
<path d="M7 12h8" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
@ -46,7 +46,7 @@ import { Dropdown, Tooltip } from 'frappe-ui'
|
|||||||
import { timeAgo, formatDate, formatTime } from '@/utils'
|
import { timeAgo, formatDate, formatTime } from '@/utils'
|
||||||
import { statusesStore } from '@/stores/statuses'
|
import { statusesStore } from '@/stores/statuses'
|
||||||
import { capture } from '@/telemetry'
|
import { capture } from '@/telemetry'
|
||||||
import { computed, defineModel } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
const data = defineModel()
|
const data = defineModel()
|
||||||
const emit = defineEmits(['updateField'])
|
const emit = defineEmits(['updateField'])
|
||||||
|
|||||||
@ -58,6 +58,79 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="customizeQuickFilter"
|
||||||
|
class="flex items-center justify-between gap-2 p-5"
|
||||||
|
>
|
||||||
|
<div class="flex flex-1 items-center overflow-hidden pl-1 gap-2">
|
||||||
|
<FadedScrollableDiv
|
||||||
|
class="flex items-center gap-2 overflow-x-auto -ml-1"
|
||||||
|
orientation="horizontal"
|
||||||
|
>
|
||||||
|
<Draggable
|
||||||
|
class="flex gap-2"
|
||||||
|
:list="newQuickFilters"
|
||||||
|
group="filters"
|
||||||
|
item-key="fieldname"
|
||||||
|
>
|
||||||
|
<template #item="{ element: filter }">
|
||||||
|
<Button class="group whitespace-nowrap cursor-grab">
|
||||||
|
<template #default>
|
||||||
|
<Tooltip :text="filter.fieldname">
|
||||||
|
<span>{{ filter.label }}</span>
|
||||||
|
</Tooltip>
|
||||||
|
</template>
|
||||||
|
<template #suffix>
|
||||||
|
<FeatherIcon
|
||||||
|
class="h-3.5 cursor-pointer group-hover:flex hidden"
|
||||||
|
name="x"
|
||||||
|
@click.stop="removeQuickFilter(filter)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</Draggable>
|
||||||
|
</FadedScrollableDiv>
|
||||||
|
<Autocomplete
|
||||||
|
value=""
|
||||||
|
:options="quickFilterOptions"
|
||||||
|
@change="(e) => addQuickFilter(e)"
|
||||||
|
>
|
||||||
|
<template #target="{ togglePopover }">
|
||||||
|
<Button
|
||||||
|
class="whitespace-nowrap mr-2"
|
||||||
|
variant="ghost"
|
||||||
|
@click="togglePopover()"
|
||||||
|
:label="__('Add filter')"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<FeatherIcon name="plus" class="h-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
<template #item-label="{ option }">
|
||||||
|
<Tooltip :text="option.value" :hover-delay="1">
|
||||||
|
<div class="flex-1 truncate text-ink-gray-7">
|
||||||
|
{{ option.label }}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</template>
|
||||||
|
</Autocomplete>
|
||||||
|
</div>
|
||||||
|
<div class="-ml-2 h-[70%] border-l" />
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<Button
|
||||||
|
:label="__('Save')"
|
||||||
|
:loading="updateQuickFilters.loading"
|
||||||
|
@click="saveQuickFilters"
|
||||||
|
/>
|
||||||
|
<Button @click="customizeQuickFilter = false">
|
||||||
|
<template #icon>
|
||||||
|
<FeatherIcon name="x" class="h-4 w-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-else class="flex items-center justify-between gap-2 px-5 py-4">
|
<div v-else class="flex items-center justify-between gap-2 px-5 py-4">
|
||||||
<FadedScrollableDiv
|
<FadedScrollableDiv
|
||||||
class="flex flex-1 items-center overflow-x-auto -ml-1"
|
class="flex flex-1 items-center overflow-x-auto -ml-1"
|
||||||
@ -120,9 +193,7 @@
|
|||||||
@update="(isDefault) => updateColumns(isDefault)"
|
@update="(isDefault) => updateColumns(isDefault)"
|
||||||
/>
|
/>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
v-if="
|
v-if="route.params.viewType !== 'kanban'"
|
||||||
!options.hideColumnsButton && route.params.viewType !== 'kanban'
|
|
||||||
"
|
|
||||||
:options="[
|
:options="[
|
||||||
{
|
{
|
||||||
group: __('Options'),
|
group: __('Options'),
|
||||||
@ -130,9 +201,15 @@
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
label: __('Export'),
|
label: __('Export'),
|
||||||
icon: () =>
|
icon: () => h(ExportIcon, { class: 'h-4 w-4' }),
|
||||||
h(FeatherIcon, { name: 'download', class: 'h-4 w-4' }),
|
|
||||||
onClick: () => (showExportDialog = true),
|
onClick: () => (showExportDialog = true),
|
||||||
|
condition: () => !options.hideColumnsButton,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Customize quick filters'),
|
||||||
|
icon: () => h(QuickFilterIcon, { class: 'h-4 w-4' }),
|
||||||
|
onClick: () => showCustomizeQuickFilter(),
|
||||||
|
condition: () => isManager(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -218,7 +295,10 @@ import DuplicateIcon from '@/components/Icons/DuplicateIcon.vue'
|
|||||||
import CheckIcon from '@/components/Icons/CheckIcon.vue'
|
import CheckIcon from '@/components/Icons/CheckIcon.vue'
|
||||||
import PinIcon from '@/components/Icons/PinIcon.vue'
|
import PinIcon from '@/components/Icons/PinIcon.vue'
|
||||||
import UnpinIcon from '@/components/Icons/UnpinIcon.vue'
|
import UnpinIcon from '@/components/Icons/UnpinIcon.vue'
|
||||||
|
import ExportIcon from '@/components/Icons/ExportIcon.vue'
|
||||||
|
import QuickFilterIcon from '@/components/Icons/QuickFilterIcon.vue'
|
||||||
import ViewModal from '@/components/Modals/ViewModal.vue'
|
import ViewModal from '@/components/Modals/ViewModal.vue'
|
||||||
|
import Autocomplete from '@/components/frappe-ui/Autocomplete.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 GroupBy from '@/components/GroupBy.vue'
|
||||||
@ -229,8 +309,10 @@ import { getSettings } from '@/stores/settings'
|
|||||||
import { globalStore } from '@/stores/global'
|
import { globalStore } from '@/stores/global'
|
||||||
import { viewsStore } from '@/stores/views'
|
import { viewsStore } from '@/stores/views'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { isEmoji } from '@/utils'
|
import { getMeta } from '@/stores/meta'
|
||||||
|
import { isEmoji, createToast } from '@/utils'
|
||||||
import {
|
import {
|
||||||
|
Tooltip,
|
||||||
createResource,
|
createResource,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
call,
|
call,
|
||||||
@ -241,6 +323,7 @@ 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 '@/composables/settings'
|
import { isMobileView } from '@/composables/settings'
|
||||||
|
import Draggable from 'vuedraggable'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -594,11 +677,102 @@ const viewsDropdownOptions = computed(() => {
|
|||||||
return _views
|
return _views
|
||||||
})
|
})
|
||||||
|
|
||||||
const quickFilterList = computed(() => {
|
const { getFields } = getMeta(props.doctype)
|
||||||
let filters = [{ fieldname: 'name', fieldtype: 'Data', label: __('ID') }]
|
|
||||||
if (quickFilters.data) {
|
const customizeQuickFilter = ref(false)
|
||||||
filters.push(...quickFilters.data)
|
|
||||||
|
function showCustomizeQuickFilter() {
|
||||||
|
customizeQuickFilter.value = true
|
||||||
|
setupNewQuickFilters(quickFilters.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
const newQuickFilters = ref([])
|
||||||
|
|
||||||
|
function addQuickFilter(f) {
|
||||||
|
if (!newQuickFilters.value.some((filter) => filter.fieldname === f.value)) {
|
||||||
|
newQuickFilters.value.push({
|
||||||
|
label: f.label,
|
||||||
|
fieldname: f.value,
|
||||||
|
fieldtype: f.fieldtype,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeQuickFilter(f) {
|
||||||
|
newQuickFilters.value = newQuickFilters.value.filter(
|
||||||
|
(filter) => filter.fieldname !== f.fieldname,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateQuickFilters = createResource({
|
||||||
|
url: 'crm.api.doc.update_quick_filters',
|
||||||
|
onSuccess() {
|
||||||
|
customizeQuickFilter.value = false
|
||||||
|
|
||||||
|
quickFilters.update({ params: { doctype: props.doctype, cached: false } })
|
||||||
|
quickFilters.reload()
|
||||||
|
|
||||||
|
createToast({
|
||||||
|
title: __('Quick Filters updated successfully'),
|
||||||
|
icon: 'check',
|
||||||
|
iconClasses: 'text-ink-green-3',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
function saveQuickFilters() {
|
||||||
|
let new_filters =
|
||||||
|
newQuickFilters.value?.map((filter) => filter.fieldname) || []
|
||||||
|
let old_filters = quickFilters.data?.map((filter) => filter.fieldname) || []
|
||||||
|
|
||||||
|
updateQuickFilters.update({
|
||||||
|
params: {
|
||||||
|
quick_filters: JSON.stringify(new_filters),
|
||||||
|
old_filters: JSON.stringify(old_filters),
|
||||||
|
doctype: props.doctype,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
updateQuickFilters.fetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
const quickFilterOptions = computed(() => {
|
||||||
|
let fields = getFields()
|
||||||
|
if (!fields) return []
|
||||||
|
|
||||||
|
let restrictedFieldtypes = [
|
||||||
|
'Tab Break',
|
||||||
|
'Section Break',
|
||||||
|
'Column Break',
|
||||||
|
'Table',
|
||||||
|
'Table MultiSelect',
|
||||||
|
'HTML',
|
||||||
|
'Button',
|
||||||
|
'Image',
|
||||||
|
'Fold',
|
||||||
|
'Heading',
|
||||||
|
]
|
||||||
|
let options = fields
|
||||||
|
.filter((f) => f.label && !restrictedFieldtypes.includes(f.fieldtype))
|
||||||
|
.map((field) => ({
|
||||||
|
label: field.label,
|
||||||
|
value: field.fieldname,
|
||||||
|
fieldtype: field.fieldtype,
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (!options.some((f) => f.fieldname === 'name')) {
|
||||||
|
options.push({
|
||||||
|
label: __('Name'),
|
||||||
|
value: 'name',
|
||||||
|
fieldtype: 'Data',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
|
})
|
||||||
|
|
||||||
|
const quickFilterList = computed(() => {
|
||||||
|
let filters = quickFilters.data || []
|
||||||
|
|
||||||
filters.forEach((filter) => {
|
filters.forEach((filter) => {
|
||||||
filter['value'] = filter.fieldtype == 'Check' ? false : ''
|
filter['value'] = filter.fieldtype == 'Check' ? false : ''
|
||||||
@ -630,8 +804,19 @@ const quickFilters = createResource({
|
|||||||
params: { doctype: props.doctype },
|
params: { doctype: props.doctype },
|
||||||
cache: ['Quick Filters', props.doctype],
|
cache: ['Quick Filters', props.doctype],
|
||||||
auto: true,
|
auto: true,
|
||||||
|
onSuccess(filters) {
|
||||||
|
setupNewQuickFilters(filters)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function setupNewQuickFilters(filters) {
|
||||||
|
newQuickFilters.value = filters.map((f) => ({
|
||||||
|
label: f.label,
|
||||||
|
fieldname: f.fieldname,
|
||||||
|
fieldtype: f.fieldtype,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
function applyQuickFilter(filter, value) {
|
function applyQuickFilter(filter, value) {
|
||||||
let filters = { ...list.value.params.filters }
|
let filters = { ...list.value.params.filters }
|
||||||
let field = filter.fieldname
|
let field = filter.fieldname
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user