1
0
forked from test/crm

Merge pull request #189 from shariquerik/view-icon

feat: Custom View Icons
This commit is contained in:
Shariq Ansari 2024-05-20 19:53:00 +05:30 committed by GitHub
commit d9da3dfffa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 85 additions and 27 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: '',

View File

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

View File

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

View File

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