Merge pull request #47 from shariquerik/pinned-views

feat: Pinned Views
This commit is contained in:
Shariq Ansari 2024-01-01 19:34:24 +05:30 committed by GitHub
commit 6cabe46852
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 202 additions and 11 deletions

View File

@ -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",

View File

@ -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))

View File

@ -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>

View 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>

View 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>

View 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>

View File

@ -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(() => {

View File

@ -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

View File

@ -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,
}
})