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",
|
||||
"field_order": [
|
||||
"label",
|
||||
"icon",
|
||||
"user",
|
||||
"is_default",
|
||||
"column_break_zacm",
|
||||
@ -111,11 +112,16 @@
|
||||
"fieldname": "is_default",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Default"
|
||||
},
|
||||
{
|
||||
"fieldname": "icon",
|
||||
"fieldtype": "Data",
|
||||
"label": "Icon"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-03 18:38:09.412745",
|
||||
"modified": "2024-05-20 17:24:18.662389",
|
||||
"modified_by": "Administrator",
|
||||
"module": "FCRM",
|
||||
"name": "CRM View Settings",
|
||||
|
||||
@ -27,6 +27,7 @@ def create(view):
|
||||
doc = frappe.new_doc("CRM View Settings")
|
||||
doc.name = view.label
|
||||
doc.label = view.label
|
||||
doc.icon = view.icon
|
||||
doc.dt = view.doctype
|
||||
doc.user = frappe.session.user
|
||||
doc.route_name = view.route_name or ""
|
||||
@ -52,6 +53,7 @@ def update(view):
|
||||
|
||||
doc = frappe.get_doc("CRM View Settings", view.name)
|
||||
doc.label = view.label
|
||||
doc.icon = view.icon
|
||||
doc.route_name = view.route_name or ""
|
||||
doc.load_default_columns = view.load_default_columns or False
|
||||
doc.filters = json.dumps(filters)
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
<Button
|
||||
class="rounded-l-none border-l"
|
||||
icon="x"
|
||||
@click.stop="clearfilter"
|
||||
@click.stop="clearfilter(false)"
|
||||
/>
|
||||
</Tooltip>
|
||||
</template>
|
||||
@ -423,7 +423,7 @@ function removeFilter(index) {
|
||||
function clearfilter(close) {
|
||||
filters.value.clear()
|
||||
apply()
|
||||
close()
|
||||
close && close()
|
||||
}
|
||||
|
||||
function updateValue(value, filter) {
|
||||
|
||||
@ -66,7 +66,7 @@
|
||||
<script setup>
|
||||
import { gemoji } from 'gemoji'
|
||||
import { Popover } from 'frappe-ui'
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
|
||||
const search = ref('')
|
||||
const emoji = defineModel()
|
||||
@ -109,5 +109,9 @@ function randomInt(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1) + min)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!emoji.value) setRandom()
|
||||
})
|
||||
|
||||
defineExpose({ setRandom })
|
||||
</script>
|
||||
|
||||
@ -115,7 +115,7 @@ import { viewsStore } from '@/stores/views'
|
||||
import { notificationsStore } from '@/stores/notifications'
|
||||
import { FeatherIcon } from 'frappe-ui'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
import { computed, h } from 'vue'
|
||||
|
||||
const { getPinnedViews, getPublicViews } = viewsStore()
|
||||
const { toggle: toggleNotificationPanel } = notificationsStore()
|
||||
@ -196,7 +196,7 @@ function parseView(views) {
|
||||
return views.map((view) => {
|
||||
return {
|
||||
label: view.label,
|
||||
icon: getIcon(view.route_name),
|
||||
icon: getIcon(view.route_name, view.icon),
|
||||
to: {
|
||||
name: view.route_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) {
|
||||
case 'Leads':
|
||||
return LeadsIcon
|
||||
|
||||
@ -21,20 +21,35 @@
|
||||
}"
|
||||
>
|
||||
<template #body-content>
|
||||
<FormControl
|
||||
variant="outline"
|
||||
size="md"
|
||||
type="text"
|
||||
:label="__('View Name')"
|
||||
:placeholder="__('My Open Deals')"
|
||||
v-model="view.label"
|
||||
/>
|
||||
<div class="mb-1.5 block text-base text-gray-600">
|
||||
{{ __('View Name') }}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<IconPicker v-model="view.icon" v-slot="{ togglePopover }">
|
||||
<Button
|
||||
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>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<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'
|
||||
|
||||
const props = defineProps({
|
||||
@ -60,6 +75,7 @@ const duplicateMode = ref(false)
|
||||
const _view = ref({
|
||||
name: '',
|
||||
label: '',
|
||||
icon: '',
|
||||
filters: {},
|
||||
order_by: 'modified desc',
|
||||
columns: '',
|
||||
|
||||
@ -11,13 +11,13 @@
|
||||
<div class="flex items-center truncate">
|
||||
<Tooltip :text="label" placement="right" :disabled="!isCollapsed">
|
||||
<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
|
||||
v-if="typeof icon == 'string'"
|
||||
: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>
|
||||
</slot>
|
||||
</Tooltip>
|
||||
|
||||
@ -5,7 +5,8 @@
|
||||
<template #default="{ open }">
|
||||
<Button :label="__(currentView.label)">
|
||||
<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 #suffix>
|
||||
<FeatherIcon
|
||||
@ -126,6 +127,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<ViewModal
|
||||
v-model="showViewModal"
|
||||
v-model:view="viewModalObj"
|
||||
:doctype="doctype"
|
||||
:options="{
|
||||
afterCreate: async (v) => {
|
||||
@ -138,8 +141,6 @@
|
||||
reloadView()
|
||||
},
|
||||
}"
|
||||
v-model:view="view"
|
||||
v-model="showViewModal"
|
||||
/>
|
||||
<Dialog
|
||||
v-model="showExportDialog"
|
||||
@ -199,6 +200,7 @@ import ColumnSettings from '@/components/ColumnSettings.vue'
|
||||
import { globalStore } from '@/stores/global'
|
||||
import { viewsStore } from '@/stores/views'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { isEmoji } from '@/utils'
|
||||
import { createResource, Dropdown, call, FeatherIcon } from 'frappe-ui'
|
||||
import { computed, ref, onMounted, watch, h } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
@ -250,6 +252,7 @@ const currentView = computed(() => {
|
||||
const view = ref({
|
||||
name: '',
|
||||
label: '',
|
||||
icon: '',
|
||||
filters: {},
|
||||
order_by: 'modified desc',
|
||||
columns: '',
|
||||
@ -288,6 +291,7 @@ function getParams() {
|
||||
view.value = {
|
||||
name: _view.name,
|
||||
label: _view.label,
|
||||
icon: _view.icon,
|
||||
filters: _view.filters,
|
||||
order_by: _view.order_by,
|
||||
columns: _view.columns,
|
||||
@ -301,6 +305,7 @@ function getParams() {
|
||||
view.value = {
|
||||
name: '',
|
||||
label: '',
|
||||
icon: '',
|
||||
filters: {},
|
||||
order_by: 'modified desc',
|
||||
columns: '',
|
||||
@ -391,6 +396,14 @@ const defaultViews = [
|
||||
},
|
||||
]
|
||||
|
||||
function getIcon(icon) {
|
||||
if (isEmoji(icon)) {
|
||||
return h('div', icon)
|
||||
} else {
|
||||
return icon || 'list'
|
||||
}
|
||||
}
|
||||
|
||||
const viewsDropdownOptions = computed(() => {
|
||||
let _views = [
|
||||
{
|
||||
@ -403,7 +416,7 @@ const viewsDropdownOptions = computed(() => {
|
||||
if (list.value?.data?.views) {
|
||||
list.value.data.views.forEach((view) => {
|
||||
view.label = __(view.label)
|
||||
view.icon = view.icon || 'list'
|
||||
view.icon = getIcon(view.icon)
|
||||
view.filters =
|
||||
typeof view.filters == 'string'
|
||||
? JSON.parse(view.filters)
|
||||
@ -561,6 +574,7 @@ function create_or_update_default_view() {
|
||||
reloadView()
|
||||
view.value = {
|
||||
label: view.value.label,
|
||||
icon: view.value.icon,
|
||||
name: view.value.name,
|
||||
filters: defaultParams.value.filters,
|
||||
order_by: defaultParams.value.order_by,
|
||||
@ -610,9 +624,9 @@ const viewActions = computed(() => {
|
||||
|
||||
if (route.query.view && (!view.value.public || isManager())) {
|
||||
actions[0].items.push({
|
||||
label: __('Rename'),
|
||||
label: __('Edit'),
|
||||
icon: () => h(EditIcon, { class: 'h-4 w-4' }),
|
||||
onClick: () => renameView(),
|
||||
onClick: () => editView(),
|
||||
})
|
||||
|
||||
if (!view.value.public) {
|
||||
@ -664,16 +678,22 @@ const viewActions = computed(() => {
|
||||
return actions
|
||||
})
|
||||
|
||||
const viewModalObj = ref({})
|
||||
|
||||
function duplicateView() {
|
||||
let label = __(getView(route.query.view)?.label) || __('List View')
|
||||
view.value.name = ''
|
||||
view.value.label = label + __(' (New)')
|
||||
viewModalObj.value = view.value
|
||||
showViewModal.value = true
|
||||
}
|
||||
|
||||
function renameView() {
|
||||
function editView() {
|
||||
let cView = getView(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
|
||||
}
|
||||
|
||||
@ -715,6 +735,7 @@ function cancelChanges() {
|
||||
function saveView() {
|
||||
view.value = {
|
||||
label: view.value.label,
|
||||
icon: view.value.icon,
|
||||
name: view.value.name,
|
||||
filters: defaultParams.value.filters,
|
||||
order_by: defaultParams.value.order_by,
|
||||
@ -723,6 +744,7 @@ function saveView() {
|
||||
route_name: route.name,
|
||||
load_default_columns: view.value.load_default_columns,
|
||||
}
|
||||
viewModalObj.value = view.value
|
||||
showViewModal.value = true
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import TaskStatusIcon from '@/components/Icons/TaskStatusIcon.vue'
|
||||
import TaskPriorityIcon from '@/components/Icons/TaskPriorityIcon.vue'
|
||||
import { useDateFormat, useTimeAgo } from '@vueuse/core'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { gemoji } from 'gemoji'
|
||||
import { toast } from 'frappe-ui'
|
||||
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