Merge pull request #189 from shariquerik/view-icon
feat: Custom View Icons
This commit is contained in:
commit
d9da3dfffa
@ -6,6 +6,7 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"label",
|
"label",
|
||||||
|
"icon",
|
||||||
"user",
|
"user",
|
||||||
"is_default",
|
"is_default",
|
||||||
"column_break_zacm",
|
"column_break_zacm",
|
||||||
@ -111,11 +112,16 @@
|
|||||||
"fieldname": "is_default",
|
"fieldname": "is_default",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Is Default"
|
"label": "Is Default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "icon",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Icon"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-02-03 18:38:09.412745",
|
"modified": "2024-05-20 17:24:18.662389",
|
||||||
"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.icon = view.icon
|
||||||
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 ""
|
||||||
@ -52,6 +53,7 @@ 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.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)
|
||||||
|
|||||||
@ -18,7 +18,7 @@
|
|||||||
<Button
|
<Button
|
||||||
class="rounded-l-none border-l"
|
class="rounded-l-none border-l"
|
||||||
icon="x"
|
icon="x"
|
||||||
@click.stop="clearfilter"
|
@click.stop="clearfilter(false)"
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</template>
|
</template>
|
||||||
@ -423,7 +423,7 @@ function removeFilter(index) {
|
|||||||
function clearfilter(close) {
|
function clearfilter(close) {
|
||||||
filters.value.clear()
|
filters.value.clear()
|
||||||
apply()
|
apply()
|
||||||
close()
|
close && close()
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateValue(value, filter) {
|
function updateValue(value, filter) {
|
||||||
|
|||||||
@ -66,7 +66,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { gemoji } from 'gemoji'
|
import { gemoji } from 'gemoji'
|
||||||
import { Popover } from 'frappe-ui'
|
import { Popover } from 'frappe-ui'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const emoji = defineModel()
|
const emoji = defineModel()
|
||||||
@ -109,5 +109,9 @@ function randomInt(min, max) {
|
|||||||
return Math.floor(Math.random() * (max - min + 1) + min)
|
return Math.floor(Math.random() * (max - min + 1) + min)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!emoji.value) setRandom()
|
||||||
|
})
|
||||||
|
|
||||||
defineExpose({ setRandom })
|
defineExpose({ setRandom })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -115,7 +115,7 @@ import { viewsStore } from '@/stores/views'
|
|||||||
import { notificationsStore } from '@/stores/notifications'
|
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 } from 'vue'
|
import { computed, h } from 'vue'
|
||||||
|
|
||||||
const { getPinnedViews, getPublicViews } = viewsStore()
|
const { getPinnedViews, getPublicViews } = viewsStore()
|
||||||
const { toggle: toggleNotificationPanel } = notificationsStore()
|
const { toggle: toggleNotificationPanel } = notificationsStore()
|
||||||
@ -196,7 +196,7 @@ function parseView(views) {
|
|||||||
return views.map((view) => {
|
return views.map((view) => {
|
||||||
return {
|
return {
|
||||||
label: view.label,
|
label: view.label,
|
||||||
icon: getIcon(view.route_name),
|
icon: getIcon(view.route_name, view.icon),
|
||||||
to: {
|
to: {
|
||||||
name: view.route_name,
|
name: view.route_name,
|
||||||
query: { view: view.name },
|
query: { view: view.name },
|
||||||
@ -205,7 +205,9 @@ function parseView(views) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function getIcon(routeName) {
|
function getIcon(routeName, icon) {
|
||||||
|
if (icon) return h('div', { class: 'size-auto' }, icon)
|
||||||
|
|
||||||
switch (routeName) {
|
switch (routeName) {
|
||||||
case 'Leads':
|
case 'Leads':
|
||||||
return LeadsIcon
|
return LeadsIcon
|
||||||
|
|||||||
@ -21,20 +21,35 @@
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<template #body-content>
|
<template #body-content>
|
||||||
<FormControl
|
<div class="mb-1.5 block text-base text-gray-600">
|
||||||
variant="outline"
|
{{ __('View Name') }}
|
||||||
size="md"
|
</div>
|
||||||
type="text"
|
<div class="flex gap-2">
|
||||||
:label="__('View Name')"
|
<IconPicker v-model="view.icon" v-slot="{ togglePopover }">
|
||||||
:placeholder="__('My Open Deals')"
|
<Button
|
||||||
v-model="view.label"
|
variant="outline"
|
||||||
/>
|
size="md"
|
||||||
|
class="flex size-8 text-2xl leading-none"
|
||||||
|
:label="view.icon"
|
||||||
|
@click="togglePopover"
|
||||||
|
/>
|
||||||
|
</IconPicker>
|
||||||
|
<TextInput
|
||||||
|
class="flex-1"
|
||||||
|
variant="outline"
|
||||||
|
size="md"
|
||||||
|
type="text"
|
||||||
|
:placeholder="__('My Open Deals')"
|
||||||
|
v-model="view.label"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { call } from 'frappe-ui'
|
import IconPicker from '@/components/IconPicker.vue'
|
||||||
|
import { call, TextInput } from 'frappe-ui'
|
||||||
import { ref, watch, nextTick } from 'vue'
|
import { ref, watch, nextTick } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -60,6 +75,7 @@ const duplicateMode = ref(false)
|
|||||||
const _view = ref({
|
const _view = ref({
|
||||||
name: '',
|
name: '',
|
||||||
label: '',
|
label: '',
|
||||||
|
icon: '',
|
||||||
filters: {},
|
filters: {},
|
||||||
order_by: 'modified desc',
|
order_by: 'modified desc',
|
||||||
columns: '',
|
columns: '',
|
||||||
|
|||||||
@ -11,13 +11,13 @@
|
|||||||
<div class="flex items-center truncate">
|
<div class="flex items-center truncate">
|
||||||
<Tooltip :text="label" placement="right" :disabled="!isCollapsed">
|
<Tooltip :text="label" placement="right" :disabled="!isCollapsed">
|
||||||
<slot name="icon">
|
<slot name="icon">
|
||||||
<span class="grid h-4.5 w-4.5 flex-shrink-0 place-items-center">
|
<span class="grid flex-shrink-0 place-items-center">
|
||||||
<FeatherIcon
|
<FeatherIcon
|
||||||
v-if="typeof icon == 'string'"
|
v-if="typeof icon == 'string'"
|
||||||
:name="icon"
|
:name="icon"
|
||||||
class="h-4.5 w-4.5 text-gray-700"
|
class="size-4.5 text-gray-700"
|
||||||
/>
|
/>
|
||||||
<component v-else :is="icon" class="h-4.5 w-4.5 text-gray-700" />
|
<component v-else :is="icon" class="size-4.5 text-gray-700" />
|
||||||
</span>
|
</span>
|
||||||
</slot>
|
</slot>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@ -5,7 +5,8 @@
|
|||||||
<template #default="{ open }">
|
<template #default="{ open }">
|
||||||
<Button :label="__(currentView.label)">
|
<Button :label="__(currentView.label)">
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<FeatherIcon :name="currentView.icon" class="h-4" />
|
<div v-if="isEmoji(currentView.icon)">{{ currentView.icon }}</div>
|
||||||
|
<FeatherIcon v-else :name="currentView.icon" class="h-4" />
|
||||||
</template>
|
</template>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
<FeatherIcon
|
<FeatherIcon
|
||||||
@ -126,6 +127,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ViewModal
|
<ViewModal
|
||||||
|
v-model="showViewModal"
|
||||||
|
v-model:view="viewModalObj"
|
||||||
:doctype="doctype"
|
:doctype="doctype"
|
||||||
:options="{
|
:options="{
|
||||||
afterCreate: async (v) => {
|
afterCreate: async (v) => {
|
||||||
@ -138,8 +141,6 @@
|
|||||||
reloadView()
|
reloadView()
|
||||||
},
|
},
|
||||||
}"
|
}"
|
||||||
v-model:view="view"
|
|
||||||
v-model="showViewModal"
|
|
||||||
/>
|
/>
|
||||||
<Dialog
|
<Dialog
|
||||||
v-model="showExportDialog"
|
v-model="showExportDialog"
|
||||||
@ -199,6 +200,7 @@ import ColumnSettings from '@/components/ColumnSettings.vue'
|
|||||||
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 { 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 } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
@ -250,6 +252,7 @@ const currentView = computed(() => {
|
|||||||
const view = ref({
|
const view = ref({
|
||||||
name: '',
|
name: '',
|
||||||
label: '',
|
label: '',
|
||||||
|
icon: '',
|
||||||
filters: {},
|
filters: {},
|
||||||
order_by: 'modified desc',
|
order_by: 'modified desc',
|
||||||
columns: '',
|
columns: '',
|
||||||
@ -288,6 +291,7 @@ function getParams() {
|
|||||||
view.value = {
|
view.value = {
|
||||||
name: _view.name,
|
name: _view.name,
|
||||||
label: _view.label,
|
label: _view.label,
|
||||||
|
icon: _view.icon,
|
||||||
filters: _view.filters,
|
filters: _view.filters,
|
||||||
order_by: _view.order_by,
|
order_by: _view.order_by,
|
||||||
columns: _view.columns,
|
columns: _view.columns,
|
||||||
@ -301,6 +305,7 @@ function getParams() {
|
|||||||
view.value = {
|
view.value = {
|
||||||
name: '',
|
name: '',
|
||||||
label: '',
|
label: '',
|
||||||
|
icon: '',
|
||||||
filters: {},
|
filters: {},
|
||||||
order_by: 'modified desc',
|
order_by: 'modified desc',
|
||||||
columns: '',
|
columns: '',
|
||||||
@ -391,6 +396,14 @@ const defaultViews = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
function getIcon(icon) {
|
||||||
|
if (isEmoji(icon)) {
|
||||||
|
return h('div', icon)
|
||||||
|
} else {
|
||||||
|
return icon || 'list'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const viewsDropdownOptions = computed(() => {
|
const viewsDropdownOptions = computed(() => {
|
||||||
let _views = [
|
let _views = [
|
||||||
{
|
{
|
||||||
@ -403,7 +416,7 @@ 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 = view.icon || 'list'
|
view.icon = getIcon(view.icon)
|
||||||
view.filters =
|
view.filters =
|
||||||
typeof view.filters == 'string'
|
typeof view.filters == 'string'
|
||||||
? JSON.parse(view.filters)
|
? JSON.parse(view.filters)
|
||||||
@ -561,6 +574,7 @@ function create_or_update_default_view() {
|
|||||||
reloadView()
|
reloadView()
|
||||||
view.value = {
|
view.value = {
|
||||||
label: view.value.label,
|
label: view.value.label,
|
||||||
|
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,
|
||||||
@ -610,9 +624,9 @@ const viewActions = computed(() => {
|
|||||||
|
|
||||||
if (route.query.view && (!view.value.public || isManager())) {
|
if (route.query.view && (!view.value.public || isManager())) {
|
||||||
actions[0].items.push({
|
actions[0].items.push({
|
||||||
label: __('Rename'),
|
label: __('Edit'),
|
||||||
icon: () => h(EditIcon, { class: 'h-4 w-4' }),
|
icon: () => h(EditIcon, { class: 'h-4 w-4' }),
|
||||||
onClick: () => renameView(),
|
onClick: () => editView(),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!view.value.public) {
|
if (!view.value.public) {
|
||||||
@ -664,16 +678,22 @@ const viewActions = computed(() => {
|
|||||||
return actions
|
return actions
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const viewModalObj = ref({})
|
||||||
|
|
||||||
function duplicateView() {
|
function duplicateView() {
|
||||||
let label = __(getView(route.query.view)?.label) || __('List View')
|
let label = __(getView(route.query.view)?.label) || __('List View')
|
||||||
view.value.name = ''
|
view.value.name = ''
|
||||||
view.value.label = label + __(' (New)')
|
view.value.label = label + __(' (New)')
|
||||||
|
viewModalObj.value = view.value
|
||||||
showViewModal.value = true
|
showViewModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function renameView() {
|
function editView() {
|
||||||
|
let cView = getView(route.query.view)
|
||||||
view.value.name = route.query.view
|
view.value.name = route.query.view
|
||||||
view.value.label = __(getView(route.query.view).label)
|
view.value.label = __(cView?.label) || __('List View')
|
||||||
|
view.value.icon = cView?.icon || ''
|
||||||
|
viewModalObj.value = view.value
|
||||||
showViewModal.value = true
|
showViewModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -715,6 +735,7 @@ function cancelChanges() {
|
|||||||
function saveView() {
|
function saveView() {
|
||||||
view.value = {
|
view.value = {
|
||||||
label: view.value.label,
|
label: view.value.label,
|
||||||
|
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,
|
||||||
@ -723,6 +744,7 @@ function saveView() {
|
|||||||
route_name: route.name,
|
route_name: route.name,
|
||||||
load_default_columns: view.value.load_default_columns,
|
load_default_columns: view.value.load_default_columns,
|
||||||
}
|
}
|
||||||
|
viewModalObj.value = view.value
|
||||||
showViewModal.value = true
|
showViewModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import TaskStatusIcon from '@/components/Icons/TaskStatusIcon.vue'
|
|||||||
import TaskPriorityIcon from '@/components/Icons/TaskPriorityIcon.vue'
|
import TaskPriorityIcon from '@/components/Icons/TaskPriorityIcon.vue'
|
||||||
import { useDateFormat, useTimeAgo } from '@vueuse/core'
|
import { useDateFormat, useTimeAgo } from '@vueuse/core'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
|
import { gemoji } from 'gemoji'
|
||||||
import { toast } from 'frappe-ui'
|
import { toast } from 'frappe-ui'
|
||||||
import { h } from 'vue'
|
import { h } from 'vue'
|
||||||
|
|
||||||
@ -169,3 +170,8 @@ export function copyToClipboard(text) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isEmoji(str) {
|
||||||
|
const emojiList = gemoji.map((emoji) => emoji.emoji)
|
||||||
|
return emojiList.includes(str)
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user