Merge pull request #190 from frappe/develop

chore: Merge develop to main
This commit is contained in:
Shariq Ansari 2024-05-20 19:53:43 +05:30 committed by GitHub
commit 8a7f5cdaa8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 145 additions and 46 deletions

View File

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

View File

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

View File

@ -1,29 +1,75 @@
<template>
<div ref="scrollableDiv" class="scrr" :style="`maskImage: ${maskStyle}`">
<div
ref="scrollableDiv"
:style="`maskImage: ${maskStyle}`"
@scroll="updateMaskStyle"
>
<slot></slot>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
const props = defineProps({
maskHeight: {
maskLength: {
type: Number,
default: 20,
default: 30,
},
orientation: {
type: String,
default: 'vertical',
},
})
const scrollableDiv = ref(null)
const maskStyle = ref('none')
const side = computed(() =>
props.orientation == 'horizontal' ? 'right' : 'bottom'
)
function setMaskStyle() {
// show mask only if div is scrollable
if (scrollableDiv.value.scrollHeight > scrollableDiv.value.clientHeight) {
maskStyle.value = `linear-gradient(to bottom, black calc(100% - ${props.maskHeight}px), transparent 100%);`
} else {
function updateMaskStyle() {
if (!scrollableDiv.value) return
let scrollWidth = scrollableDiv.value.scrollWidth
let clientWidth = scrollableDiv.value.clientWidth
let scrollHeight = scrollableDiv.value.scrollHeight
let clientHeight = scrollableDiv.value.clientHeight
let scrollTop = scrollableDiv.value.scrollTop
let scrollLeft = scrollableDiv.value.scrollLeft
maskStyle.value = 'none'
// faded on both sides
if (
(side.value == 'right' && scrollWidth > clientWidth) ||
(side.value == 'bottom' && scrollHeight > clientHeight)
) {
maskStyle.value = `linear-gradient(to ${side.value}, transparent, black ${props.maskLength}px, black calc(100% - ${props.maskLength}px), transparent);`
}
// faded on left or top
if (
(side.value == 'right' && scrollLeft - 20 > clientWidth) ||
(side.value == 'bottom' && scrollTop + clientHeight >= scrollHeight)
) {
maskStyle.value = `linear-gradient(to ${side.value}, transparent, black ${props.maskLength}px, black 100%, transparent);`
}
// faded on right or bottom
if (
(side.value == 'right' && scrollLeft == 0) ||
(side.value == 'bottom' && scrollTop == 0)
) {
maskStyle.value = `linear-gradient(to ${side.value}, black calc(100% - ${props.maskLength}px), transparent 100%);`
}
if (
(side.value == 'right' && clientWidth == scrollWidth) ||
(side.value == 'bottom' && clientHeight == scrollHeight)
) {
maskStyle.value = 'none'
}
}
onMounted(() => setMaskStyle())
onMounted(() => setTimeout(() => updateMaskStyle(), 300))
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
@ -23,15 +24,9 @@
</Dropdown>
</div>
<div class="-mr-2 h-[70%] border-l" />
<div
<FadedScrollableDiv
class="flex flex-1 items-center overflow-x-auto px-1"
style="
mask-image: linear-gradient(
to right,
black calc(100% - 20px),
transparent 100%
);
"
orientation="horizontal"
>
<div
v-for="filter in quickFilterList"
@ -78,7 +73,7 @@
@change.stop="applyQuickFilter(filter, $event.target.value)"
/>
</div>
</div>
</FadedScrollableDiv>
<div class="-ml-2 h-[70%] border-l" />
<div class="flex items-center gap-2">
<div
@ -132,6 +127,8 @@
</div>
</div>
<ViewModal
v-model="showViewModal"
v-model:view="viewModalObj"
:doctype="doctype"
:options="{
afterCreate: async (v) => {
@ -144,8 +141,6 @@
reloadView()
},
}"
v-model:view="view"
v-model="showViewModal"
/>
<Dialog
v-model="showExportDialog"
@ -200,10 +195,12 @@ 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'
import FadedScrollableDiv from '@/components/FadedScrollableDiv.vue'
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'
@ -255,6 +252,7 @@ const currentView = computed(() => {
const view = ref({
name: '',
label: '',
icon: '',
filters: {},
order_by: 'modified desc',
columns: '',
@ -293,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,
@ -306,6 +305,7 @@ function getParams() {
view.value = {
name: '',
label: '',
icon: '',
filters: {},
order_by: 'modified desc',
columns: '',
@ -396,6 +396,14 @@ const defaultViews = [
},
]
function getIcon(icon) {
if (isEmoji(icon)) {
return h('div', icon)
} else {
return icon || 'list'
}
}
const viewsDropdownOptions = computed(() => {
let _views = [
{
@ -408,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)
@ -566,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,
@ -615,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) {
@ -669,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
}
@ -720,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,
@ -728,6 +744,7 @@ function saveView() {
route_name: route.name,
load_default_columns: view.value.load_default_columns,
}
viewModalObj.value = view.value
showViewModal.value = true
}

View File

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