Merge pull request #190 from frappe/develop
chore: Merge develop to main
This commit is contained in:
commit
8a7f5cdaa8
@ -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)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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