Merge pull request #47 from shariquerik/pinned-views
feat: Pinned Views
This commit is contained in:
commit
6cabe46852
@ -9,6 +9,8 @@
|
||||
"user",
|
||||
"column_break_zacm",
|
||||
"dt",
|
||||
"route_name",
|
||||
"pinned",
|
||||
"columns_tab",
|
||||
"default_columns",
|
||||
"columns",
|
||||
@ -84,11 +86,22 @@
|
||||
"fieldname": "default_columns",
|
||||
"fieldtype": "Check",
|
||||
"label": "Default Columns"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "pinned",
|
||||
"fieldtype": "Check",
|
||||
"label": "Pinned"
|
||||
},
|
||||
{
|
||||
"fieldname": "route_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Route Name"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-12-30 19:28:02.541487",
|
||||
"modified": "2024-01-01 18:44:22.815490",
|
||||
"modified_by": "Administrator",
|
||||
"module": "FCRM",
|
||||
"name": "CRM View Settings",
|
||||
|
||||
@ -24,6 +24,7 @@ def create(view, duplicate=False):
|
||||
doc.user = frappe.session.user
|
||||
doc.filters = json.dumps(view.filters)
|
||||
doc.order_by = view.order_by
|
||||
doc.route_name = view.route_name or ""
|
||||
doc.default_columns = view.default_columns or False
|
||||
|
||||
if not view.columns:
|
||||
@ -59,6 +60,7 @@ def update(view):
|
||||
|
||||
doc = frappe.get_doc("CRM View Settings", view.name)
|
||||
doc.label = view.label
|
||||
doc.route_name = view.route_name or ""
|
||||
doc.default_columns = default_columns
|
||||
doc.filters = json.dumps(filters)
|
||||
doc.order_by = view.order_by
|
||||
@ -72,6 +74,12 @@ def delete(name):
|
||||
if frappe.db.exists("CRM View Settings", name):
|
||||
frappe.delete_doc("CRM View Settings", name)
|
||||
|
||||
@frappe.whitelist()
|
||||
def pin(name, value):
|
||||
doc = frappe.get_doc("CRM View Settings", name)
|
||||
doc.pinned = value
|
||||
doc.save()
|
||||
|
||||
def remove_duplicates(l):
|
||||
return list(dict.fromkeys(l))
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
class="flex h-full flex-col justify-between transition-all duration-300 ease-in-out"
|
||||
:class="isSidebarCollapsed ? 'w-12' : 'w-56'"
|
||||
>
|
||||
<div class="flex flex-col overflow-hidden">
|
||||
<div class="flex flex-1 flex-col overflow-hidden">
|
||||
<UserDropdown class="p-2" :isCollapsed="isSidebarCollapsed" />
|
||||
<div class="flex flex-col overflow-y-auto">
|
||||
<SidebarLink
|
||||
@ -15,6 +15,41 @@
|
||||
class="mx-2 my-0.5"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="isSidebarCollapsed && getPinnedViews().length"
|
||||
class="mx-2 my-2 h-1 border-b"
|
||||
/>
|
||||
<div
|
||||
v-if="getPinnedViews().length"
|
||||
class="flex flex-col overflow-y-auto"
|
||||
:class="isSidebarCollapsed ? 'mt-0' : 'mt-4'"
|
||||
>
|
||||
<div
|
||||
class="h-7 px-3 text-base text-gray-600 transition-all duration-300 ease-in-out"
|
||||
:class="
|
||||
isSidebarCollapsed
|
||||
? 'ml-0 h-0 overflow-hidden opacity-0'
|
||||
: 'ml-2 w-auto opacity-100'
|
||||
"
|
||||
>
|
||||
Pinned Views
|
||||
</div>
|
||||
<SidebarLink
|
||||
v-for="pinnedView in getPinnedViews()"
|
||||
:icon="
|
||||
h(getIcon(pinnedView.route_name), {
|
||||
class: 'h-4.5 w-4.5 text-gray-700',
|
||||
})
|
||||
"
|
||||
:label="pinnedView.label"
|
||||
:to="{
|
||||
name: pinnedView.route_name,
|
||||
query: { view: pinnedView.name },
|
||||
}"
|
||||
:isCollapsed="isSidebarCollapsed"
|
||||
class="mx-2 my-0.5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SidebarLink
|
||||
:label="isSidebarCollapsed ? 'Expand' : 'Collapse'"
|
||||
@ -35,6 +70,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import PinIcon from '@/components/Icons/PinIcon.vue'
|
||||
import UserDropdown from '@/components/UserDropdown.vue'
|
||||
import LeadsIcon from '@/components/Icons/LeadsIcon.vue'
|
||||
import DealsIcon from '@/components/Icons/DealsIcon.vue'
|
||||
@ -44,7 +80,11 @@ import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||
import CollapseSidebar from '@/components/Icons/CollapseSidebar.vue'
|
||||
import SidebarLink from '@/components/SidebarLink.vue'
|
||||
import { viewsStore } from '@/stores/views'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { h } from 'vue'
|
||||
|
||||
const { getPinnedViews } = viewsStore()
|
||||
|
||||
const links = [
|
||||
{
|
||||
@ -79,5 +119,24 @@ const links = [
|
||||
},
|
||||
]
|
||||
|
||||
function getIcon(routeName) {
|
||||
switch (routeName) {
|
||||
case 'Leads':
|
||||
return LeadsIcon
|
||||
case 'Deals':
|
||||
return DealsIcon
|
||||
case 'Contacts':
|
||||
return ContactsIcon
|
||||
case 'Organizations':
|
||||
return OrganizationsIcon
|
||||
case 'Notes':
|
||||
return NoteIcon
|
||||
case 'Call Logs':
|
||||
return PhoneIcon
|
||||
default:
|
||||
return PinIcon
|
||||
}
|
||||
}
|
||||
|
||||
const isSidebarCollapsed = useStorage('sidebar_is_collapsed', false)
|
||||
</script>
|
||||
|
||||
16
frontend/src/components/Icons/DuplicateIcon.vue
Normal file
16
frontend/src/components/Icons/DuplicateIcon.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
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M6.72124 1.6001C5.61667 1.6001 4.72124 2.49553 4.72124 3.6001V4.42747H5.72124V3.6001C5.72124 3.04781 6.16895 2.6001 6.72124 2.6001H12.9997C13.552 2.6001 13.9997 3.04781 13.9997 3.6001V8.82643C13.9997 9.37871 13.552 9.82643 12.9997 9.82643H12.1801V10.8264H12.9997C14.1043 10.8264 14.9997 9.931 14.9997 8.82643V3.6001C14.9997 2.49553 14.1043 1.6001 12.9997 1.6001H6.72124ZM3 5.28494C1.89543 5.28494 1 6.18037 1 7.28494V12.5113C1 13.6158 1.89543 14.5113 3 14.5113H9.27846C10.383 14.5113 11.2785 13.6158 11.2785 12.5113V7.28494C11.2785 6.18037 10.383 5.28494 9.27846 5.28494H3ZM2 7.28494C2 6.73266 2.44772 6.28494 3 6.28494H9.27846C9.83075 6.28494 10.2785 6.73266 10.2785 7.28494V12.5113C10.2785 13.0636 9.83075 13.5113 9.27846 13.5113H3C2.44771 13.5113 2 13.0636 2 12.5113V7.28494Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
16
frontend/src/components/Icons/PinIcon.vue
Normal file
16
frontend/src/components/Icons/PinIcon.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
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M6.17683 1C4.89138 1 3.81543 1.97482 3.68896 3.25405L3.19111 8.2899C3.16577 8.54615 3.0749 8.79156 2.92723 9.00252L1.98134 10.3538C1.51739 11.0166 1.99155 11.9273 2.80057 11.9273H7.53349V14.4999C7.53349 14.7761 7.75735 14.9999 8.03349 14.9999C8.30963 14.9999 8.53349 14.7761 8.53349 14.4999V11.9273H13.2016C14.0107 11.9273 14.4848 11.0166 14.0209 10.3538L13.068 8.99254C12.9236 8.78634 12.8335 8.54713 12.8059 8.29695L12.246 3.22567C12.1062 1.95882 11.0357 1 9.76113 1H6.17683ZM4.68411 3.35243C4.75999 2.5849 5.40556 2 6.17683 2H9.76113C10.5259 2 11.1682 2.57529 11.2521 3.3354L11.8119 8.40668C11.858 8.82365 12.0082 9.22233 12.2488 9.566L13.2016 10.9273H2.80057L3.74646 9.57598C3.99257 9.22439 4.14403 8.81536 4.18625 8.38828L4.68411 3.35243Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
25
frontend/src/components/Icons/UnpinIcon.vue
Normal file
25
frontend/src/components/Icons/UnpinIcon.vue
Normal file
@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<svg
|
||||
width="15"
|
||||
height="14"
|
||||
viewBox="0 0 15 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M5.1783 0C3.89284 0 2.81689 0.974825 2.69042 2.25405L2.19257 7.2899C2.16724 7.54615 2.07636 7.79156 1.92869 8.00252L0.982801 9.35379C0.518857 10.0166 0.99301 10.9273 1.80203 10.9273H6.53506V13.4998C6.53506 13.776 6.75892 13.9998 7.03506 13.9998C7.31121 13.9998 7.53506 13.776 7.53506 13.4998V10.9273H12.2031C13.0121 10.9273 13.4863 10.0166 13.0223 9.35379L12.0695 7.99254C11.9251 7.78634 11.835 7.54713 11.8074 7.29695L11.2475 2.22567C11.1076 0.958818 10.0371 0 8.76259 0H5.1783ZM3.68557 2.35243C3.76145 1.5849 4.40702 1 5.1783 1H8.76259C9.52732 1 10.1696 1.57529 10.2535 2.3354L10.8134 7.40668C10.8594 7.82365 11.0097 8.22233 11.2502 8.566L12.2031 9.92725H1.80203L2.74793 8.57598C2.99404 8.22439 3.1455 7.81536 3.18772 7.38828L3.68557 2.35243Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<rect
|
||||
x="0.792893"
|
||||
width="2"
|
||||
height="17"
|
||||
rx="1"
|
||||
transform="rotate(-45 0.792893 0)"
|
||||
fill="currentColor"
|
||||
stroke="white"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@ -45,7 +45,7 @@ const props = defineProps({
|
||||
default: '',
|
||||
},
|
||||
to: {
|
||||
type: String,
|
||||
type: [Object, String],
|
||||
default: '',
|
||||
},
|
||||
isCollapsed: {
|
||||
@ -55,7 +55,11 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
function handleClick() {
|
||||
router.push({ name: props.to })
|
||||
if (typeof props.to === 'object') {
|
||||
router.push(props.to)
|
||||
} else {
|
||||
router.push({ name: props.to })
|
||||
}
|
||||
}
|
||||
|
||||
let isActive = computed(() => {
|
||||
|
||||
@ -63,6 +63,9 @@
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import DuplicateIcon from '@/components/Icons/DuplicateIcon.vue'
|
||||
import PinIcon from '@/components/Icons/PinIcon.vue'
|
||||
import UnpinIcon from '@/components/Icons/UnpinIcon.vue'
|
||||
import ViewModal from '@/components/Modals/ViewModal.vue'
|
||||
import SortBy from '@/components/SortBy.vue'
|
||||
import Filter from '@/components/Filter.vue'
|
||||
@ -71,7 +74,7 @@ import { globalStore } from '@/stores/global'
|
||||
import { viewsStore } from '@/stores/views'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import { createResource, FeatherIcon, Dropdown, call } from 'frappe-ui'
|
||||
import { computed, ref, defineModel, onMounted, watch } from 'vue'
|
||||
import { computed, ref, defineModel, onMounted, watch, h } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
|
||||
const props = defineProps({
|
||||
@ -86,7 +89,7 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const { $dialog } = globalStore()
|
||||
const { getView } = viewsStore()
|
||||
const { reload: reloadView, getView } = viewsStore()
|
||||
|
||||
const list = defineModel()
|
||||
|
||||
@ -111,6 +114,7 @@ const view = ref({
|
||||
columns: '',
|
||||
rows: '',
|
||||
default_columns: false,
|
||||
pinned: false,
|
||||
})
|
||||
|
||||
function getParams() {
|
||||
@ -128,7 +132,9 @@ function getParams() {
|
||||
order_by: _view.order_by,
|
||||
columns: _view.columns,
|
||||
rows: _view.rows,
|
||||
route_name: _view.route_name,
|
||||
default_columns: _view.row,
|
||||
pinned: _view.pinned,
|
||||
}
|
||||
} else {
|
||||
view.value = {
|
||||
@ -138,7 +144,9 @@ function getParams() {
|
||||
order_by: 'modified desc',
|
||||
columns: '',
|
||||
rows: '',
|
||||
route_name: '',
|
||||
default_columns: true,
|
||||
pinned: false,
|
||||
}
|
||||
}
|
||||
|
||||
@ -201,10 +209,19 @@ function setupViews(views) {
|
||||
}
|
||||
})
|
||||
|
||||
if (views.length) {
|
||||
let pinnedViews = views?.filter((v) => v.pinned) || []
|
||||
let savedViews = views?.filter((v) => !v.pinned) || []
|
||||
|
||||
if (savedViews.length) {
|
||||
viewsDropdownOptions.value.push({
|
||||
group: 'Saved Views',
|
||||
items: views,
|
||||
items: savedViews,
|
||||
})
|
||||
}
|
||||
if (pinnedViews.length) {
|
||||
viewsDropdownOptions.value.push({
|
||||
group: 'Pinned Views',
|
||||
items: pinnedViews,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -273,8 +290,8 @@ const viewActions = computed(() => {
|
||||
hideLabel: true,
|
||||
items: [
|
||||
{
|
||||
label: 'Duplicate View',
|
||||
icon: 'copy',
|
||||
label: 'Duplicate',
|
||||
icon: () => h(DuplicateIcon, { class: 'h-4 w-4' }),
|
||||
onClick: () => {
|
||||
view.value.name = ''
|
||||
view.value.label = view.value.label + ' New'
|
||||
@ -286,6 +303,21 @@ const viewActions = computed(() => {
|
||||
]
|
||||
|
||||
if (route.query.view) {
|
||||
o[0].items.push({
|
||||
label: view.value.pinned ? 'Unpin View' : 'Pin View',
|
||||
icon: () =>
|
||||
h(view.value.pinned ? UnpinIcon : PinIcon, { class: 'h-4 w-4' }),
|
||||
onClick: () => {
|
||||
call('crm.fcrm.doctype.crm_view_settings.crm_view_settings.pin', {
|
||||
name: route.query.view,
|
||||
value: !view.value.pinned,
|
||||
}).then(() => {
|
||||
view.value.pinned = !view.value.pinned
|
||||
reloadView()
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
o.push({
|
||||
group: 'Delete View',
|
||||
hideLabel: true,
|
||||
@ -312,6 +344,7 @@ const viewActions = computed(() => {
|
||||
}
|
||||
).then(() => {
|
||||
router.push({ name: route.name })
|
||||
reloadView()
|
||||
})
|
||||
},
|
||||
},
|
||||
@ -337,6 +370,7 @@ function saveView() {
|
||||
order_by: defaultParams.value.order_by,
|
||||
columns: defaultParams.value.columns,
|
||||
rows: defaultParams.value.rows,
|
||||
route_name: route.name,
|
||||
default_columns: view.value.default_columns,
|
||||
}
|
||||
showViewModal.value = true
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { createListResource } from 'frappe-ui'
|
||||
import { reactive } from 'vue'
|
||||
import { reactive, ref } from 'vue'
|
||||
|
||||
export const viewsStore = defineStore('crm-views', () => {
|
||||
let viewsByName = reactive({})
|
||||
let pinnedViews = ref([])
|
||||
|
||||
const views = createListResource({
|
||||
doctype: 'CRM View Settings',
|
||||
@ -12,8 +13,12 @@ export const viewsStore = defineStore('crm-views', () => {
|
||||
initialData: [],
|
||||
auto: true,
|
||||
transform(views) {
|
||||
pinnedViews.value = []
|
||||
for (let view of views) {
|
||||
viewsByName[view.name] = view
|
||||
if (view.pinned) {
|
||||
pinnedViews.value?.push(view)
|
||||
}
|
||||
}
|
||||
return views
|
||||
},
|
||||
@ -27,8 +32,19 @@ export const viewsStore = defineStore('crm-views', () => {
|
||||
return viewsByName[view]
|
||||
}
|
||||
|
||||
function getPinnedViews() {
|
||||
if (!pinnedViews.value?.length) return []
|
||||
return pinnedViews.value
|
||||
}
|
||||
|
||||
function reload() {
|
||||
views.reload()
|
||||
}
|
||||
|
||||
return {
|
||||
views,
|
||||
getPinnedViews,
|
||||
reload,
|
||||
getView,
|
||||
}
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user