Merge pull request #319 from shariquerik/view-selector
fix: Show view selector dropdown in breadcrumb
This commit is contained in:
commit
0edde767fd
21
frontend/src/components/Icon.vue
Normal file
21
frontend/src/components/Icon.vue
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="isEmoji(icon)" v-bind="$attrs">
|
||||||
|
{{ icon }}
|
||||||
|
</div>
|
||||||
|
<FeatherIcon
|
||||||
|
v-else-if="typeof icon == 'string'"
|
||||||
|
:name="icon"
|
||||||
|
v-bind="$attrs"
|
||||||
|
/>
|
||||||
|
<component v-else :is="icon" v-bind="$attrs" />
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { isEmoji } from '@/utils'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
icon: {
|
||||||
|
type: [String, Object],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
16
frontend/src/components/Icons/GroupByIcon.vue
Normal file
16
frontend/src/components/Icons/GroupByIcon.vue
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M1.64645 1.89645C1.84171 1.70118 2.15829 1.70118 2.35355 1.89645L4.35355 3.89645C4.54882 4.09171 4.54882 4.40829 4.35355 4.60355L2.35355 6.60355C2.15829 6.79882 1.84171 6.79882 1.64645 6.60355C1.45118 6.40829 1.45118 6.09171 1.64645 5.89645L3.29289 4.25L1.64645 2.60355C1.45118 2.40829 1.45118 2.09171 1.64645 1.89645ZM5.5 3.2002C5.5 2.92405 5.72386 2.7002 6 2.7002H14C14.2761 2.7002 14.5 2.92405 14.5 3.2002C14.5 3.47634 14.2761 3.7002 14 3.7002H6C5.72386 3.7002 5.5 3.47634 5.5 3.2002ZM5.5 8.00024C5.5 7.7241 5.72386 7.50024 6 7.50024H14C14.2761 7.50024 14.5 7.7241 14.5 8.00024C14.5 8.27639 14.2761 8.50024 14 8.50024H6C5.72386 8.50024 5.5 8.27639 5.5 8.00024ZM6 12.3003C5.72386 12.3003 5.5 12.5242 5.5 12.8003C5.5 13.0764 5.72386 13.3003 6 13.3003H14C14.2761 13.3003 14.5 13.0764 14.5 12.8003C14.5 12.5242 14.2761 12.3003 14 12.3003H6ZM2.35355 9.39645C2.15829 9.20118 1.84171 9.20118 1.64645 9.39645C1.45118 9.59171 1.45118 9.90829 1.64645 10.1036L3.29289 11.75L1.64645 13.3964C1.45118 13.5917 1.45118 13.9083 1.64645 14.1036C1.84171 14.2988 2.15829 14.2988 2.35355 14.1036L4.35355 12.1036C4.54882 11.9083 4.54882 11.5917 4.35355 11.3964L2.35355 9.39645Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
@ -1,18 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="16"
|
width="16"
|
||||||
height="16"
|
height="16"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 16 16"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
stroke-width="1.5"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="lucide lucide-kanban"
|
|
||||||
>
|
>
|
||||||
<path d="M6 5v11" />
|
<path
|
||||||
<path d="M12 5v6" />
|
fill-rule="evenodd"
|
||||||
<path d="M18 5v14" />
|
clip-rule="evenodd"
|
||||||
|
d="M3.69971 3.00098C3.69971 2.72483 3.47585 2.50098 3.19971 2.50098C2.92356 2.50098 2.69971 2.72483 2.69971 3.00098V11.001C2.69971 11.2771 2.92356 11.501 3.19971 11.501C3.47585 11.501 3.69971 11.2771 3.69971 11.001V3.00098ZM8 2.50098C8.27614 2.50098 8.5 2.72483 8.5 3.00098V13.001C8.5 13.2771 8.27614 13.501 8 13.501C7.72386 13.501 7.5 13.2771 7.5 13.001V3.00098C7.5 2.72483 7.72386 2.50098 8 2.50098ZM12.7998 2.50098C13.0759 2.50098 13.2998 2.72483 13.2998 3.00098V8.00098C13.2998 8.27712 13.0759 8.50098 12.7998 8.50098C12.5237 8.50098 12.2998 8.27712 12.2998 8.00098V3.00098C12.2998 2.72483 12.5237 2.50098 12.7998 2.50098Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
16
frontend/src/components/Icons/ListIcon.vue
Normal file
16
frontend/src/components/Icons/ListIcon.vue
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M2.5 3.2002C2.5 2.92405 2.72386 2.7002 3 2.7002H13C13.2761 2.7002 13.5 2.92405 13.5 3.2002C13.5 3.47634 13.2761 3.7002 13 3.7002H3C2.72386 3.7002 2.5 3.47634 2.5 3.2002ZM2.5 8.00024C2.5 7.7241 2.72386 7.50024 3 7.50024H13C13.2761 7.50024 13.5 7.7241 13.5 8.00024C13.5 8.27639 13.2761 8.50024 13 8.50024H3C2.72386 8.50024 2.5 8.27639 2.5 8.00024ZM3 12.3003C2.72386 12.3003 2.5 12.5242 2.5 12.8003C2.5 13.0764 2.72386 13.3003 3 13.3003H13C13.2761 13.3003 13.5 13.0764 13.5 12.8003C13.5 12.5242 13.2761 12.3003 13 12.3003H3Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<Teleport to="#app-header" v-if="showHeader">
|
<Teleport to="#app-header" v-if="showHeader">
|
||||||
<slot>
|
<slot>
|
||||||
<header class="flex h-12 items-center justify-between py-2.5 pl-5">
|
<header class="flex h-10.5 items-center justify-between py-[7px] pl-5">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<slot name="left-header" />
|
<slot name="left-header" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
getRowRoute: (row) => ({
|
getRowRoute: (row) => ({
|
||||||
name: 'Contact',
|
name: 'Contact',
|
||||||
params: { contactId: row.name },
|
params: { contactId: row.name },
|
||||||
|
query: { view: route.query.view, viewType: route.params.viewType },
|
||||||
}),
|
}),
|
||||||
selectable: options.selectable,
|
selectable: options.selectable,
|
||||||
showTooltip: options.showTooltip,
|
showTooltip: options.showTooltip,
|
||||||
@ -174,6 +175,7 @@ import {
|
|||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { sessionStore } from '@/stores/session'
|
import { sessionStore } from '@/stores/session'
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
rows: {
|
rows: {
|
||||||
@ -205,6 +207,8 @@ const emit = defineEmits([
|
|||||||
'likeDoc',
|
'likeDoc',
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
const pageLengthCount = defineModel()
|
const pageLengthCount = defineModel()
|
||||||
const list = defineModel('list')
|
const list = defineModel('list')
|
||||||
|
|
||||||
@ -230,7 +234,7 @@ const listBulkActionsRef = ref(null)
|
|||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
customListActions: computed(
|
customListActions: computed(
|
||||||
() => listBulkActionsRef.value?.customListActions
|
() => listBulkActionsRef.value?.customListActions,
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -4,14 +4,21 @@
|
|||||||
:columns="columns"
|
:columns="columns"
|
||||||
:rows="rows"
|
:rows="rows"
|
||||||
:options="{
|
:options="{
|
||||||
getRowRoute: (row) => ({ name: 'Deal', params: { dealId: row.name } }),
|
getRowRoute: (row) => ({
|
||||||
|
name: 'Deal',
|
||||||
|
params: { dealId: row.name },
|
||||||
|
query: { view: route.query.view, viewType: route.params.viewType },
|
||||||
|
}),
|
||||||
selectable: options.selectable,
|
selectable: options.selectable,
|
||||||
showTooltip: options.showTooltip,
|
showTooltip: options.showTooltip,
|
||||||
resizeColumn: options.resizeColumn,
|
resizeColumn: options.resizeColumn,
|
||||||
}"
|
}"
|
||||||
row-key="name"
|
row-key="name"
|
||||||
>
|
>
|
||||||
<ListHeader class="sm:mx-5 mx-3" @columnWidthUpdated="emit('columnWidthUpdated')">
|
<ListHeader
|
||||||
|
class="sm:mx-5 mx-3"
|
||||||
|
@columnWidthUpdated="emit('columnWidthUpdated')"
|
||||||
|
>
|
||||||
<ListHeaderItem
|
<ListHeaderItem
|
||||||
v-for="column in columns"
|
v-for="column in columns"
|
||||||
:key="column.key"
|
:key="column.key"
|
||||||
@ -204,6 +211,7 @@ import {
|
|||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { sessionStore } from '@/stores/session'
|
import { sessionStore } from '@/stores/session'
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
rows: {
|
rows: {
|
||||||
@ -235,6 +243,8 @@ const emit = defineEmits([
|
|||||||
'likeDoc',
|
'likeDoc',
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
const pageLengthCount = defineModel()
|
const pageLengthCount = defineModel()
|
||||||
const list = defineModel('list')
|
const list = defineModel('list')
|
||||||
|
|
||||||
@ -260,7 +270,7 @@ const listBulkActionsRef = ref(null)
|
|||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
customListActions: computed(
|
customListActions: computed(
|
||||||
() => listBulkActionsRef.value?.customListActions
|
() => listBulkActionsRef.value?.customListActions,
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -4,14 +4,21 @@
|
|||||||
:columns="columns"
|
:columns="columns"
|
||||||
:rows="rows"
|
:rows="rows"
|
||||||
:options="{
|
:options="{
|
||||||
getRowRoute: (row) => ({ name: 'Lead', params: { leadId: row.name } }),
|
getRowRoute: (row) => ({
|
||||||
|
name: 'Lead',
|
||||||
|
params: { leadId: row.name },
|
||||||
|
query: { view: route.query.view, viewType: route.params.viewType },
|
||||||
|
}),
|
||||||
selectable: options.selectable,
|
selectable: options.selectable,
|
||||||
showTooltip: options.showTooltip,
|
showTooltip: options.showTooltip,
|
||||||
resizeColumn: options.resizeColumn,
|
resizeColumn: options.resizeColumn,
|
||||||
}"
|
}"
|
||||||
row-key="name"
|
row-key="name"
|
||||||
>
|
>
|
||||||
<ListHeader class="sm:mx-5 mx-3" @columnWidthUpdated="emit('columnWidthUpdated')">
|
<ListHeader
|
||||||
|
class="sm:mx-5 mx-3"
|
||||||
|
@columnWidthUpdated="emit('columnWidthUpdated')"
|
||||||
|
>
|
||||||
<ListHeaderItem
|
<ListHeaderItem
|
||||||
v-for="column in columns"
|
v-for="column in columns"
|
||||||
:key="column.key"
|
:key="column.key"
|
||||||
@ -217,6 +224,7 @@ import {
|
|||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { sessionStore } from '@/stores/session'
|
import { sessionStore } from '@/stores/session'
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
rows: {
|
rows: {
|
||||||
@ -248,6 +256,8 @@ const emit = defineEmits([
|
|||||||
'likeDoc',
|
'likeDoc',
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
const pageLengthCount = defineModel()
|
const pageLengthCount = defineModel()
|
||||||
const list = defineModel('list')
|
const list = defineModel('list')
|
||||||
|
|
||||||
@ -273,7 +283,7 @@ const listBulkActionsRef = ref(null)
|
|||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
customListActions: computed(
|
customListActions: computed(
|
||||||
() => listBulkActionsRef.value?.customListActions
|
() => listBulkActionsRef.value?.customListActions,
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
getRowRoute: (row) => ({
|
getRowRoute: (row) => ({
|
||||||
name: 'Organization',
|
name: 'Organization',
|
||||||
params: { organizationId: row.name },
|
params: { organizationId: row.name },
|
||||||
|
query: { view: route.query.view, viewType: route.params.viewType },
|
||||||
}),
|
}),
|
||||||
selectable: options.selectable,
|
selectable: options.selectable,
|
||||||
showTooltip: options.showTooltip,
|
showTooltip: options.showTooltip,
|
||||||
@ -156,6 +157,7 @@ import {
|
|||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { sessionStore } from '@/stores/session'
|
import { sessionStore } from '@/stores/session'
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
rows: {
|
rows: {
|
||||||
@ -187,6 +189,8 @@ const emit = defineEmits([
|
|||||||
'likeDoc',
|
'likeDoc',
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
const pageLengthCount = defineModel()
|
const pageLengthCount = defineModel()
|
||||||
const list = defineModel('list')
|
const list = defineModel('list')
|
||||||
|
|
||||||
|
|||||||
@ -108,9 +108,9 @@ watch(show, (value) => {
|
|||||||
duplicateMode.value = false
|
duplicateMode.value = false
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
_view.value = { ...view.value }
|
_view.value = { ...view.value }
|
||||||
if (_view.value.name) {
|
if (_view.value.mode === 'edit') {
|
||||||
editMode.value = true
|
editMode.value = true
|
||||||
} else if (_view.value.label) {
|
} else if (_view.value.mode === 'duplicate') {
|
||||||
duplicateMode.value = true
|
duplicateMode.value = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
97
frontend/src/components/ViewBreadcrumbs.vue
Normal file
97
frontend/src/components/ViewBreadcrumbs.vue
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<router-link
|
||||||
|
:to="{ name: routeName }"
|
||||||
|
class="px-0.5 py-1 text-lg font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 text-gray-600 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
{{ __(routeName) }}
|
||||||
|
</router-link>
|
||||||
|
<span class="mx-0.5 text-base text-gray-500" aria-hidden="true"> / </span>
|
||||||
|
<Dropdown v-if="viewControls" :options="viewControls.viewsDropdownOptions">
|
||||||
|
<template #default="{ open }">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="text-lg font-medium text-nowrap"
|
||||||
|
:label="__(viewControls.currentView.label)"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<Icon :icon="viewControls.currentView.icon" class="h-4" />
|
||||||
|
</template>
|
||||||
|
<template #suffix>
|
||||||
|
<FeatherIcon
|
||||||
|
:name="open ? 'chevron-up' : 'chevron-down'"
|
||||||
|
class="h-4 text-gray-800"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
<template #item="{ item, active }">
|
||||||
|
<button
|
||||||
|
:class="[
|
||||||
|
active ? 'bg-gray-100' : 'text-gray-800',
|
||||||
|
'group flex gap-4 h-7 w-full justify-between items-center rounded px-2 text-base',
|
||||||
|
]"
|
||||||
|
@click="item.onClick"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<FeatherIcon
|
||||||
|
v-if="item.icon && typeof item.icon === 'string'"
|
||||||
|
:name="item.icon"
|
||||||
|
class="mr-2 h-4 w-4 flex-shrink-0 text-gray-700"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<component
|
||||||
|
class="mr-2 h-4 w-4 flex-shrink-0 text-gray-700"
|
||||||
|
v-else-if="item.icon"
|
||||||
|
:is="item.icon"
|
||||||
|
/>
|
||||||
|
<span class="whitespace-nowrap">
|
||||||
|
{{ item.label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="item.name"
|
||||||
|
class="flex flex-row-reverse gap-2 items-center min-w-11"
|
||||||
|
>
|
||||||
|
<Dropdown
|
||||||
|
:class="active ? 'block' : 'hidden'"
|
||||||
|
placement="right-start"
|
||||||
|
:options="viewControls.viewActions(item)"
|
||||||
|
>
|
||||||
|
<template #default="{ togglePopover }">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="!size-5"
|
||||||
|
icon="more-horizontal"
|
||||||
|
@click.stop="togglePopover()"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Dropdown>
|
||||||
|
<FeatherIcon
|
||||||
|
v-if="isCurrentView(item)"
|
||||||
|
name="check"
|
||||||
|
class="size-4 text-gray-700"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import Icon from '@/components/Icon.vue'
|
||||||
|
import Dropdown from '@/components/frappe-ui/Dropdown.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
routeName: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const viewControls = defineModel()
|
||||||
|
|
||||||
|
const isCurrentView = (item) => {
|
||||||
|
return item.name === viewControls.value.currentView.name
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -3,43 +3,6 @@
|
|||||||
v-if="isMobileView"
|
v-if="isMobileView"
|
||||||
class="flex flex-col justify-between gap-2 sm:px-5 px-3 py-4"
|
class="flex flex-col justify-between gap-2 sm:px-5 px-3 py-4"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between gap-2 overflow-x-auto">
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<Dropdown :options="viewsDropdownOptions">
|
|
||||||
<template #default="{ open }">
|
|
||||||
<Button :label="__(currentView.label)">
|
|
||||||
<template #prefix>
|
|
||||||
<div v-if="isEmoji(currentView.icon)">
|
|
||||||
{{ currentView.icon }}
|
|
||||||
</div>
|
|
||||||
<FeatherIcon
|
|
||||||
v-else-if="typeof currentView.icon == 'string'"
|
|
||||||
:name="currentView.icon"
|
|
||||||
class="h-4"
|
|
||||||
/>
|
|
||||||
<component v-else :is="currentView.icon" class="h-4" />
|
|
||||||
</template>
|
|
||||||
<template #suffix>
|
|
||||||
<FeatherIcon
|
|
||||||
:name="open ? 'chevron-up' : 'chevron-down'"
|
|
||||||
class="h-4 text-gray-600"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</Button>
|
|
||||||
</template>
|
|
||||||
</Dropdown>
|
|
||||||
<Dropdown :options="viewActions">
|
|
||||||
<template #default>
|
|
||||||
<Button icon="more-horizontal" />
|
|
||||||
</template>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
<Button :label="__('Refresh')" @click="reload()" :loading="isLoading">
|
|
||||||
<template #icon>
|
|
||||||
<RefreshIcon class="h-4 w-4" />
|
|
||||||
</template>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<div class="flex items-center justify-between gap-2 overflow-x-auto">
|
<div class="flex items-center justify-between gap-2 overflow-x-auto">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
@ -59,6 +22,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
|
<Button :label="__('Refresh')" @click="reload()" :loading="isLoading">
|
||||||
|
<template #icon>
|
||||||
|
<RefreshIcon class="h-4 w-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
<SortBy
|
<SortBy
|
||||||
v-if="route.params.viewType !== 'kanban'"
|
v-if="route.params.viewType !== 'kanban'"
|
||||||
v-model="list"
|
v-model="list"
|
||||||
@ -91,37 +59,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex items-center justify-between gap-2 px-5 py-4">
|
<div v-else class="flex items-center justify-between gap-2 px-5 py-4">
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<Dropdown :options="viewsDropdownOptions">
|
|
||||||
<template #default="{ open }">
|
|
||||||
<Button :label="__(currentView.label)">
|
|
||||||
<template #prefix>
|
|
||||||
<div v-if="isEmoji(currentView.icon)">{{ currentView.icon }}</div>
|
|
||||||
<FeatherIcon
|
|
||||||
v-else-if="typeof currentView.icon == 'string'"
|
|
||||||
:name="currentView.icon"
|
|
||||||
class="h-4"
|
|
||||||
/>
|
|
||||||
<component v-else :is="currentView.icon" class="h-4" />
|
|
||||||
</template>
|
|
||||||
<template #suffix>
|
|
||||||
<FeatherIcon
|
|
||||||
:name="open ? 'chevron-up' : 'chevron-down'"
|
|
||||||
class="h-4 text-gray-600"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</Button>
|
|
||||||
</template>
|
|
||||||
</Dropdown>
|
|
||||||
<Dropdown :options="viewActions">
|
|
||||||
<template #default>
|
|
||||||
<Button icon="more-horizontal" />
|
|
||||||
</template>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
<div class="-mr-2 h-[70%] border-l" />
|
|
||||||
<FadedScrollableDiv
|
<FadedScrollableDiv
|
||||||
class="flex flex-1 items-center overflow-x-auto px-1"
|
class="flex flex-1 items-center overflow-x-auto -ml-1"
|
||||||
orientation="horizontal"
|
orientation="horizontal"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -223,6 +162,7 @@
|
|||||||
afterUpdate: () => {
|
afterUpdate: () => {
|
||||||
viewUpdated = false
|
viewUpdated = false
|
||||||
reloadView()
|
reloadView()
|
||||||
|
list.reload()
|
||||||
},
|
},
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
@ -268,8 +208,9 @@
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import DetailsIcon from '@/components/Icons/DetailsIcon.vue'
|
import ListIcon from '@/components/Icons/ListIcon.vue'
|
||||||
import KanbanIcon from '@/components/Icons/KanbanIcon.vue'
|
import KanbanIcon from '@/components/Icons/KanbanIcon.vue'
|
||||||
|
import GroupByIcon from '@/components/Icons/GroupByIcon.vue'
|
||||||
import QuickFilterField from '@/components/QuickFilterField.vue'
|
import QuickFilterField from '@/components/QuickFilterField.vue'
|
||||||
import RefreshIcon from '@/components/Icons/RefreshIcon.vue'
|
import RefreshIcon from '@/components/Icons/RefreshIcon.vue'
|
||||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||||
@ -340,15 +281,18 @@ function getViewType() {
|
|||||||
let viewType = route.params.viewType || 'list'
|
let viewType = route.params.viewType || 'list'
|
||||||
let types = {
|
let types = {
|
||||||
list: {
|
list: {
|
||||||
label: __('List View'),
|
name: 'list',
|
||||||
icon: 'list',
|
label: __('List'),
|
||||||
|
icon: markRaw(ListIcon),
|
||||||
},
|
},
|
||||||
group_by: {
|
group_by: {
|
||||||
label: __('Group By View'),
|
name: 'group_by',
|
||||||
icon: markRaw(DetailsIcon),
|
label: __('Group By'),
|
||||||
|
icon: markRaw(GroupByIcon),
|
||||||
},
|
},
|
||||||
kanban: {
|
kanban: {
|
||||||
label: __('Kanban View'),
|
name: 'kanban',
|
||||||
|
label: __('Kanban'),
|
||||||
icon: markRaw(KanbanIcon),
|
icon: markRaw(KanbanIcon),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -359,6 +303,7 @@ function getViewType() {
|
|||||||
const currentView = computed(() => {
|
const currentView = computed(() => {
|
||||||
let _view = getView(route.query.view, route.params.viewType, props.doctype)
|
let _view = getView(route.query.view, route.params.viewType, props.doctype)
|
||||||
return {
|
return {
|
||||||
|
name: _view?.name || getViewType().name,
|
||||||
label:
|
label:
|
||||||
_view?.label || props.options?.defaultViewName || getViewType().label,
|
_view?.label || props.options?.defaultViewName || getViewType().label,
|
||||||
icon: _view?.icon || getViewType().icon,
|
icon: _view?.icon || getViewType().icon,
|
||||||
@ -531,27 +476,19 @@ let allowedViews = props.options.allowedViews || ['list']
|
|||||||
|
|
||||||
if (allowedViews.includes('list')) {
|
if (allowedViews.includes('list')) {
|
||||||
defaultViews.push({
|
defaultViews.push({
|
||||||
label: __(props.options?.defaultViewName) || __('List View'),
|
name: 'list',
|
||||||
icon: 'list',
|
label: __(props.options?.defaultViewName) || __('List'),
|
||||||
|
icon: markRaw(ListIcon),
|
||||||
onClick() {
|
onClick() {
|
||||||
viewUpdated.value = false
|
viewUpdated.value = false
|
||||||
router.push({ name: route.name })
|
router.push({ name: route.name })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (allowedViews.includes('group_by')) {
|
|
||||||
defaultViews.push({
|
|
||||||
label: __(props.options?.defaultViewName) || __('Group By View'),
|
|
||||||
icon: markRaw(DetailsIcon),
|
|
||||||
onClick() {
|
|
||||||
viewUpdated.value = false
|
|
||||||
router.push({ name: route.name, params: { viewType: 'group_by' } })
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (allowedViews.includes('kanban')) {
|
if (allowedViews.includes('kanban')) {
|
||||||
defaultViews.push({
|
defaultViews.push({
|
||||||
label: __(props.options?.defaultViewName) || __('Kanban View'),
|
name: 'kanban',
|
||||||
|
label: __(props.options?.defaultViewName) || __('Kanban'),
|
||||||
icon: markRaw(KanbanIcon),
|
icon: markRaw(KanbanIcon),
|
||||||
onClick() {
|
onClick() {
|
||||||
viewUpdated.value = false
|
viewUpdated.value = false
|
||||||
@ -559,14 +496,27 @@ if (allowedViews.includes('kanban')) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (allowedViews.includes('group_by')) {
|
||||||
|
defaultViews.push({
|
||||||
|
name: 'group_by',
|
||||||
|
label: __(props.options?.defaultViewName) || __('Group By'),
|
||||||
|
icon: markRaw(GroupByIcon),
|
||||||
|
onClick() {
|
||||||
|
viewUpdated.value = false
|
||||||
|
router.push({ name: route.name, params: { viewType: 'group_by' } })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function getIcon(icon, type) {
|
function getIcon(icon, type) {
|
||||||
if (isEmoji(icon)) {
|
if (isEmoji(icon)) {
|
||||||
return h('div', icon)
|
return h('div', icon)
|
||||||
} else if (!icon && type === 'group_by') {
|
} else if (!icon && type === 'group_by') {
|
||||||
return markRaw(DetailsIcon)
|
return markRaw(GroupByIcon)
|
||||||
|
} else if (!icon && type === 'kanban') {
|
||||||
|
return markRaw(KanbanIcon)
|
||||||
}
|
}
|
||||||
return icon || 'list'
|
return icon || markRaw(ListIcon)
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewsDropdownOptions = computed(() => {
|
const viewsDropdownOptions = computed(() => {
|
||||||
@ -580,6 +530,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.name = view.name
|
||||||
view.label = __(view.label)
|
view.label = __(view.label)
|
||||||
view.type = view.type || 'list'
|
view.type = view.type || 'list'
|
||||||
view.icon = getIcon(view.icon, view.type)
|
view.icon = getIcon(view.icon, view.type)
|
||||||
@ -602,17 +553,16 @@ const viewsDropdownOptions = computed(() => {
|
|||||||
)
|
)
|
||||||
let pinnedViews = list.value.data.views.filter((v) => v.pinned)
|
let pinnedViews = list.value.data.views.filter((v) => v.pinned)
|
||||||
|
|
||||||
publicViews.length &&
|
|
||||||
_views.push({
|
|
||||||
group: __('Public Views'),
|
|
||||||
items: publicViews,
|
|
||||||
})
|
|
||||||
|
|
||||||
savedViews.length &&
|
savedViews.length &&
|
||||||
_views.push({
|
_views.push({
|
||||||
group: __('Saved Views'),
|
group: __('Saved Views'),
|
||||||
items: savedViews,
|
items: savedViews,
|
||||||
})
|
})
|
||||||
|
publicViews.length &&
|
||||||
|
_views.push({
|
||||||
|
group: __('Public Views'),
|
||||||
|
items: publicViews,
|
||||||
|
})
|
||||||
pinnedViews.length &&
|
pinnedViews.length &&
|
||||||
_views.push({
|
_views.push({
|
||||||
group: __('Pinned Views'),
|
group: __('Pinned Views'),
|
||||||
@ -620,6 +570,18 @@ const viewsDropdownOptions = computed(() => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_views.push({
|
||||||
|
group: __('Actions'),
|
||||||
|
hideLabel: true,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: __('Create View'),
|
||||||
|
icon: 'plus',
|
||||||
|
onClick: () => createView(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
return _views
|
return _views
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -912,7 +874,10 @@ function updatePageLength(value, loadMore = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// View Actions
|
// View Actions
|
||||||
const viewActions = computed(() => {
|
const viewActions = (view) => {
|
||||||
|
let isDefault = typeof view.name === 'string'
|
||||||
|
let _view = getView(view.name)
|
||||||
|
|
||||||
let actions = [
|
let actions = [
|
||||||
{
|
{
|
||||||
group: __('Default Views'),
|
group: __('Default Views'),
|
||||||
@ -921,37 +886,36 @@ const viewActions = computed(() => {
|
|||||||
{
|
{
|
||||||
label: __('Duplicate'),
|
label: __('Duplicate'),
|
||||||
icon: () => h(DuplicateIcon, { class: 'h-4 w-4' }),
|
icon: () => h(DuplicateIcon, { class: 'h-4 w-4' }),
|
||||||
onClick: () => duplicateView(),
|
onClick: () => duplicateView(_view),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
if (route.query.view && (!view.value.public || isManager())) {
|
if (!isDefault && (!_view.public || isManager())) {
|
||||||
actions[0].items.push({
|
actions[0].items.push({
|
||||||
label: __('Edit'),
|
label: __('Edit'),
|
||||||
icon: () => h(EditIcon, { class: 'h-4 w-4' }),
|
icon: () => h(EditIcon, { class: 'h-4 w-4' }),
|
||||||
onClick: () => editView(),
|
onClick: () => editView(_view),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!view.value.public) {
|
if (!_view.public) {
|
||||||
actions[0].items.push({
|
actions[0].items.push({
|
||||||
label: view.value.pinned ? __('Unpin View') : __('Pin View'),
|
label: _view.pinned ? __('Unpin View') : __('Pin View'),
|
||||||
icon: () =>
|
icon: () => h(_view.pinned ? UnpinIcon : PinIcon, { class: 'h-4 w-4' }),
|
||||||
h(view.value.pinned ? UnpinIcon : PinIcon, { class: 'h-4 w-4' }),
|
onClick: () => pinView(_view),
|
||||||
onClick: () => pinView(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isManager()) {
|
if (isManager()) {
|
||||||
actions[0].items.push({
|
actions[0].items.push({
|
||||||
label: view.value.public ? __('Make Private') : __('Make Public'),
|
label: _view.public ? __('Make Private') : __('Make Public'),
|
||||||
icon: () =>
|
icon: () =>
|
||||||
h(FeatherIcon, {
|
h(FeatherIcon, {
|
||||||
name: view.value.public ? 'lock' : 'unlock',
|
name: _view.public ? 'lock' : 'unlock',
|
||||||
class: 'h-4 w-4',
|
class: 'h-4 w-4',
|
||||||
}),
|
}),
|
||||||
onClick: () => publicView(),
|
onClick: () => publicView(_view),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -965,14 +929,16 @@ const viewActions = computed(() => {
|
|||||||
onClick: () =>
|
onClick: () =>
|
||||||
$dialog({
|
$dialog({
|
||||||
title: __('Delete View'),
|
title: __('Delete View'),
|
||||||
message: __('Are you sure you want to delete this view?'),
|
message: __('Are you sure you want to delete "{0}" view?', [
|
||||||
|
_view.label,
|
||||||
|
]),
|
||||||
variant: 'danger',
|
variant: 'danger',
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
label: __('Delete'),
|
label: __('Delete'),
|
||||||
variant: 'solid',
|
variant: 'solid',
|
||||||
theme: 'red',
|
theme: 'red',
|
||||||
onClick: (close) => deleteView(close),
|
onClick: (close) => deleteView(_view, close),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@ -981,56 +947,61 @@ const viewActions = computed(() => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
return actions
|
return actions
|
||||||
})
|
}
|
||||||
|
|
||||||
const viewModalObj = ref({})
|
const viewModalObj = ref({})
|
||||||
|
|
||||||
function duplicateView() {
|
function createView() {
|
||||||
let label =
|
|
||||||
__(
|
|
||||||
getView(route.query.view, route.params.viewType, props.doctype)?.label,
|
|
||||||
) || getViewType().label
|
|
||||||
view.value.name = ''
|
view.value.name = ''
|
||||||
view.value.label = label + __(' (New)')
|
view.value.label = ''
|
||||||
|
view.value.icon = ''
|
||||||
viewModalObj.value = view.value
|
viewModalObj.value = view.value
|
||||||
|
viewModalObj.value.mode = 'create'
|
||||||
showViewModal.value = true
|
showViewModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function editView() {
|
function duplicateView(v) {
|
||||||
let cView = getView(route.query.view, route.params.viewType, props.doctype)
|
v.label = v.label + __(' (New)')
|
||||||
view.value.name = route.query.view
|
viewModalObj.value = v
|
||||||
view.value.label = __(cView?.label) || getViewType().label
|
viewModalObj.value.mode = 'duplicate'
|
||||||
view.value.icon = cView?.icon || ''
|
|
||||||
viewModalObj.value = view.value
|
|
||||||
showViewModal.value = true
|
showViewModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function publicView() {
|
function editView(v) {
|
||||||
|
viewModalObj.value = v
|
||||||
|
viewModalObj.value.mode = 'edit'
|
||||||
|
showViewModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function publicView(v) {
|
||||||
call('crm.fcrm.doctype.crm_view_settings.crm_view_settings.public', {
|
call('crm.fcrm.doctype.crm_view_settings.crm_view_settings.public', {
|
||||||
name: route.query.view,
|
name: v.name,
|
||||||
value: !view.value.public,
|
value: !v.public,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
view.value.public = !view.value.public
|
v.public = !v.public
|
||||||
reloadView()
|
reloadView()
|
||||||
|
list.value.reload()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function pinView() {
|
function pinView(v) {
|
||||||
call('crm.fcrm.doctype.crm_view_settings.crm_view_settings.pin', {
|
call('crm.fcrm.doctype.crm_view_settings.crm_view_settings.pin', {
|
||||||
name: route.query.view,
|
name: v.name,
|
||||||
value: !view.value.pinned,
|
value: !v.pinned,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
view.value.pinned = !view.value.pinned
|
v.pinned = !v.pinned
|
||||||
reloadView()
|
reloadView()
|
||||||
|
list.value.reload()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteView(close) {
|
function deleteView(v, close) {
|
||||||
call('crm.fcrm.doctype.crm_view_settings.crm_view_settings.delete', {
|
call('crm.fcrm.doctype.crm_view_settings.crm_view_settings.delete', {
|
||||||
name: route.query.view,
|
name: v.name,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
router.push({ name: route.name })
|
router.push({ name: route.name })
|
||||||
reloadView()
|
reloadView()
|
||||||
|
list.value.reload()
|
||||||
})
|
})
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
@ -1059,6 +1030,7 @@ function saveView() {
|
|||||||
load_default_columns: view.value.load_default_columns,
|
load_default_columns: view.value.load_default_columns,
|
||||||
}
|
}
|
||||||
viewModalObj.value = view.value
|
viewModalObj.value = view.value
|
||||||
|
viewModalObj.value.mode = 'edit'
|
||||||
showViewModal.value = true
|
showViewModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1119,6 +1091,9 @@ defineExpose({
|
|||||||
likeDoc,
|
likeDoc,
|
||||||
updateKanbanSettings,
|
updateKanbanSettings,
|
||||||
loadMoreKanban,
|
loadMoreKanban,
|
||||||
|
viewActions,
|
||||||
|
viewsDropdownOptions,
|
||||||
|
currentView,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Watchers
|
// Watchers
|
||||||
|
|||||||
@ -5,9 +5,9 @@
|
|||||||
:show="open"
|
:show="open"
|
||||||
:placement="popoverPlacement"
|
:placement="popoverPlacement"
|
||||||
>
|
>
|
||||||
<template #target>
|
<template #target="{ togglePopover }">
|
||||||
<MenuButton as="div">
|
<MenuButton as="template">
|
||||||
<slot v-if="$slots.default" v-bind="{ open }" />
|
<slot v-if="$slots.default" v-bind="{ open, togglePopover }" />
|
||||||
<Button v-else :active="open" v-bind="button">
|
<Button v-else :active="open" v-bind="button">
|
||||||
{{ button ? button?.label || null : 'Options' }}
|
{{ button ? button?.label || null : 'Options' }}
|
||||||
</Button>
|
</Button>
|
||||||
@ -17,13 +17,18 @@
|
|||||||
<template #body>
|
<template #body>
|
||||||
<div
|
<div
|
||||||
class="rounded-lg bg-white shadow-2xl ring-1 ring-black ring-opacity-5 focus:outline-none"
|
class="rounded-lg bg-white shadow-2xl ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||||
|
:class="{
|
||||||
|
'mt-2': ['bottom', 'left', 'right'].includes(placement),
|
||||||
|
'ml-2': placement == 'right-start',
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<MenuItems
|
<MenuItems
|
||||||
class="mt-2 min-w-40 divide-y divide-gray-100"
|
class="min-w-40 divide-y divide-gray-100"
|
||||||
:class="{
|
:class="{
|
||||||
'left-0 origin-top-left': placement == 'left',
|
'left-0 origin-top-left': placement == 'left',
|
||||||
'right-0 origin-top-right': placement == 'right',
|
'right-0 origin-top-right': placement == 'right',
|
||||||
'inset-x-0 origin-top': placement == 'center',
|
'inset-x-0 origin-top': placement == 'center',
|
||||||
|
'mt-0 origin-top-right': placement == 'right-start',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div v-for="group in groups" :key="group.key" class="p-1.5">
|
<div v-for="group in groups" :key="group.key" class="p-1.5">
|
||||||
@ -38,34 +43,36 @@
|
|||||||
:key="item.label"
|
:key="item.label"
|
||||||
v-slot="{ active }"
|
v-slot="{ active }"
|
||||||
>
|
>
|
||||||
<component
|
<slot name="item" v-bind="{ item, active }">
|
||||||
v-if="item.component"
|
|
||||||
:is="item.component"
|
|
||||||
:active="active"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
v-else
|
|
||||||
:class="[
|
|
||||||
active ? 'bg-gray-100' : 'text-gray-800',
|
|
||||||
'group flex h-7 w-full items-center rounded px-2 text-base',
|
|
||||||
]"
|
|
||||||
@click="item.onClick"
|
|
||||||
>
|
|
||||||
<FeatherIcon
|
|
||||||
v-if="item.icon && typeof item.icon === 'string'"
|
|
||||||
:name="item.icon"
|
|
||||||
class="mr-2 h-4 w-4 flex-shrink-0 text-gray-700"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<component
|
<component
|
||||||
class="mr-2 h-4 w-4 flex-shrink-0 text-gray-700"
|
v-if="item.component"
|
||||||
v-else-if="item.icon"
|
:is="item.component"
|
||||||
:is="item.icon"
|
:active="active"
|
||||||
/>
|
/>
|
||||||
<span class="whitespace-nowrap">
|
<button
|
||||||
{{ item.label }}
|
v-else
|
||||||
</span>
|
:class="[
|
||||||
</button>
|
active ? 'bg-gray-100' : 'text-gray-800',
|
||||||
|
'group flex h-7 w-full items-center rounded px-2 text-base',
|
||||||
|
]"
|
||||||
|
@click="item.onClick"
|
||||||
|
>
|
||||||
|
<FeatherIcon
|
||||||
|
v-if="item.icon && typeof item.icon === 'string'"
|
||||||
|
:name="item.icon"
|
||||||
|
class="mr-2 h-4 w-4 flex-shrink-0 text-gray-700"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<component
|
||||||
|
class="mr-2 h-4 w-4 flex-shrink-0 text-gray-700"
|
||||||
|
v-else-if="item.icon"
|
||||||
|
:is="item.icon"
|
||||||
|
/>
|
||||||
|
<span class="whitespace-nowrap">
|
||||||
|
{{ item.label }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</slot>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</div>
|
</div>
|
||||||
</MenuItems>
|
</MenuItems>
|
||||||
@ -130,6 +137,7 @@ const popoverPlacement = computed(() => {
|
|||||||
if (props.placement === 'left') return 'bottom-start'
|
if (props.placement === 'left') return 'bottom-start'
|
||||||
if (props.placement === 'right') return 'bottom-end'
|
if (props.placement === 'right') return 'bottom-end'
|
||||||
if (props.placement === 'center') return 'bottom-center'
|
if (props.placement === 'center') return 'bottom-center'
|
||||||
|
if (props.placement === 'right-start') return 'right-start'
|
||||||
return 'bottom'
|
return 'bottom'
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -140,6 +148,7 @@ function normalizeDropdownItem(option) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
name: option.name,
|
||||||
label: option.label,
|
label: option.label,
|
||||||
icon: option.icon,
|
icon: option.icon,
|
||||||
group: option.group,
|
group: option.group,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<LayoutHeader>
|
<LayoutHeader>
|
||||||
<template #left-header>
|
<template #left-header>
|
||||||
<Breadcrumbs :items="breadcrumbs" />
|
<ViewBreadcrumbs v-model="viewControls" routeName="Call Logs" />
|
||||||
</template>
|
</template>
|
||||||
<template #right-header>
|
<template #right-header>
|
||||||
<CustomActions
|
<CustomActions
|
||||||
@ -54,6 +54,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import ViewBreadcrumbs from '@/components/ViewBreadcrumbs.vue'
|
||||||
import CustomActions from '@/components/CustomActions.vue'
|
import CustomActions from '@/components/CustomActions.vue'
|
||||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
@ -61,11 +62,8 @@ import ViewControls from '@/components/ViewControls.vue'
|
|||||||
import CallLogsListView from '@/components/ListViews/CallLogsListView.vue'
|
import CallLogsListView from '@/components/ListViews/CallLogsListView.vue'
|
||||||
import CallLogModal from '@/components/Modals/CallLogModal.vue'
|
import CallLogModal from '@/components/Modals/CallLogModal.vue'
|
||||||
import { getCallLogDetail } from '@/utils/callLog'
|
import { getCallLogDetail } from '@/utils/callLog'
|
||||||
import { Breadcrumbs } from 'frappe-ui'
|
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
const breadcrumbs = [{ label: __('Call Logs'), route: { name: 'Call Logs' } }]
|
|
||||||
|
|
||||||
const callLogsListView = ref(null)
|
const callLogsListView = ref(null)
|
||||||
|
|
||||||
// callLogs data is loaded in the ViewControls component
|
// callLogs data is loaded in the ViewControls component
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<LayoutHeader v-if="contact.data">
|
<LayoutHeader v-if="contact.data">
|
||||||
<template #left-header>
|
<template #left-header>
|
||||||
<Breadcrumbs :items="breadcrumbs" />
|
<Breadcrumbs :items="breadcrumbs">
|
||||||
|
<template #prefix="{ item }">
|
||||||
|
<Icon v-if="item.icon" :icon="item.icon" class="mr-2 h-4" />
|
||||||
|
</template>
|
||||||
|
</Breadcrumbs>
|
||||||
</template>
|
</template>
|
||||||
</LayoutHeader>
|
</LayoutHeader>
|
||||||
<div v-if="contact.data" class="flex h-full flex-col overflow-hidden">
|
<div v-if="contact.data" class="flex h-full flex-col overflow-hidden">
|
||||||
@ -216,16 +220,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import Icon from '@/components/Icon.vue'
|
||||||
Breadcrumbs,
|
|
||||||
Avatar,
|
|
||||||
FileUploader,
|
|
||||||
Tooltip,
|
|
||||||
Tabs,
|
|
||||||
call,
|
|
||||||
createResource,
|
|
||||||
usePageMeta,
|
|
||||||
} from 'frappe-ui'
|
|
||||||
import Dropdown from '@/components/frappe-ui/Dropdown.vue'
|
import Dropdown from '@/components/frappe-ui/Dropdown.vue'
|
||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
import Email2Icon from '@/components/Icons/Email2Icon.vue'
|
import Email2Icon from '@/components/Icons/Email2Icon.vue'
|
||||||
@ -242,13 +237,24 @@ import {
|
|||||||
timeAgo,
|
timeAgo,
|
||||||
formatNumberIntoCurrency,
|
formatNumberIntoCurrency,
|
||||||
} from '@/utils'
|
} from '@/utils'
|
||||||
|
import { getView } from '@/utils/view'
|
||||||
import { globalStore } from '@/stores/global.js'
|
import { globalStore } from '@/stores/global.js'
|
||||||
import { usersStore } from '@/stores/users.js'
|
import { usersStore } from '@/stores/users.js'
|
||||||
import { organizationsStore } from '@/stores/organizations.js'
|
import { organizationsStore } from '@/stores/organizations.js'
|
||||||
import { statusesStore } from '@/stores/statuses'
|
import { statusesStore } from '@/stores/statuses'
|
||||||
import { callEnabled } from '@/composables/settings'
|
import { callEnabled } from '@/composables/settings'
|
||||||
|
import {
|
||||||
|
Breadcrumbs,
|
||||||
|
Avatar,
|
||||||
|
FileUploader,
|
||||||
|
Tooltip,
|
||||||
|
Tabs,
|
||||||
|
call,
|
||||||
|
createResource,
|
||||||
|
usePageMeta,
|
||||||
|
} from 'frappe-ui'
|
||||||
import { ref, computed, h } from 'vue'
|
import { ref, computed, h } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
const { $dialog, makeCall } = globalStore()
|
const { $dialog, makeCall } = globalStore()
|
||||||
|
|
||||||
@ -263,6 +269,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const showContactModal = ref(false)
|
const showContactModal = ref(false)
|
||||||
@ -287,6 +294,22 @@ const contact = createResource({
|
|||||||
|
|
||||||
const breadcrumbs = computed(() => {
|
const breadcrumbs = computed(() => {
|
||||||
let items = [{ label: __('Contacts'), route: { name: 'Contacts' } }]
|
let items = [{ label: __('Contacts'), route: { name: 'Contacts' } }]
|
||||||
|
|
||||||
|
if (route.query.view || route.query.viewType) {
|
||||||
|
let view = getView(route.query.view, route.query.viewType, 'Contact')
|
||||||
|
if (view) {
|
||||||
|
items.push({
|
||||||
|
label: __(view.label),
|
||||||
|
icon: view.icon,
|
||||||
|
route: {
|
||||||
|
name: 'Contacts',
|
||||||
|
params: { viewType: route.query.viewType },
|
||||||
|
query: { view: route.query.view },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
label: contact.data?.full_name,
|
label: contact.data?.full_name,
|
||||||
route: { name: 'Contact', params: { contactId: props.contactId } },
|
route: { name: 'Contact', params: { contactId: props.contactId } },
|
||||||
@ -300,7 +323,6 @@ usePageMeta(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
function validateFile(file) {
|
function validateFile(file) {
|
||||||
let extn = file.name.split('.').pop().toLowerCase()
|
let extn = file.name.split('.').pop().toLowerCase()
|
||||||
if (!['png', 'jpg', 'jpeg'].includes(extn)) {
|
if (!['png', 'jpg', 'jpeg'].includes(extn)) {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<LayoutHeader>
|
<LayoutHeader>
|
||||||
<template #left-header>
|
<template #left-header>
|
||||||
<Breadcrumbs :items="breadcrumbs" />
|
<ViewBreadcrumbs v-model="viewControls" routeName="Contacts" />
|
||||||
</template>
|
</template>
|
||||||
<template #right-header>
|
<template #right-header>
|
||||||
<CustomActions
|
<CustomActions
|
||||||
@ -72,6 +72,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import ViewBreadcrumbs from '@/components/ViewBreadcrumbs.vue'
|
||||||
import CustomActions from '@/components/CustomActions.vue'
|
import CustomActions from '@/components/CustomActions.vue'
|
||||||
import ContactsIcon from '@/components/Icons/ContactsIcon.vue'
|
import ContactsIcon from '@/components/Icons/ContactsIcon.vue'
|
||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
@ -79,37 +80,15 @@ import ContactModal from '@/components/Modals/ContactModal.vue'
|
|||||||
import QuickEntryModal from '@/components/Settings/QuickEntryModal.vue'
|
import QuickEntryModal from '@/components/Settings/QuickEntryModal.vue'
|
||||||
import ContactsListView from '@/components/ListViews/ContactsListView.vue'
|
import ContactsListView from '@/components/ListViews/ContactsListView.vue'
|
||||||
import ViewControls from '@/components/ViewControls.vue'
|
import ViewControls from '@/components/ViewControls.vue'
|
||||||
import { Breadcrumbs } from 'frappe-ui'
|
|
||||||
import { organizationsStore } from '@/stores/organizations.js'
|
import { organizationsStore } from '@/stores/organizations.js'
|
||||||
import { dateFormat, dateTooltipFormat, timeAgo } from '@/utils'
|
import { dateFormat, dateTooltipFormat, timeAgo } from '@/utils'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
|
||||||
|
|
||||||
const { getOrganization } = organizationsStore()
|
const { getOrganization } = organizationsStore()
|
||||||
const route = useRoute()
|
|
||||||
|
|
||||||
const showContactModal = ref(false)
|
const showContactModal = ref(false)
|
||||||
const showQuickEntryModal = ref(false)
|
const showQuickEntryModal = ref(false)
|
||||||
|
|
||||||
const currentContact = computed(() => {
|
|
||||||
return contacts.value?.data?.data?.find(
|
|
||||||
(contact) => contact.name === route.params.contactId,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const breadcrumbs = computed(() => {
|
|
||||||
let items = [{ label: __('Contacts'), route: { name: 'Contacts' } }]
|
|
||||||
if (!currentContact.value) return items
|
|
||||||
items.push({
|
|
||||||
label: __(currentContact.value.full_name),
|
|
||||||
route: {
|
|
||||||
name: 'Contact',
|
|
||||||
params: { contactId: currentContact.value.name },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return items
|
|
||||||
})
|
|
||||||
|
|
||||||
const contactsListView = ref(null)
|
const contactsListView = ref(null)
|
||||||
|
|
||||||
// contacts data is loaded in the ViewControls component
|
// contacts data is loaded in the ViewControls component
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<LayoutHeader v-if="deal.data">
|
<LayoutHeader v-if="deal.data">
|
||||||
<template #left-header>
|
<template #left-header>
|
||||||
<Breadcrumbs :items="breadcrumbs" />
|
<Breadcrumbs :items="breadcrumbs">
|
||||||
|
<template #prefix="{ item }">
|
||||||
|
<Icon v-if="item.icon" :icon="item.icon" class="mr-2 h-4" />
|
||||||
|
</template>
|
||||||
|
</Breadcrumbs>
|
||||||
</template>
|
</template>
|
||||||
<template #right-header>
|
<template #right-header>
|
||||||
<CustomActions
|
<CustomActions
|
||||||
@ -302,6 +306,7 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import Icon from '@/components/Icon.vue'
|
||||||
import Resizer from '@/components/Resizer.vue'
|
import Resizer from '@/components/Resizer.vue'
|
||||||
import LoadingIndicator from '@/components/Icons/LoadingIndicator.vue'
|
import LoadingIndicator from '@/components/Icons/LoadingIndicator.vue'
|
||||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||||
@ -337,6 +342,7 @@ import {
|
|||||||
errorMessage,
|
errorMessage,
|
||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
} from '@/utils'
|
} from '@/utils'
|
||||||
|
import { getView } from '@/utils/view'
|
||||||
import { globalStore } from '@/stores/global'
|
import { globalStore } from '@/stores/global'
|
||||||
import { organizationsStore } from '@/stores/organizations'
|
import { organizationsStore } from '@/stores/organizations'
|
||||||
import { statusesStore } from '@/stores/statuses'
|
import { statusesStore } from '@/stores/statuses'
|
||||||
@ -353,12 +359,13 @@ import {
|
|||||||
usePageMeta,
|
usePageMeta,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { ref, computed, h, onMounted } from 'vue'
|
import { ref, computed, h, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
const { $dialog, makeCall } = globalStore()
|
const { $dialog, makeCall } = globalStore()
|
||||||
const { organizations, getOrganization } = organizationsStore()
|
const { organizations, getOrganization } = organizationsStore()
|
||||||
const { statusOptions, getDealStatus } = statusesStore()
|
const { statusOptions, getDealStatus } = statusesStore()
|
||||||
const { isManager } = usersStore()
|
const { isManager } = usersStore()
|
||||||
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -452,6 +459,22 @@ function validateRequired(fieldname, value) {
|
|||||||
|
|
||||||
const breadcrumbs = computed(() => {
|
const breadcrumbs = computed(() => {
|
||||||
let items = [{ label: __('Deals'), route: { name: 'Deals' } }]
|
let items = [{ label: __('Deals'), route: { name: 'Deals' } }]
|
||||||
|
|
||||||
|
if (route.query.view || route.query.viewType) {
|
||||||
|
let view = getView(route.query.view, route.query.viewType, 'CRM Deal')
|
||||||
|
if (view) {
|
||||||
|
items.push({
|
||||||
|
label: __(view.label),
|
||||||
|
icon: view.icon,
|
||||||
|
route: {
|
||||||
|
name: 'Deals',
|
||||||
|
params: { viewType: route.query.viewType },
|
||||||
|
query: { view: route.query.view },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
label: organization.value?.name || __('Untitled'),
|
label: organization.value?.name || __('Untitled'),
|
||||||
route: { name: 'Deal', params: { dealId: deal.data.name } },
|
route: { name: 'Deal', params: { dealId: deal.data.name } },
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<LayoutHeader>
|
<LayoutHeader>
|
||||||
<template #left-header>
|
<template #left-header>
|
||||||
<Breadcrumbs :items="breadcrumbs" />
|
<ViewBreadcrumbs v-model="viewControls" routeName="Deals" />
|
||||||
</template>
|
</template>
|
||||||
<template #right-header>
|
<template #right-header>
|
||||||
<CustomActions
|
<CustomActions
|
||||||
@ -32,7 +32,11 @@
|
|||||||
v-if="route.params.viewType == 'kanban'"
|
v-if="route.params.viewType == 'kanban'"
|
||||||
v-model="deals"
|
v-model="deals"
|
||||||
:options="{
|
:options="{
|
||||||
getRoute: (row) => ({ name: 'Deal', params: { dealId: row.name } }),
|
getRoute: (row) => ({
|
||||||
|
name: 'Deal',
|
||||||
|
params: { dealId: row.name },
|
||||||
|
query: { view: route.query.view, viewType: route.params.viewType },
|
||||||
|
}),
|
||||||
onNewClick: (column) => onNewClick(column),
|
onNewClick: (column) => onNewClick(column),
|
||||||
}"
|
}"
|
||||||
@update="(data) => viewControls.updateKanbanSettings(data)"
|
@update="(data) => viewControls.updateKanbanSettings(data)"
|
||||||
@ -259,6 +263,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import ViewBreadcrumbs from '@/components/ViewBreadcrumbs.vue'
|
||||||
import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
||||||
import CustomActions from '@/components/CustomActions.vue'
|
import CustomActions from '@/components/CustomActions.vue'
|
||||||
import EmailAtIcon from '@/components/Icons/EmailAtIcon.vue'
|
import EmailAtIcon from '@/components/Icons/EmailAtIcon.vue'
|
||||||
@ -288,12 +293,10 @@ import {
|
|||||||
formatNumberIntoCurrency,
|
formatNumberIntoCurrency,
|
||||||
formatTime,
|
formatTime,
|
||||||
} from '@/utils'
|
} from '@/utils'
|
||||||
import { Breadcrumbs, Tooltip, Avatar, Dropdown } from 'frappe-ui'
|
import { Tooltip, Avatar, Dropdown } from 'frappe-ui'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { ref, reactive, computed, h } from 'vue'
|
import { ref, reactive, computed, h } from 'vue'
|
||||||
|
|
||||||
const breadcrumbs = [{ label: __('Deals'), route: { name: 'Deals' } }]
|
|
||||||
|
|
||||||
const { makeCall } = globalStore()
|
const { makeCall } = globalStore()
|
||||||
const { getUser } = usersStore()
|
const { getUser } = usersStore()
|
||||||
const { getOrganization } = organizationsStore()
|
const { getOrganization } = organizationsStore()
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<LayoutHeader>
|
<LayoutHeader>
|
||||||
<template #left-header>
|
<template #left-header>
|
||||||
<Breadcrumbs :items="breadcrumbs" />
|
<ViewBreadcrumbs v-model="viewControls" routeName="Email Templates" />
|
||||||
</template>
|
</template>
|
||||||
<template #right-header>
|
<template #right-header>
|
||||||
<CustomActions
|
<CustomActions
|
||||||
@ -68,6 +68,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import ViewBreadcrumbs from '@/components/ViewBreadcrumbs.vue'
|
||||||
import CustomActions from '@/components/CustomActions.vue'
|
import CustomActions from '@/components/CustomActions.vue'
|
||||||
import Email2Icon from '@/components/Icons/Email2Icon.vue'
|
import Email2Icon from '@/components/Icons/Email2Icon.vue'
|
||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
@ -75,13 +76,8 @@ import ViewControls from '@/components/ViewControls.vue'
|
|||||||
import EmailTemplatesListView from '@/components/ListViews/EmailTemplatesListView.vue'
|
import EmailTemplatesListView from '@/components/ListViews/EmailTemplatesListView.vue'
|
||||||
import EmailTemplateModal from '@/components/Modals/EmailTemplateModal.vue'
|
import EmailTemplateModal from '@/components/Modals/EmailTemplateModal.vue'
|
||||||
import { dateFormat, dateTooltipFormat, timeAgo } from '@/utils'
|
import { dateFormat, dateTooltipFormat, timeAgo } from '@/utils'
|
||||||
import { Breadcrumbs } from 'frappe-ui'
|
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
const breadcrumbs = [
|
|
||||||
{ label: __('Email Templates'), route: { name: 'Email Templates' } },
|
|
||||||
]
|
|
||||||
|
|
||||||
const emailTemplatesListView = ref(null)
|
const emailTemplatesListView = ref(null)
|
||||||
|
|
||||||
// emailTemplates data is loaded in the ViewControls component
|
// emailTemplates data is loaded in the ViewControls component
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<LayoutHeader v-if="lead.data">
|
<LayoutHeader v-if="lead.data">
|
||||||
<template #left-header>
|
<template #left-header>
|
||||||
<Breadcrumbs :items="breadcrumbs" />
|
<Breadcrumbs :items="breadcrumbs">
|
||||||
|
<template #prefix="{ item }">
|
||||||
|
<Icon v-if="item.icon" :icon="item.icon" class="mr-2 h-4" />
|
||||||
|
</template>
|
||||||
|
</Breadcrumbs>
|
||||||
</template>
|
</template>
|
||||||
<template #right-header>
|
<template #right-header>
|
||||||
<CustomActions
|
<CustomActions
|
||||||
@ -273,6 +277,7 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import Icon from '@/components/Icon.vue'
|
||||||
import Resizer from '@/components/Resizer.vue'
|
import Resizer from '@/components/Resizer.vue'
|
||||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||||
import ActivityIcon from '@/components/Icons/ActivityIcon.vue'
|
import ActivityIcon from '@/components/Icons/ActivityIcon.vue'
|
||||||
@ -306,6 +311,7 @@ import {
|
|||||||
errorMessage,
|
errorMessage,
|
||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
} from '@/utils'
|
} from '@/utils'
|
||||||
|
import { getView } from '@/utils/view'
|
||||||
import { globalStore } from '@/stores/global'
|
import { globalStore } from '@/stores/global'
|
||||||
import { contactsStore } from '@/stores/contacts'
|
import { contactsStore } from '@/stores/contacts'
|
||||||
import { organizationsStore } from '@/stores/organizations'
|
import { organizationsStore } from '@/stores/organizations'
|
||||||
@ -421,6 +427,22 @@ function validateRequired(fieldname, value) {
|
|||||||
|
|
||||||
const breadcrumbs = computed(() => {
|
const breadcrumbs = computed(() => {
|
||||||
let items = [{ label: __('Leads'), route: { name: 'Leads' } }]
|
let items = [{ label: __('Leads'), route: { name: 'Leads' } }]
|
||||||
|
|
||||||
|
if (route.query.view || route.query.viewType) {
|
||||||
|
let view = getView(route.query.view, route.query.viewType, 'CRM Lead')
|
||||||
|
if (view) {
|
||||||
|
items.push({
|
||||||
|
label: __(view.label),
|
||||||
|
icon: view.icon,
|
||||||
|
route: {
|
||||||
|
name: 'Leads',
|
||||||
|
params: { viewType: route.query.viewType },
|
||||||
|
query: { view: route.query.view },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
label: lead.data.lead_name || __('Untitled'),
|
label: lead.data.lead_name || __('Untitled'),
|
||||||
route: { name: 'Lead', params: { leadId: lead.data.name } },
|
route: { name: 'Lead', params: { leadId: lead.data.name } },
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<LayoutHeader>
|
<LayoutHeader>
|
||||||
<template #left-header>
|
<template #left-header>
|
||||||
<Breadcrumbs :items="breadcrumbs" />
|
<ViewBreadcrumbs v-model="viewControls" routeName="Leads" />
|
||||||
</template>
|
</template>
|
||||||
<template #right-header>
|
<template #right-header>
|
||||||
<CustomActions
|
<CustomActions
|
||||||
@ -33,7 +33,11 @@
|
|||||||
v-if="route.params.viewType == 'kanban'"
|
v-if="route.params.viewType == 'kanban'"
|
||||||
v-model="leads"
|
v-model="leads"
|
||||||
:options="{
|
:options="{
|
||||||
getRoute: (row) => ({ name: 'Lead', params: { leadId: row.name } }),
|
getRoute: (row) => ({
|
||||||
|
name: 'Lead',
|
||||||
|
params: { leadId: row.name },
|
||||||
|
query: { view: route.query.view, viewType: route.params.viewType },
|
||||||
|
}),
|
||||||
onNewClick: (column) => onNewClick(column),
|
onNewClick: (column) => onNewClick(column),
|
||||||
}"
|
}"
|
||||||
@update="(data) => viewControls.updateKanbanSettings(data)"
|
@update="(data) => viewControls.updateKanbanSettings(data)"
|
||||||
@ -281,6 +285,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import ViewBreadcrumbs from '@/components/ViewBreadcrumbs.vue'
|
||||||
import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
||||||
import CustomActions from '@/components/CustomActions.vue'
|
import CustomActions from '@/components/CustomActions.vue'
|
||||||
import EmailAtIcon from '@/components/Icons/EmailAtIcon.vue'
|
import EmailAtIcon from '@/components/Icons/EmailAtIcon.vue'
|
||||||
@ -304,12 +309,10 @@ import { organizationsStore } from '@/stores/organizations'
|
|||||||
import { statusesStore } from '@/stores/statuses'
|
import { statusesStore } from '@/stores/statuses'
|
||||||
import { callEnabled } from '@/composables/settings'
|
import { callEnabled } from '@/composables/settings'
|
||||||
import { dateFormat, dateTooltipFormat, timeAgo, formatTime } from '@/utils'
|
import { dateFormat, dateTooltipFormat, timeAgo, formatTime } from '@/utils'
|
||||||
import { Breadcrumbs, Avatar, Tooltip, Dropdown } from 'frappe-ui'
|
import { Avatar, Tooltip, Dropdown } from 'frappe-ui'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { ref, computed, reactive, h } from 'vue'
|
import { ref, computed, reactive, h } from 'vue'
|
||||||
|
|
||||||
const breadcrumbs = [{ label: __('Leads'), route: { name: 'Leads' } }]
|
|
||||||
|
|
||||||
const { makeCall } = globalStore()
|
const { makeCall } = globalStore()
|
||||||
const { getUser } = usersStore()
|
const { getUser } = usersStore()
|
||||||
const { getOrganization } = organizationsStore()
|
const { getOrganization } = organizationsStore()
|
||||||
|
|||||||
@ -3,7 +3,11 @@
|
|||||||
<header
|
<header
|
||||||
class="relative flex h-12 items-center justify-between gap-2 py-2.5 pl-5"
|
class="relative flex h-12 items-center justify-between gap-2 py-2.5 pl-5"
|
||||||
>
|
>
|
||||||
<Breadcrumbs :items="breadcrumbs" />
|
<Breadcrumbs :items="breadcrumbs">
|
||||||
|
<template #prefix="{ item }">
|
||||||
|
<Icon v-if="item.icon" :icon="item.icon" class="mr-2 h-4" />
|
||||||
|
</template>
|
||||||
|
</Breadcrumbs>
|
||||||
<div class="absolute right-0">
|
<div class="absolute right-0">
|
||||||
<Dropdown :options="statusOptions('deal', updateField)">
|
<Dropdown :options="statusOptions('deal', updateField)">
|
||||||
<template #default="{ open }">
|
<template #default="{ open }">
|
||||||
@ -245,6 +249,7 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import Icon from '@/components/Icon.vue'
|
||||||
import DetailsIcon from '@/components/Icons/DetailsIcon.vue'
|
import DetailsIcon from '@/components/Icons/DetailsIcon.vue'
|
||||||
import LoadingIndicator from '@/components/Icons/LoadingIndicator.vue'
|
import LoadingIndicator from '@/components/Icons/LoadingIndicator.vue'
|
||||||
import ActivityIcon from '@/components/Icons/ActivityIcon.vue'
|
import ActivityIcon from '@/components/Icons/ActivityIcon.vue'
|
||||||
@ -269,16 +274,16 @@ import Section from '@/components/Section.vue'
|
|||||||
import SectionFields from '@/components/SectionFields.vue'
|
import SectionFields from '@/components/SectionFields.vue'
|
||||||
import SLASection from '@/components/SLASection.vue'
|
import SLASection from '@/components/SLASection.vue'
|
||||||
import CustomActions from '@/components/CustomActions.vue'
|
import CustomActions from '@/components/CustomActions.vue'
|
||||||
import {
|
import { createToast, setupAssignees, setupCustomActions } from '@/utils'
|
||||||
createToast,
|
import { getView } from '@/utils/view'
|
||||||
setupAssignees,
|
|
||||||
setupCustomActions,
|
|
||||||
errorMessage,
|
|
||||||
} from '@/utils'
|
|
||||||
import { globalStore } from '@/stores/global'
|
import { globalStore } from '@/stores/global'
|
||||||
import { organizationsStore } from '@/stores/organizations'
|
import { organizationsStore } from '@/stores/organizations'
|
||||||
import { statusesStore } from '@/stores/statuses'
|
import { statusesStore } from '@/stores/statuses'
|
||||||
import { whatsappEnabled, callEnabled, isMobileView } from '@/composables/settings'
|
import {
|
||||||
|
whatsappEnabled,
|
||||||
|
callEnabled,
|
||||||
|
isMobileView,
|
||||||
|
} from '@/composables/settings'
|
||||||
import {
|
import {
|
||||||
createResource,
|
createResource,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
@ -288,11 +293,12 @@ import {
|
|||||||
call,
|
call,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { ref, computed, h, onMounted } from 'vue'
|
import { ref, computed, h, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
const { $dialog, makeCall } = globalStore()
|
const { $dialog } = globalStore()
|
||||||
const { organizations, getOrganization } = organizationsStore()
|
const { organizations, getOrganization } = organizationsStore()
|
||||||
const { statusOptions, getDealStatus } = statusesStore()
|
const { statusOptions, getDealStatus } = statusesStore()
|
||||||
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -385,6 +391,22 @@ function validateRequired(fieldname, value) {
|
|||||||
|
|
||||||
const breadcrumbs = computed(() => {
|
const breadcrumbs = computed(() => {
|
||||||
let items = [{ label: __('Deals'), route: { name: 'Deals' } }]
|
let items = [{ label: __('Deals'), route: { name: 'Deals' } }]
|
||||||
|
|
||||||
|
if (route.query.view || route.query.viewType) {
|
||||||
|
let view = getView(route.query.view, route.query.viewType, 'CRM Deal')
|
||||||
|
if (view) {
|
||||||
|
items.push({
|
||||||
|
label: __(view.label),
|
||||||
|
icon: view.icon,
|
||||||
|
route: {
|
||||||
|
name: 'Deals',
|
||||||
|
params: { viewType: route.query.viewType },
|
||||||
|
query: { view: route.query.view },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
label: organization.value?.name || __('Untitled'),
|
label: organization.value?.name || __('Untitled'),
|
||||||
route: { name: 'Deal', params: { dealId: deal.data.name } },
|
route: { name: 'Deal', params: { dealId: deal.data.name } },
|
||||||
|
|||||||
@ -3,7 +3,11 @@
|
|||||||
<header
|
<header
|
||||||
class="relative flex h-12 items-center justify-between gap-2 py-2.5 pl-5"
|
class="relative flex h-12 items-center justify-between gap-2 py-2.5 pl-5"
|
||||||
>
|
>
|
||||||
<Breadcrumbs :items="breadcrumbs" />
|
<Breadcrumbs :items="breadcrumbs">
|
||||||
|
<template #prefix="{ item }">
|
||||||
|
<Icon v-if="item.icon" :icon="item.icon" class="mr-2 h-4" />
|
||||||
|
</template>
|
||||||
|
</Breadcrumbs>
|
||||||
<div class="absolute right-0">
|
<div class="absolute right-0">
|
||||||
<Dropdown :options="statusOptions('lead', updateField)">
|
<Dropdown :options="statusOptions('lead', updateField)">
|
||||||
<template #default="{ open }">
|
<template #default="{ open }">
|
||||||
@ -138,7 +142,7 @@
|
|||||||
<div v-else class="mt-2.5 text-base">
|
<div v-else class="mt-2.5 text-base">
|
||||||
{{
|
{{
|
||||||
__(
|
__(
|
||||||
'New organization will be created based on the data in details section'
|
'New organization will be created based on the data in details section',
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
@ -170,6 +174,7 @@
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import Icon from '@/components/Icon.vue'
|
||||||
import DetailsIcon from '@/components/Icons/DetailsIcon.vue'
|
import DetailsIcon from '@/components/Icons/DetailsIcon.vue'
|
||||||
import ActivityIcon from '@/components/Icons/ActivityIcon.vue'
|
import ActivityIcon from '@/components/Icons/ActivityIcon.vue'
|
||||||
import EmailIcon from '@/components/Icons/EmailIcon.vue'
|
import EmailIcon from '@/components/Icons/EmailIcon.vue'
|
||||||
@ -191,11 +196,16 @@ import SectionFields from '@/components/SectionFields.vue'
|
|||||||
import SLASection from '@/components/SLASection.vue'
|
import SLASection from '@/components/SLASection.vue'
|
||||||
import CustomActions from '@/components/CustomActions.vue'
|
import CustomActions from '@/components/CustomActions.vue'
|
||||||
import { createToast, setupAssignees, setupCustomActions } from '@/utils'
|
import { createToast, setupAssignees, setupCustomActions } from '@/utils'
|
||||||
|
import { getView } from '@/utils/view'
|
||||||
import { globalStore } from '@/stores/global'
|
import { globalStore } from '@/stores/global'
|
||||||
import { contactsStore } from '@/stores/contacts'
|
import { contactsStore } from '@/stores/contacts'
|
||||||
import { organizationsStore } from '@/stores/organizations'
|
import { organizationsStore } from '@/stores/organizations'
|
||||||
import { statusesStore } from '@/stores/statuses'
|
import { statusesStore } from '@/stores/statuses'
|
||||||
import { whatsappEnabled, callEnabled, isMobileView } from '@/composables/settings'
|
import {
|
||||||
|
whatsappEnabled,
|
||||||
|
callEnabled,
|
||||||
|
isMobileView,
|
||||||
|
} from '@/composables/settings'
|
||||||
import {
|
import {
|
||||||
createResource,
|
createResource,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
@ -298,6 +308,22 @@ function validateRequired(fieldname, value) {
|
|||||||
|
|
||||||
const breadcrumbs = computed(() => {
|
const breadcrumbs = computed(() => {
|
||||||
let items = [{ label: __('Leads'), route: { name: 'Leads' } }]
|
let items = [{ label: __('Leads'), route: { name: 'Leads' } }]
|
||||||
|
|
||||||
|
if (route.query.view || route.query.viewType) {
|
||||||
|
let view = getView(route.query.view, route.query.viewType, 'CRM Lead')
|
||||||
|
if (view) {
|
||||||
|
items.push({
|
||||||
|
label: __(view.label),
|
||||||
|
icon: view.icon,
|
||||||
|
route: {
|
||||||
|
name: 'Leads',
|
||||||
|
params: { viewType: route.query.viewType },
|
||||||
|
query: { view: route.query.view },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
label: lead.data.lead_name || __('Untitled'),
|
label: lead.data.lead_name || __('Untitled'),
|
||||||
route: { name: 'Lead', params: { leadId: lead.data.name } },
|
route: { name: 'Lead', params: { leadId: lead.data.name } },
|
||||||
@ -359,7 +385,7 @@ const tabs = computed(() => {
|
|||||||
watch(tabs, (value) => {
|
watch(tabs, (value) => {
|
||||||
if (value && route.params.tabName) {
|
if (value && route.params.tabName) {
|
||||||
let index = value.findIndex(
|
let index = value.findIndex(
|
||||||
(tab) => tab.name.toLowerCase() === route.params.tabName.toLowerCase()
|
(tab) => tab.name.toLowerCase() === route.params.tabName.toLowerCase(),
|
||||||
)
|
)
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
tabIndex.value = index
|
tabIndex.value = index
|
||||||
@ -447,7 +473,7 @@ async function convertToDeal(updated) {
|
|||||||
organization: lead.data.organization,
|
organization: lead.data.organization,
|
||||||
},
|
},
|
||||||
'',
|
'',
|
||||||
() => convertToDeal(true)
|
() => convertToDeal(true),
|
||||||
)
|
)
|
||||||
showConvertToDealModal.value = false
|
showConvertToDealModal.value = false
|
||||||
} else {
|
} else {
|
||||||
@ -455,7 +481,7 @@ async function convertToDeal(updated) {
|
|||||||
'crm.fcrm.doctype.crm_lead.crm_lead.convert_to_deal',
|
'crm.fcrm.doctype.crm_lead.crm_lead.convert_to_deal',
|
||||||
{
|
{
|
||||||
lead: lead.data.name,
|
lead: lead.data.name,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
if (deal) {
|
if (deal) {
|
||||||
if (updated) {
|
if (updated) {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<LayoutHeader>
|
<LayoutHeader>
|
||||||
<template #left-header>
|
<template #left-header>
|
||||||
<Breadcrumbs :items="breadcrumbs" />
|
<ViewBreadcrumbs v-model="viewControls" routeName="Notes" />
|
||||||
</template>
|
</template>
|
||||||
<template #right-header>
|
<template #right-header>
|
||||||
<Button variant="solid" :label="__('Create')" @click="createNote">
|
<Button variant="solid" :label="__('Create')" @click="createNote">
|
||||||
@ -10,6 +10,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</LayoutHeader>
|
</LayoutHeader>
|
||||||
<ViewControls
|
<ViewControls
|
||||||
|
ref="viewControls"
|
||||||
v-model="notes"
|
v-model="notes"
|
||||||
v-model:loadMore="loadMore"
|
v-model:loadMore="loadMore"
|
||||||
v-model:updatedPageCount="updatedPageCount"
|
v-model:updatedPageCount="updatedPageCount"
|
||||||
@ -102,6 +103,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import ViewBreadcrumbs from '@/components/ViewBreadcrumbs.vue'
|
||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
||||||
@ -109,33 +111,25 @@ import NoteModal from '@/components/Modals/NoteModal.vue'
|
|||||||
import ViewControls from '@/components/ViewControls.vue'
|
import ViewControls from '@/components/ViewControls.vue'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { timeAgo, dateFormat, dateTooltipFormat } from '@/utils'
|
import { timeAgo, dateFormat, dateTooltipFormat } from '@/utils'
|
||||||
import {
|
import { TextEditor, call, Dropdown, Tooltip, ListFooter } from 'frappe-ui'
|
||||||
TextEditor,
|
|
||||||
call,
|
|
||||||
Dropdown,
|
|
||||||
Tooltip,
|
|
||||||
Breadcrumbs,
|
|
||||||
ListFooter,
|
|
||||||
} from 'frappe-ui'
|
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
const { getUser } = usersStore()
|
const { getUser } = usersStore()
|
||||||
|
|
||||||
const breadcrumbs = [{ label: __('Notes'), route: { name: 'Notes' } }]
|
|
||||||
|
|
||||||
const showNoteModal = ref(false)
|
const showNoteModal = ref(false)
|
||||||
const currentNote = ref(null)
|
const currentNote = ref(null)
|
||||||
|
|
||||||
const notes = ref({})
|
const notes = ref({})
|
||||||
const loadMore = ref(1)
|
const loadMore = ref(1)
|
||||||
const updatedPageCount = ref(20)
|
const updatedPageCount = ref(20)
|
||||||
|
const viewControls = ref(null)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => notes.value?.data?.page_length_count,
|
() => notes.value?.data?.page_length_count,
|
||||||
(val, old_value) => {
|
(val, old_value) => {
|
||||||
if (!val || val === old_value) return
|
if (!val || val === old_value) return
|
||||||
updatedPageCount.value = val
|
updatedPageCount.value = val
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
function createNote() {
|
function createNote() {
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<LayoutHeader v-if="organization.doc">
|
<LayoutHeader v-if="organization.doc">
|
||||||
<template #left-header>
|
<template #left-header>
|
||||||
<Breadcrumbs :items="breadcrumbs" />
|
<Breadcrumbs :items="breadcrumbs">
|
||||||
|
<template #prefix="{ item }">
|
||||||
|
<Icon v-if="item.icon" :icon="item.icon" class="mr-2 h-4" />
|
||||||
|
</template>
|
||||||
|
</Breadcrumbs>
|
||||||
</template>
|
</template>
|
||||||
</LayoutHeader>
|
</LayoutHeader>
|
||||||
<div v-if="organization.doc" class="flex flex-1 flex-col overflow-hidden">
|
<div v-if="organization.doc" class="flex flex-1 flex-col overflow-hidden">
|
||||||
@ -226,17 +230,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import Icon from '@/components/Icon.vue'
|
||||||
Breadcrumbs,
|
|
||||||
Avatar,
|
|
||||||
FileUploader,
|
|
||||||
Dropdown,
|
|
||||||
Tabs,
|
|
||||||
call,
|
|
||||||
createListResource,
|
|
||||||
createDocumentResource,
|
|
||||||
usePageMeta,
|
|
||||||
} from 'frappe-ui'
|
|
||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
|
import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
|
||||||
import QuickEntryModal from '@/components/Settings/QuickEntryModal.vue'
|
import QuickEntryModal from '@/components/Settings/QuickEntryModal.vue'
|
||||||
@ -252,14 +246,26 @@ import ContactsIcon from '@/components/Icons/ContactsIcon.vue'
|
|||||||
import { globalStore } from '@/stores/global'
|
import { globalStore } from '@/stores/global'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { statusesStore } from '@/stores/statuses'
|
import { statusesStore } from '@/stores/statuses'
|
||||||
|
import { getView } from '@/utils/view'
|
||||||
import {
|
import {
|
||||||
dateFormat,
|
dateFormat,
|
||||||
dateTooltipFormat,
|
dateTooltipFormat,
|
||||||
timeAgo,
|
timeAgo,
|
||||||
formatNumberIntoCurrency,
|
formatNumberIntoCurrency,
|
||||||
} from '@/utils'
|
} from '@/utils'
|
||||||
|
import {
|
||||||
|
Breadcrumbs,
|
||||||
|
Avatar,
|
||||||
|
FileUploader,
|
||||||
|
Dropdown,
|
||||||
|
Tabs,
|
||||||
|
call,
|
||||||
|
createListResource,
|
||||||
|
createDocumentResource,
|
||||||
|
usePageMeta,
|
||||||
|
} from 'frappe-ui'
|
||||||
import { h, computed, ref } from 'vue'
|
import { h, computed, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
organizationId: {
|
organizationId: {
|
||||||
@ -274,6 +280,7 @@ const showOrganizationModal = ref(false)
|
|||||||
const showQuickEntryModal = ref(false)
|
const showQuickEntryModal = ref(false)
|
||||||
const detailMode = ref(false)
|
const detailMode = ref(false)
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const organization = createDocumentResource({
|
const organization = createDocumentResource({
|
||||||
@ -286,6 +293,26 @@ const organization = createDocumentResource({
|
|||||||
|
|
||||||
const breadcrumbs = computed(() => {
|
const breadcrumbs = computed(() => {
|
||||||
let items = [{ label: __('Organizations'), route: { name: 'Organizations' } }]
|
let items = [{ label: __('Organizations'), route: { name: 'Organizations' } }]
|
||||||
|
|
||||||
|
if (route.query.view || route.query.viewType) {
|
||||||
|
let view = getView(
|
||||||
|
route.query.view,
|
||||||
|
route.query.viewType,
|
||||||
|
'CRM Organization',
|
||||||
|
)
|
||||||
|
if (view) {
|
||||||
|
items.push({
|
||||||
|
label: __(view.label),
|
||||||
|
icon: view.icon,
|
||||||
|
route: {
|
||||||
|
name: 'Organizations',
|
||||||
|
params: { viewType: route.query.viewType },
|
||||||
|
query: { view: route.query.view },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
label: props.organizationId,
|
label: props.organizationId,
|
||||||
route: {
|
route: {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<LayoutHeader>
|
<LayoutHeader>
|
||||||
<template #left-header>
|
<template #left-header>
|
||||||
<Breadcrumbs :items="breadcrumbs" />
|
<ViewBreadcrumbs v-model="viewControls" routeName="Organizations" />
|
||||||
</template>
|
</template>
|
||||||
<template #right-header>
|
<template #right-header>
|
||||||
<CustomActions
|
<CustomActions
|
||||||
@ -70,6 +70,7 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import ViewBreadcrumbs from '@/components/ViewBreadcrumbs.vue'
|
||||||
import CustomActions from '@/components/CustomActions.vue'
|
import CustomActions from '@/components/CustomActions.vue'
|
||||||
import OrganizationsIcon from '@/components/Icons/OrganizationsIcon.vue'
|
import OrganizationsIcon from '@/components/Icons/OrganizationsIcon.vue'
|
||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
@ -77,7 +78,6 @@ import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
|
|||||||
import QuickEntryModal from '@/components/Settings/QuickEntryModal.vue'
|
import QuickEntryModal from '@/components/Settings/QuickEntryModal.vue'
|
||||||
import OrganizationsListView from '@/components/ListViews/OrganizationsListView.vue'
|
import OrganizationsListView from '@/components/ListViews/OrganizationsListView.vue'
|
||||||
import ViewControls from '@/components/ViewControls.vue'
|
import ViewControls from '@/components/ViewControls.vue'
|
||||||
import { Breadcrumbs } from 'frappe-ui'
|
|
||||||
import {
|
import {
|
||||||
dateFormat,
|
dateFormat,
|
||||||
dateTooltipFormat,
|
dateTooltipFormat,
|
||||||
@ -85,33 +85,11 @@ import {
|
|||||||
formatNumberIntoCurrency,
|
formatNumberIntoCurrency,
|
||||||
} from '@/utils'
|
} from '@/utils'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
|
|
||||||
const organizationsListView = ref(null)
|
const organizationsListView = ref(null)
|
||||||
const showOrganizationModal = ref(false)
|
const showOrganizationModal = ref(false)
|
||||||
const showQuickEntryModal = ref(false)
|
const showQuickEntryModal = ref(false)
|
||||||
|
|
||||||
const currentOrganization = computed(() => {
|
|
||||||
return organizations.value?.data?.data?.find(
|
|
||||||
(organization) => organization.name === route.params.organizationId,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const breadcrumbs = computed(() => {
|
|
||||||
let items = [{ label: __('Organizations'), route: { name: 'Organizations' } }]
|
|
||||||
if (!currentOrganization.value) return items
|
|
||||||
items.push({
|
|
||||||
label: __(currentOrganization.value.name),
|
|
||||||
route: {
|
|
||||||
name: 'Organization',
|
|
||||||
params: { organizationId: currentOrganization.value.name },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return items
|
|
||||||
})
|
|
||||||
|
|
||||||
// organizations data is loaded in the ViewControls component
|
// organizations data is loaded in the ViewControls component
|
||||||
const organizations = ref({})
|
const organizations = ref({})
|
||||||
const loadMore = ref(1)
|
const loadMore = ref(1)
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<LayoutHeader>
|
<LayoutHeader>
|
||||||
<template #left-header>
|
<template #left-header>
|
||||||
<Breadcrumbs :items="breadcrumbs" />
|
<ViewBreadcrumbs v-model="viewControls" routeName="Tasks" />
|
||||||
</template>
|
</template>
|
||||||
<template #right-header>
|
<template #right-header>
|
||||||
<CustomActions
|
<CustomActions
|
||||||
@ -193,6 +193,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import ViewBreadcrumbs from '@/components/ViewBreadcrumbs.vue'
|
||||||
import CustomActions from '@/components/CustomActions.vue'
|
import CustomActions from '@/components/CustomActions.vue'
|
||||||
import ArrowUpRightIcon from '@/components/Icons/ArrowUpRightIcon.vue'
|
import ArrowUpRightIcon from '@/components/Icons/ArrowUpRightIcon.vue'
|
||||||
import TaskStatusIcon from '@/components/Icons/TaskStatusIcon.vue'
|
import TaskStatusIcon from '@/components/Icons/TaskStatusIcon.vue'
|
||||||
@ -205,19 +206,10 @@ import KanbanView from '@/components/Kanban/KanbanView.vue'
|
|||||||
import TaskModal from '@/components/Modals/TaskModal.vue'
|
import TaskModal from '@/components/Modals/TaskModal.vue'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { dateFormat, dateTooltipFormat, timeAgo } from '@/utils'
|
import { dateFormat, dateTooltipFormat, timeAgo } from '@/utils'
|
||||||
import {
|
import { Tooltip, Avatar, TextEditor, Dropdown, call } from 'frappe-ui'
|
||||||
Breadcrumbs,
|
|
||||||
Tooltip,
|
|
||||||
Avatar,
|
|
||||||
TextEditor,
|
|
||||||
Dropdown,
|
|
||||||
call,
|
|
||||||
} from 'frappe-ui'
|
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const breadcrumbs = [{ label: __('Tasks'), route: { name: 'Tasks' } }]
|
|
||||||
|
|
||||||
const { getUser } = usersStore()
|
const { getUser } = usersStore()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|||||||
@ -40,7 +40,8 @@ const routes = [
|
|||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/notes',
|
alias: '/notes',
|
||||||
|
path: '/notes/view/:viewType?',
|
||||||
name: 'Notes',
|
name: 'Notes',
|
||||||
component: () => import('@/pages/Notes.vue'),
|
component: () => import('@/pages/Notes.vue'),
|
||||||
},
|
},
|
||||||
@ -51,7 +52,8 @@ const routes = [
|
|||||||
component: () => import('@/pages/Tasks.vue'),
|
component: () => import('@/pages/Tasks.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/contacts',
|
alias: '/contacts',
|
||||||
|
path: '/contacts/view/:viewType?',
|
||||||
name: 'Contacts',
|
name: 'Contacts',
|
||||||
component: () => import('@/pages/Contacts.vue'),
|
component: () => import('@/pages/Contacts.vue'),
|
||||||
meta: { scrollPos: { top: 0, left: 0 } },
|
meta: { scrollPos: { top: 0, left: 0 } },
|
||||||
@ -63,7 +65,8 @@ const routes = [
|
|||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/organizations',
|
alias: '/organizations',
|
||||||
|
path: '/organizations/view/:viewType?',
|
||||||
name: 'Organizations',
|
name: 'Organizations',
|
||||||
component: () => import('@/pages/Organizations.vue'),
|
component: () => import('@/pages/Organizations.vue'),
|
||||||
meta: { scrollPos: { top: 0, left: 0 } },
|
meta: { scrollPos: { top: 0, left: 0 } },
|
||||||
@ -75,13 +78,15 @@ const routes = [
|
|||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/call-logs',
|
alias: '/call-logs',
|
||||||
|
path: '/call-logs/view/:viewType?',
|
||||||
name: 'Call Logs',
|
name: 'Call Logs',
|
||||||
component: () => import('@/pages/CallLogs.vue'),
|
component: () => import('@/pages/CallLogs.vue'),
|
||||||
meta: { scrollPos: { top: 0, left: 0 } },
|
meta: { scrollPos: { top: 0, left: 0 } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/email-templates',
|
alias: '/email-templates',
|
||||||
|
path: '/email-templates/view/:viewType?',
|
||||||
name: 'Email Templates',
|
name: 'Email Templates',
|
||||||
component: () => import('@/pages/EmailTemplates.vue'),
|
component: () => import('@/pages/EmailTemplates.vue'),
|
||||||
meta: { scrollPos: { top: 0, left: 0 } },
|
meta: { scrollPos: { top: 0, left: 0 } },
|
||||||
@ -92,11 +97,6 @@ const routes = [
|
|||||||
component: () => import('@/pages/EmailTemplate.vue'),
|
component: () => import('@/pages/EmailTemplate.vue'),
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/dashboard',
|
|
||||||
name: 'Dashboard',
|
|
||||||
component: () => import('@/pages/Dashboard.vue'),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/:invalidpath',
|
path: '/:invalidpath',
|
||||||
name: 'Invalid Page',
|
name: 'Invalid Page',
|
||||||
|
|||||||
35
frontend/src/utils/view.js
Normal file
35
frontend/src/utils/view.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import ListIcon from '@/components/Icons/ListIcon.vue'
|
||||||
|
import GroupByIcon from '@/components/Icons/GroupByIcon.vue'
|
||||||
|
import KanbanIcon from '@/components/Icons/KanbanIcon.vue'
|
||||||
|
import { viewsStore } from '@/stores/views'
|
||||||
|
import { markRaw } from 'vue'
|
||||||
|
|
||||||
|
const { getView: getViewDetails } = viewsStore()
|
||||||
|
|
||||||
|
function defaultView(type) {
|
||||||
|
let types = {
|
||||||
|
list: {
|
||||||
|
label: __('List'),
|
||||||
|
icon: markRaw(ListIcon),
|
||||||
|
},
|
||||||
|
group_by: {
|
||||||
|
label: __('Group By'),
|
||||||
|
icon: markRaw(GroupByIcon),
|
||||||
|
},
|
||||||
|
kanban: {
|
||||||
|
label: __('Kanban'),
|
||||||
|
icon: markRaw(KanbanIcon),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return types[type]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getView(view, type, doctype) {
|
||||||
|
let viewType = type || 'list'
|
||||||
|
let viewDetails = getViewDetails(view, viewType, doctype)
|
||||||
|
if (viewDetails && !viewDetails.icon) {
|
||||||
|
viewDetails.icon = defaultView(viewType).icon
|
||||||
|
}
|
||||||
|
return viewDetails || defaultView(viewType)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user