Merge pull request #194 from frappe/develop

chore: Merge develop to main
This commit is contained in:
Shariq Ansari 2024-05-21 16:58:34 +05:30 committed by GitHub
commit 53cc1df965
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 434 additions and 666 deletions

View File

@ -376,7 +376,7 @@ def get_type(field):
return "read_only"
return field.fieldtype.lower()
def get_assigned_users(doctype, name):
def get_assigned_users(doctype, name, default_assigned_to=None):
assigned_users = frappe.get_all(
"ToDo",
fields=["allocated_to"],
@ -388,7 +388,12 @@ def get_assigned_users(doctype, name):
pluck="allocated_to",
)
return list(set(assigned_users))
users = list(set(assigned_users))
# if users is empty, add default_assigned_to
if not users and default_assigned_to:
users = [default_assigned_to]
return users
@frappe.whitelist()

View File

@ -30,7 +30,7 @@ def get_deal(name):
deal["doctype_fields"], deal["all_fields"] = get_doctype_fields("CRM Deal", name)
deal["doctype"] = "CRM Deal"
deal["_form_script"] = get_form_script('CRM Deal')
deal["_assign"] = get_assigned_users("CRM Deal", deal.name)
deal["_assign"] = get_assigned_users("CRM Deal", deal.name, deal.owner)
return deal
@frappe.whitelist()

View File

@ -108,8 +108,8 @@ class CRMDeal(Document):
"""
sla = get_sla(self)
if not sla:
self.first_responded_on = None
self.first_response_time = None
# self.first_responded_on = None
# self.first_response_time = None
return
self.sla = sla.name

View File

@ -18,5 +18,5 @@ def get_lead(name):
lead["doctype_fields"], lead["all_fields"] = get_doctype_fields("CRM Lead", name)
lead["doctype"] = "CRM Lead"
lead["_form_script"] = get_form_script('CRM Lead')
lead["_assign"] = get_assigned_users("CRM Lead", lead.name)
lead["_assign"] = get_assigned_users("CRM Lead", lead.name, lead.owner)
return lead

View File

@ -234,8 +234,8 @@ class CRMLead(Document):
"""
sla = get_sla(self)
if not sla:
self.first_responded_on = None
self.first_response_time = None
# self.first_responded_on = None
# self.first_response_time = None
return
self.sla = sla.name

View File

@ -15,6 +15,16 @@
</template>
<span>{{ __('New Email') }}</span>
</Button>
<Button
v-else-if="title == 'Comments'"
variant="solid"
@click="$refs.emailBox.showComment = true"
>
<template #prefix>
<FeatherIcon name="plus" class="h-4 w-4" />
</template>
<span>{{ __('New Comment') }}</span>
</Button>
<Button
v-else-if="title == 'Calls'"
variant="solid"
@ -146,6 +156,62 @@
</div>
</div>
</div>
<div v-else-if="title == 'Comments'" class="activity pb-5">
<div v-for="(comment, i) in activities">
<div class="grid grid-cols-[30px_minmax(auto,_1fr)] gap-4 px-10">
<div
class="relative flex justify-center after:absolute after:left-[50%] after:top-0 after:-z-10 after:border-l after:border-gray-200"
:class="i != activities.length - 1 ? 'after:h-full' : 'after:h-4'"
>
<div
class="z-10 flex h-7 w-7 items-center justify-center rounded bg-gray-100"
>
<CommentIcon class="text-gray-800" />
</div>
</div>
<div class="mb-4" :id="comment.name">
<div
class="mb-0.5 flex items-start justify-stretch gap-2 py-1.5 text-base"
>
<div class="inline-flex flex-wrap gap-1 text-gray-600">
<span class="font-medium text-gray-800">
{{ comment.owner_name }}
</span>
<span>{{ __('added a') }}</span>
<span class="max-w-xs truncate font-medium text-gray-800">
{{ __('comment') }}
</span>
</div>
<div class="ml-auto whitespace-nowrap">
<Tooltip
:text="dateFormat(comment.creation, dateTooltipFormat)"
>
<div class="text-sm text-gray-600">
{{ __(timeAgo(comment.creation)) }}
</div>
</Tooltip>
</div>
</div>
<div
class="cursor-pointer rounded bg-gray-50 px-4 py-3 text-base leading-6 transition-all duration-300 ease-in-out"
>
<div class="prose-f" v-html="comment.content" />
<div
v-if="comment.attachments.length"
class="mt-2 flex flex-wrap gap-2"
>
<AttachmentItem
v-for="a in comment.attachments"
:key="a.file_url"
:label="a.file_name"
:url="a.file_url"
/>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-else-if="title == 'Tasks'" class="activity px-10 pb-5">
<div v-for="(task, i) in activities">
<div
@ -756,6 +822,11 @@
:label="__('New Email')"
@click="$refs.emailBox.show = true"
/>
<Button
v-else-if="title == 'Comments'"
:label="__('New Comment')"
@click="$refs.emailBox.showComment = true"
/>
<Button
v-else-if="title == 'Tasks'"
:label="__('Create Task')"
@ -764,7 +835,7 @@
</div>
<CommunicationArea
ref="emailBox"
v-if="['Emails', 'Activity'].includes(title)"
v-if="['Emails', 'Comments', 'Activity'].includes(title)"
v-model="doc"
v-model:reload="reload_email"
:doctype="doctype"
@ -983,6 +1054,11 @@ const defaultActions = computed(() => {
label: __('New Email'),
onClick: () => (emailBox.value.show = true),
},
{
icon: h(CommentIcon, { class: 'h-4 w-4' }),
label: __('New Comment'),
onClick: () => (emailBox.value.showComment = true),
},
{
icon: h(PhoneIcon, { class: 'h-4 w-4' }),
label: __('Make a Call'),
@ -1027,6 +1103,11 @@ const activities = computed(() => {
activities = all_activities.data.versions.filter(
(activity) => activity.activity_type === 'communication'
)
} else if (props.title == 'Comments') {
if (!all_activities.data?.versions) return []
activities = all_activities.data.versions.filter(
(activity) => activity.activity_type === 'comment'
)
} else if (props.title == 'Calls') {
if (!all_activities.data?.calls) return []
return sortByCreation(all_activities.data.calls)
@ -1089,6 +1170,8 @@ const emptyText = computed(() => {
let text = 'No Activities'
if (props.title == 'Emails') {
text = 'No Email Communications'
} else if (props.title == 'Comments') {
text = 'No Comments'
} else if (props.title == 'Calls') {
text = 'No Call Logs'
} else if (props.title == 'Notes') {
@ -1105,6 +1188,8 @@ const emptyTextIcon = computed(() => {
let icon = ActivityIcon
if (props.title == 'Emails') {
icon = EmailIcon
} else if (props.title == 'Comments') {
icon = CommentIcon
} else if (props.title == 'Calls') {
icon = PhoneIcon
} else if (props.title == 'Notes') {

View File

@ -248,5 +248,5 @@ function toggleCommentBox() {
showCommentBox.value = !showCommentBox.value
}
defineExpose({ show: showEmailBox, editor: newEmailEditor })
defineExpose({ show: showEmailBox, showComment: showCommentBox, editor: newEmailEditor })
</script>

View File

@ -66,7 +66,7 @@
<script setup>
import { gemoji } from 'gemoji'
import { Popover } from 'frappe-ui'
import { ref, computed, onMounted } from 'vue'
import { ref, computed } from 'vue'
const search = ref('')
const emoji = defineModel()
@ -109,9 +109,5 @@ function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min)
}
onMounted(() => {
if (!emoji.value) setRandom()
})
defineExpose({ setRandom })
</script>

View File

@ -0,0 +1,205 @@
<template>
<EditValueModal
v-model="showEditModal"
:doctype="doctype"
:selectedValues="selectedValues"
@reload="reload"
/>
<AssignmentModal
v-if="selectedValues"
:docs="selectedValues"
:doctype="doctype"
v-model="showAssignmentModal"
v-model:assignees="bulkAssignees"
@reload="reload"
/>
</template>
<script setup>
import EditValueModal from '@/components/Modals/EditValueModal.vue'
import AssignmentModal from '@/components/Modals/AssignmentModal.vue'
import { setupListActions, createToast } from '@/utils'
import { globalStore } from '@/stores/global'
import { call } from 'frappe-ui'
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
const props = defineProps({
doctype: {
type: String,
default: '',
},
options: {
type: Object,
default: () => ({
hideEdit: false,
hideDelete: false,
hideAssign: false,
}),
},
})
const list = defineModel()
const router = useRouter()
const { $dialog } = globalStore()
const showEditModal = ref(false)
const selectedValues = ref([])
const unselectAllAction = ref(() => {})
function editValues(selections, unselectAll) {
selectedValues.value = selections
showEditModal.value = true
unselectAllAction.value = unselectAll
}
function deleteValues(selections, unselectAll) {
$dialog({
title: __('Delete'),
message: __('Are you sure you want to delete {0} item(s)?', [
selections.size,
]),
variant: 'solid',
theme: 'red',
actions: [
{
label: __('Delete'),
variant: 'solid',
theme: 'red',
onClick: (close) => {
call('frappe.desk.reportview.delete_items', {
items: JSON.stringify(Array.from(selections)),
doctype: props.doctype,
}).then(() => {
createToast({
title: __('Deleted successfully'),
icon: 'check',
iconClasses: 'text-green-600',
})
unselectAll()
list.value.reload()
close()
})
},
},
],
})
}
const showAssignmentModal = ref(false)
const bulkAssignees = ref([])
function assignValues(selections, unselectAll) {
showAssignmentModal.value = true
selectedValues.value = selections
unselectAllAction.value = unselectAll
}
function clearAssignemnts(selections, unselectAll) {
$dialog({
title: __('Clear Assignment'),
message: __('Are you sure you want to clear assignment for {0} item(s)?', [
selections.size,
]),
variant: 'solid',
theme: 'red',
actions: [
{
label: __('Clear Assignment'),
variant: 'solid',
theme: 'red',
onClick: (close) => {
call('frappe.desk.form.assign_to.remove_multiple', {
doctype: props.doctype,
names: JSON.stringify(Array.from(selections)),
ignore_permissions: true,
}).then(() => {
createToast({
title: __('Assignment cleared successfully'),
icon: 'check',
iconClasses: 'text-green-600',
})
reload(unselectAll)
close()
})
},
},
],
})
}
const customBulkActions = ref([])
const customListActions = ref([])
function bulkActions(selections, unselectAll) {
let actions = []
if (!props.options.hideEdit) {
actions.push({
label: __('Edit'),
onClick: () => editValues(selections, unselectAll),
})
}
if (!props.options.hideDelete) {
actions.push({
label: __('Delete'),
onClick: () => deleteValues(selections, unselectAll),
})
}
if (!props.options.hideAssign) {
actions.push({
label: __('Assign To'),
onClick: () => assignValues(selections, unselectAll),
})
actions.push({
label: __('Clear Assignment'),
onClick: () => clearAssignemnts(selections, unselectAll),
})
}
customBulkActions.value.forEach((action) => {
actions.push({
label: __(action.label),
onClick: () =>
action.onClick({
list: list.value,
selections,
unselectAll,
call,
createToast,
$dialog,
router,
}),
})
})
return actions
}
function reload(unselectAll) {
unselectAllAction.value?.()
unselectAll?.()
list.value?.reload()
}
onMounted(() => {
if (!list.value?.data) return
setupListActions(list.value.data, {
list: list.value,
call,
createToast,
$dialog,
router,
})
customBulkActions.value = list.value?.data?.bulkActions || []
customListActions.value = list.value?.data?.listActions || []
})
defineExpose({
bulkActions,
customListActions,
})
</script>

View File

@ -80,7 +80,9 @@
</ListRows>
<ListSelectBanner>
<template #actions="{ selections, unselectAll }">
<Dropdown :options="bulkActions(selections, unselectAll)">
<Dropdown
:options="listBulkActionsRef.bulkActions(selections, unselectAll)"
>
<Button icon="more-horizontal" variant="ghost" />
</Dropdown>
</template>
@ -95,8 +97,18 @@
}"
@loadMore="emit('loadMore')"
/>
<ListBulkActions
ref="listBulkActionsRef"
v-model="list"
doctype="CRM Call Log"
:options="{
hideEdit: true,
hideAssign: true,
}"
/>
</template>
<script setup>
import ListBulkActions from '@/components/ListBulkActions.vue'
import {
Avatar,
ListView,
@ -108,12 +120,8 @@ import {
ListFooter,
Tooltip,
Dropdown,
call,
} from 'frappe-ui'
import { setupListActions, createToast } from '@/utils'
import { globalStore } from '@/stores/global'
import { onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { ref, watch } from 'vue'
const props = defineProps({
rows: {
@ -146,89 +154,14 @@ const emit = defineEmits([
const pageLengthCount = defineModel()
const list = defineModel('list')
const router = useRouter()
const { $dialog } = globalStore()
watch(pageLengthCount, (val, old_value) => {
if (val === old_value) return
emit('updatePageCount', val)
})
function deleteValues(selections, unselectAll) {
$dialog({
title: 'Delete',
message: `Are you sure you want to delete ${selections.size} item${
selections.size > 1 ? 's' : ''
}?`,
variant: 'danger',
actions: [
{
label: 'Delete',
variant: 'solid',
theme: 'red',
onClick: (close) => {
call('frappe.desk.reportview.delete_items', {
items: JSON.stringify(Array.from(selections)),
doctype: 'CRM Call Log',
}).then(() => {
createToast({
title: 'Deleted successfully',
icon: 'check',
iconClasses: 'text-green-600',
})
unselectAll()
list.value.reload()
close()
})
},
},
],
})
}
const customBulkActions = ref([])
const customListActions = ref([])
function bulkActions(selections, unselectAll) {
let actions = [
{
label: 'Delete',
onClick: () => deleteValues(selections, unselectAll),
},
]
customBulkActions.value.forEach((action) => {
actions.push({
label: action.label,
onClick: () =>
action.onClick({
list: list.value,
selections,
unselectAll,
call,
createToast,
$dialog,
router,
}),
})
})
return actions
}
onMounted(() => {
if (!list.value?.data) return
setupListActions(list.value.data, {
list: list.value,
call,
createToast,
$dialog,
router,
})
customBulkActions.value = list.value?.data?.bulkActions || []
customListActions.value = list.value?.data?.listActions || []
})
const listBulkActionsRef = ref(null)
defineExpose({
customListActions,
customListActions: listBulkActionsRef.value?.customListActions,
})
</script>

View File

@ -82,7 +82,9 @@
</ListRows>
<ListSelectBanner>
<template #actions="{ selections, unselectAll }">
<Dropdown :options="bulkActions(selections, unselectAll)">
<Dropdown
:options="listBulkActionsRef.bulkActions(selections, unselectAll)"
>
<Button icon="more-horizontal" variant="ghost" />
</Dropdown>
</template>
@ -98,19 +100,18 @@
}"
@loadMore="emit('loadMore')"
/>
<EditValueModal
v-model="showEditModal"
v-model:unselectAll="unselectAllAction"
<ListBulkActions
ref="listBulkActionsRef"
v-model="list"
doctype="Contact"
:selectedValues="selectedValues"
@reload="list.reload()"
:options="{
hideAssign: true,
}"
/>
</template>
<script setup>
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import EditValueModal from '@/components/Modals/EditValueModal.vue'
import { globalStore } from '@/stores/global'
import { setupListActions, createToast } from '@/utils'
import ListBulkActions from '@/components/ListBulkActions.vue'
import {
Avatar,
ListView,
@ -122,10 +123,8 @@ import {
ListFooter,
Tooltip,
Dropdown,
call,
} from 'frappe-ui'
import { ref, watch, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ref, watch } from 'vue'
const props = defineProps({
rows: {
@ -158,87 +157,14 @@ const emit = defineEmits([
const pageLengthCount = defineModel()
const list = defineModel('list')
const router = useRouter()
const { $dialog } = globalStore()
watch(pageLengthCount, (val, old_value) => {
if (val === old_value) return
emit('updatePageCount', val)
})
const showEditModal = ref(false)
const selectedValues = ref([])
const unselectAllAction = ref(() => {})
function editValues(selections, unselectAll) {
selectedValues.value = selections
showEditModal.value = true
unselectAllAction.value = unselectAll
}
function deleteValues(selections, unselectAll) {
$dialog({
title: __('Delete'),
message: __('Are you sure you want to delete {0} item(s)?', [
selections.size,
]),
variant: 'danger',
actions: [
{
label: __('Delete'),
variant: 'solid',
theme: 'red',
onClick: (close) => {
call('frappe.desk.reportview.delete_items', {
items: JSON.stringify(Array.from(selections)),
doctype: 'Contact',
}).then(() => {
createToast({
title: __('Deleted successfully'),
icon: 'check',
iconClasses: 'text-green-600',
})
unselectAll()
list.value.reload()
close()
})
},
},
],
})
}
const customListActions = ref([])
function bulkActions(selections, unselectAll) {
let actions = [
{
label: __('Edit'),
onClick: () => editValues(selections, unselectAll),
},
{
label: __('Delete'),
onClick: () => deleteValues(selections, unselectAll),
},
]
return actions
}
onMounted(() => {
if (!list.value?.data) return
setupListActions(list.value.data, {
list: list.value,
call,
createToast,
$dialog,
router,
})
// customBulkActions.value = list.value?.data?.bulkActions || []
customListActions.value = list.value?.data?.listActions || []
})
const listBulkActionsRef = ref(null)
defineExpose({
customListActions,
customListActions: listBulkActionsRef.value?.customListActions,
})
</script>

View File

@ -114,7 +114,9 @@
</ListRows>
<ListSelectBanner>
<template #actions="{ selections, unselectAll }">
<Dropdown :options="bulkActions(selections, unselectAll)">
<Dropdown
:options="listBulkActionsRef.bulkActions(selections, unselectAll)"
>
<Button icon="more-horizontal" variant="ghost" />
</Dropdown>
</template>
@ -130,20 +132,14 @@
}"
@loadMore="emit('loadMore')"
/>
<EditValueModal
v-model="showEditModal"
v-model:unselectAll="unselectAllAction"
doctype="CRM Deal"
:selectedValues="selectedValues"
@reload="list.reload()"
/>
<ListBulkActions ref="listBulkActionsRef" v-model="list" doctype="CRM Deal" />
</template>
<script setup>
import MultipleAvatar from '@/components/MultipleAvatar.vue'
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import EditValueModal from '@/components/Modals/EditValueModal.vue'
import ListBulkActions from '@/components/ListBulkActions.vue'
import {
Avatar,
ListView,
@ -154,13 +150,9 @@ import {
ListSelectBanner,
ListFooter,
Dropdown,
call,
Tooltip,
} from 'frappe-ui'
import { setupListActions, createToast } from '@/utils'
import { globalStore } from '@/stores/global'
import { onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { ref, watch } from 'vue'
const props = defineProps({
rows: {
@ -193,103 +185,14 @@ const emit = defineEmits([
const pageLengthCount = defineModel()
const list = defineModel('list')
const router = useRouter()
const { $dialog } = globalStore()
watch(pageLengthCount, (val, old_value) => {
if (val === old_value) return
emit('updatePageCount', val)
})
const showEditModal = ref(false)
const selectedValues = ref([])
const unselectAllAction = ref(() => {})
function editValues(selections, unselectAll) {
selectedValues.value = selections
showEditModal.value = true
unselectAllAction.value = unselectAll
}
function deleteValues(selections, unselectAll) {
$dialog({
title: __('Delete'),
message: __('Are you sure you want to delete {0} item(s)?', [
selections.size,
]),
variant: 'danger',
actions: [
{
label: __('Delete'),
variant: 'solid',
theme: 'red',
onClick: (close) => {
call('frappe.desk.reportview.delete_items', {
items: JSON.stringify(Array.from(selections)),
doctype: 'CRM Deal',
}).then(() => {
createToast({
title: __('Deleted successfully'),
icon: 'check',
iconClasses: 'text-green-600',
})
unselectAll()
list.value.reload()
close()
})
},
},
],
})
}
const customBulkActions = ref([])
const customListActions = ref([])
function bulkActions(selections, unselectAll) {
let actions = [
{
label: __('Edit'),
onClick: () => editValues(selections, unselectAll),
},
{
label: __('Delete'),
onClick: () => deleteValues(selections, unselectAll),
},
]
customBulkActions.value.forEach((action) => {
actions.push({
label: __(action.label),
onClick: () =>
action.onClick({
list: list.value,
selections,
unselectAll,
call,
createToast,
$dialog,
router,
}),
})
})
return actions
}
onMounted(() => {
if (!list.value?.data) return
setupListActions(list.value.data, {
list: list.value,
call,
createToast,
$dialog,
router,
})
customBulkActions.value = list.value?.data?.bulkActions || []
customListActions.value = list.value?.data?.listActions || []
})
const listBulkActionsRef = ref(null)
defineExpose({
customListActions,
customListActions: listBulkActionsRef.value?.customListActions,
})
</script>

View File

@ -69,7 +69,7 @@
</ListRows>
<ListSelectBanner>
<template #actions="{ selections, unselectAll }">
<Dropdown :options="bulkActions(selections, unselectAll)">
<Dropdown :options="listBulkActionsRef.bulkActions(selections, unselectAll)">
<Button icon="more-horizontal" variant="ghost" />
</Dropdown>
</template>
@ -84,18 +84,17 @@
}"
@loadMore="emit('loadMore')"
/>
<EditValueModal
v-model="showEditModal"
v-model:unselectAll="unselectAllAction"
<ListBulkActions
ref="listBulkActionsRef"
v-model="list"
doctype="Email Template"
:selectedValues="selectedValues"
@reload="list.reload()"
:options="{
hideAssign: true,
}"
/>
</template>
<script setup>
import EditValueModal from '@/components/Modals/EditValueModal.vue'
import { globalStore } from '@/stores/global'
import { setupListActions, createToast } from '@/utils'
import ListBulkActions from '@/components/ListBulkActions.vue'
import {
ListView,
ListHeader,
@ -105,11 +104,9 @@ import {
ListRowItem,
ListFooter,
Dropdown,
call,
Tooltip,
} from 'frappe-ui'
import { ref, watch, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ref, watch } from 'vue'
const props = defineProps({
rows: {
@ -143,87 +140,14 @@ const emit = defineEmits([
const pageLengthCount = defineModel()
const list = defineModel('list')
const router = useRouter()
const { $dialog } = globalStore()
watch(pageLengthCount, (val, old_value) => {
if (val === old_value) return
emit('updatePageCount', val)
})
const showEditModal = ref(false)
const selectedValues = ref([])
const unselectAllAction = ref(() => {})
function editValues(selections, unselectAll) {
selectedValues.value = selections
showEditModal.value = true
unselectAllAction.value = unselectAll
}
function deleteValues(selections, unselectAll) {
$dialog({
title: __('Delete'),
message: __('Are you sure you want to delete {0} item(s)?', [
selections.size,
]),
variant: 'danger',
actions: [
{
label: __('Delete'),
variant: 'solid',
theme: 'red',
onClick: (close) => {
call('frappe.desk.reportview.delete_items', {
items: JSON.stringify(Array.from(selections)),
doctype: 'Email Template',
}).then(() => {
createToast({
title: __('Deleted successfully'),
icon: 'check',
iconClasses: 'text-green-600',
})
unselectAll()
list.value.reload()
close()
})
},
},
],
})
}
const customListActions = ref([])
function bulkActions(selections, unselectAll) {
let actions = [
{
label: __('Edit'),
onClick: () => editValues(selections, unselectAll),
},
{
label: __('Delete'),
onClick: () => deleteValues(selections, unselectAll),
},
]
return actions
}
onMounted(() => {
if (!list.value?.data) return
setupListActions(list.value.data, {
list: list.value,
call,
createToast,
$dialog,
router,
})
// customBulkActions.value = list.value?.data?.bulkActions || []
customListActions.value = list.value?.data?.listActions || []
})
const listBulkActionsRef = ref(null)
defineExpose({
customListActions,
customListActions: listBulkActionsRef.value?.customListActions,
})
</script>

View File

@ -123,7 +123,7 @@
</ListRows>
<ListSelectBanner>
<template #actions="{ selections, unselectAll }">
<Dropdown :options="bulkActions(selections, unselectAll)">
<Dropdown :options="listBulkActionsRef.bulkActions(selections, unselectAll)">
<Button icon="more-horizontal" variant="ghost" />
</Dropdown>
</template>
@ -139,20 +139,14 @@
}"
@loadMore="emit('loadMore')"
/>
<EditValueModal
v-model="showEditModal"
v-model:unselectAll="unselectAllAction"
doctype="CRM Lead"
:selectedValues="selectedValues"
@reload="list.reload()"
/>
<ListBulkActions ref="listBulkActionsRef" v-model="list" doctype="CRM Lead" />
</template>
<script setup>
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import MultipleAvatar from '@/components/MultipleAvatar.vue'
import EditValueModal from '@/components/Modals/EditValueModal.vue'
import ListBulkActions from '@/components/ListBulkActions.vue'
import {
Avatar,
ListView,
@ -163,13 +157,9 @@ import {
ListRowItem,
ListFooter,
Dropdown,
call,
Tooltip,
} from 'frappe-ui'
import { setupListActions, createToast } from '@/utils'
import { globalStore } from '@/stores/global'
import { onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { ref, watch } from 'vue'
const props = defineProps({
rows: {
@ -202,103 +192,14 @@ const emit = defineEmits([
const pageLengthCount = defineModel()
const list = defineModel('list')
const router = useRouter()
const { $dialog } = globalStore()
watch(pageLengthCount, (val, old_value) => {
if (val === old_value) return
emit('updatePageCount', val)
})
const showEditModal = ref(false)
const selectedValues = ref([])
const unselectAllAction = ref(() => {})
function editValues(selections, unselectAll) {
selectedValues.value = selections
showEditModal.value = true
unselectAllAction.value = unselectAll
}
function deleteValues(selections, unselectAll) {
$dialog({
title: __('Delete'),
message: __('Are you sure you want to delete {0} item(s)?', [
selections.size,
]),
variant: 'danger',
actions: [
{
label: __('Delete'),
variant: 'solid',
theme: 'red',
onClick: (close) => {
call('frappe.desk.reportview.delete_items', {
items: JSON.stringify(Array.from(selections)),
doctype: 'CRM Lead',
}).then(() => {
createToast({
title: __('Deleted successfully'),
icon: 'check',
iconClasses: 'text-green-600',
})
unselectAll()
list.value.reload()
close()
})
},
},
],
})
}
const customBulkActions = ref([])
const customListActions = ref([])
function bulkActions(selections, unselectAll) {
let actions = [
{
label: __('Edit'),
onClick: () => editValues(selections, unselectAll),
},
{
label: __('Delete'),
onClick: () => deleteValues(selections, unselectAll),
},
]
customBulkActions.value.forEach((action) => {
actions.push({
label: __(action.label),
onClick: () =>
action.onClick({
list: list.value,
selections,
unselectAll,
call,
createToast,
$dialog,
router,
}),
})
})
return actions
}
onMounted(() => {
if (!list.value?.data) return
setupListActions(list.value.data, {
list: list.value,
call,
createToast,
$dialog,
router,
})
customBulkActions.value = list.value?.data?.bulkActions || []
customListActions.value = list.value?.data?.listActions || []
})
const listBulkActionsRef = ref(null)
defineExpose({
customListActions,
customListActions: listBulkActionsRef.value?.customListActions,
})
</script>

View File

@ -69,7 +69,9 @@
</ListRows>
<ListSelectBanner>
<template #actions="{ selections, unselectAll }">
<Dropdown :options="bulkActions(selections, unselectAll)">
<Dropdown
:options="listBulkActionsRef.bulkActions(selections, unselectAll)"
>
<Button icon="more-horizontal" variant="ghost" />
</Dropdown>
</template>
@ -84,18 +86,17 @@
}"
@loadMore="emit('loadMore')"
/>
<EditValueModal
v-model="showEditModal"
v-model:unselectAll="unselectAllAction"
<ListBulkActions
ref="listBulkActionsRef"
v-model="list"
doctype="CRM Organization"
:selectedValues="selectedValues"
@reload="list.reload()"
:options="{
hideAssign: true,
}"
/>
</template>
<script setup>
import EditValueModal from '@/components/Modals/EditValueModal.vue'
import { globalStore } from '@/stores/global'
import { setupListActions, createToast } from '@/utils'
import ListBulkActions from '@/components/ListBulkActions.vue'
import {
Avatar,
ListView,
@ -107,10 +108,8 @@ import {
ListFooter,
Tooltip,
Dropdown,
call,
} from 'frappe-ui'
import { ref, watch, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ref, watch } from 'vue'
const props = defineProps({
rows: {
@ -143,87 +142,14 @@ const emit = defineEmits([
const pageLengthCount = defineModel()
const list = defineModel('list')
const router = useRouter()
const { $dialog } = globalStore()
watch(pageLengthCount, (val, old_value) => {
if (val === old_value) return
emit('updatePageCount', val)
})
const showEditModal = ref(false)
const selectedValues = ref([])
const unselectAllAction = ref(() => {})
function editValues(selections, unselectAll) {
selectedValues.value = selections
showEditModal.value = true
unselectAllAction.value = unselectAll
}
function deleteValues(selections, unselectAll) {
$dialog({
title: __('Delete'),
message: __('Are you sure you want to delete {0} item(s)?', [
selections.size,
]),
variant: 'danger',
actions: [
{
label: __('Delete'),
variant: 'solid',
theme: 'red',
onClick: (close) => {
call('frappe.desk.reportview.delete_items', {
items: JSON.stringify(Array.from(selections)),
doctype: 'CRM Organization',
}).then(() => {
createToast({
title: __('Deleted successfully'),
icon: 'check',
iconClasses: 'text-green-600',
})
unselectAll()
list.value.reload()
close()
})
},
},
],
})
}
const customListActions = ref([])
function bulkActions(selections, unselectAll) {
let actions = [
{
label: __('Edit'),
onClick: () => editValues(selections, unselectAll),
},
{
label: __('Delete'),
onClick: () => deleteValues(selections, unselectAll),
},
]
return actions
}
onMounted(() => {
if (!list.value?.data) return
setupListActions(list.value.data, {
list: list.value,
call,
createToast,
$dialog,
router,
})
// customBulkActions.value = list.value?.data?.bulkActions || []
customListActions.value = list.value?.data?.listActions || []
})
const listBulkActionsRef = ref(null)
defineExpose({
customListActions,
customListActions: listBulkActionsRef.value?.customListActions,
})
</script>

View File

@ -82,7 +82,7 @@
</ListRows>
<ListSelectBanner>
<template #actions="{ selections, unselectAll }">
<Dropdown :options="bulkActions(selections, unselectAll)">
<Dropdown :options="listBulkActionsRef.bulkActions(selections, unselectAll)">
<Button icon="more-horizontal" variant="ghost" />
</Dropdown>
</template>
@ -97,22 +97,21 @@
}"
@loadMore="emit('loadMore')"
/>
<EditValueModal
v-model="showEditModal"
v-model:unselectAll="unselectAllAction"
<ListBulkActions
ref="listBulkActionsRef"
v-model="list"
doctype="CRM Task"
:selectedValues="selectedValues"
@reload="list.reload()"
:options="{
hideAssign: true,
}"
/>
</template>
<script setup>
import TaskStatusIcon from '@/components/Icons/TaskStatusIcon.vue'
import TaskPriorityIcon from '@/components/Icons/TaskPriorityIcon.vue'
import CalendarIcon from '@/components/Icons/CalendarIcon.vue'
import EditValueModal from '@/components/Modals/EditValueModal.vue'
import ListBulkActions from '@/components/ListBulkActions.vue'
import { dateFormat } from '@/utils'
import { globalStore } from '@/stores/global'
import { setupListActions, createToast } from '@/utils'
import {
Avatar,
ListView,
@ -123,11 +122,9 @@ import {
ListRowItem,
ListFooter,
Dropdown,
call,
Tooltip,
} from 'frappe-ui'
import { ref, watch, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ref, watch } from 'vue'
const props = defineProps({
rows: {
@ -161,87 +158,14 @@ const emit = defineEmits([
const pageLengthCount = defineModel()
const list = defineModel('list')
const router = useRouter()
const { $dialog } = globalStore()
watch(pageLengthCount, (val, old_value) => {
if (val === old_value) return
emit('updatePageCount', val)
})
const showEditModal = ref(false)
const selectedValues = ref([])
const unselectAllAction = ref(() => {})
function editValues(selections, unselectAll) {
selectedValues.value = selections
showEditModal.value = true
unselectAllAction.value = unselectAll
}
function deleteValues(selections, unselectAll) {
$dialog({
title: __('Delete'),
message: __('Are you sure you want to delete {0} item(s)?', [
selections.size,
]),
variant: 'danger',
actions: [
{
label: __('Delete'),
variant: 'solid',
theme: 'red',
onClick: (close) => {
call('frappe.desk.reportview.delete_items', {
items: JSON.stringify(Array.from(selections)),
doctype: 'CRM Task',
}).then(() => {
createToast({
title: __('Deleted successfully'),
icon: 'check',
iconClasses: 'text-green-600',
})
unselectAll()
list.value.reload()
close()
})
},
},
],
})
}
const customListActions = ref([])
function bulkActions(selections, unselectAll) {
let actions = [
{
label: __('Edit'),
onClick: () => editValues(selections, unselectAll),
},
{
label: __('Delete'),
onClick: () => deleteValues(selections, unselectAll),
},
]
return actions
}
onMounted(() => {
if (!list.value?.data) return
setupListActions(list.value.data, {
list: list.value,
call,
createToast,
$dialog,
router,
})
// customBulkActions.value = list.value?.data?.bulkActions || []
customListActions.value = list.value?.data?.listActions || []
})
const listBulkActionsRef = ref(null)
defineExpose({
customListActions,
customListActions: listBulkActionsRef.value?.customListActions,
})
</script>

View File

@ -27,6 +27,7 @@
value=""
doctype="User"
@change="(option) => addValue(option) && ($refs.input.value = '')"
:placeholder="__('John Doe')"
:hideMe="true"
>
<template #item-prefix="{ option }">
@ -83,8 +84,18 @@ const props = defineProps({
type: Object,
default: null,
},
docs: {
type: Array,
default: () => [],
},
doctype: {
type: String,
default: '',
},
})
const emit = defineEmits(['reload'])
const show = defineModel()
const assignees = defineModel('assignees')
const oldAssignees = ref([])
@ -101,7 +112,7 @@ const removeValue = (value) => {
const owner = computed(() => {
if (!props.doc) return ''
if (props.doc.doctype == 'CRM Lead') return props.doc.lead_owner
if (props.doctype == 'CRM Lead') return props.doc.lead_owner
return props.doc.deal_owner
})
@ -137,7 +148,7 @@ function updateAssignees() {
if (removedAssignees.length) {
for (let a of removedAssignees) {
call('frappe.desk.form.assign_to.remove', {
doctype: props.doc.doctype,
doctype: props.doctype,
name: props.doc.name,
assign_to: a,
})
@ -145,11 +156,23 @@ function updateAssignees() {
}
if (addedAssignees.length) {
call('frappe.desk.form.assign_to.add', {
doctype: props.doc.doctype,
name: props.doc.name,
assign_to: addedAssignees,
})
if (props.docs.size) {
call('frappe.desk.form.assign_to.add_multiple', {
doctype: props.doctype,
name: JSON.stringify(Array.from(props.docs)),
assign_to: addedAssignees,
bulk_assign: true,
re_assign: true,
}).then(() => {
emit('reload')
})
} else {
call('frappe.desk.form.assign_to.add', {
doctype: props.doctype,
name: props.doc.name,
assign_to: addedAssignees,
})
}
}
show.value = false
}

View File

@ -59,7 +59,6 @@ const props = defineProps({
})
const show = defineModel()
const unselectAll = defineModel('unselectAll')
const emit = defineEmits(['reload'])
@ -114,7 +113,6 @@ function updateValues() {
newValue.value = ''
loading.value = false
show.value = false
unselectAll.value()
emit('reload')
})
}
@ -130,6 +128,10 @@ function updateValue(v) {
newValue.value = value
}
function getSelectOptions(options) {
return options.split('\n')
}
function getValueComponent(f) {
const { type, options } = f
if (typeSelect.includes(type) || typeCheck.includes(type)) {
@ -140,6 +142,7 @@ function getValueComponent(f) {
label: o,
value: o,
})),
modelValue: newValue.value,
})
} else if (typeLink.includes(type)) {
if (type == 'Dynamic Link') {

View File

@ -67,7 +67,7 @@ import IconPicker from '@/components/IconPicker.vue'
import SmileIcon from '@/components/Icons/SmileIcon.vue'
import { createResource, Textarea, FileUploader, Dropdown } from 'frappe-ui'
import FeatherIcon from 'frappe-ui/src/components/FeatherIcon.vue'
import { ref, computed, nextTick, watch } from 'vue'
import { ref, nextTick, watch } from 'vue'
const props = defineProps({
doctype: String,

View File

@ -280,6 +280,7 @@
<AssignmentModal
v-if="deal.data"
:doc="deal.data"
doctype="CRM Deal"
v-model="showAssignmentModal"
v-model:assignees="deal.data._assignedTo"
/>
@ -289,6 +290,7 @@ import Resizer from '@/components/Resizer.vue'
import LoadingIndicator from '@/components/Icons/LoadingIndicator.vue'
import ActivityIcon from '@/components/Icons/ActivityIcon.vue'
import EmailIcon from '@/components/Icons/EmailIcon.vue'
import CommentIcon from '@/components/Icons/CommentIcon.vue'
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import TaskIcon from '@/components/Icons/TaskIcon.vue'
import NoteIcon from '@/components/Icons/NoteIcon.vue'
@ -447,6 +449,11 @@ const tabs = computed(() => {
label: __('Emails'),
icon: EmailIcon,
},
{
name: 'Comments',
label: __('Comments'),
icon: CommentIcon,
},
{
name: 'Calls',
label: __('Calls'),

View File

@ -186,6 +186,7 @@
<AssignmentModal
v-if="lead.data"
:doc="lead.data"
doctype="CRM Lead"
v-model="showAssignmentModal"
v-model:assignees="lead.data._assignedTo"
/>
@ -260,6 +261,7 @@
import Resizer from '@/components/Resizer.vue'
import ActivityIcon from '@/components/Icons/ActivityIcon.vue'
import EmailIcon from '@/components/Icons/EmailIcon.vue'
import CommentIcon from '@/components/Icons/CommentIcon.vue'
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import TaskIcon from '@/components/Icons/TaskIcon.vue'
import NoteIcon from '@/components/Icons/NoteIcon.vue'
@ -416,6 +418,11 @@ const tabs = computed(() => {
label: __('Emails'),
icon: EmailIcon,
},
{
name: 'Comments',
label: __('Comments'),
icon: CommentIcon,
},
{
name: 'Calls',
label: __('Calls'),