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 "read_only"
return field.fieldtype.lower() 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( assigned_users = frappe.get_all(
"ToDo", "ToDo",
fields=["allocated_to"], fields=["allocated_to"],
@ -388,7 +388,12 @@ def get_assigned_users(doctype, name):
pluck="allocated_to", 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() @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_fields"], deal["all_fields"] = get_doctype_fields("CRM Deal", name)
deal["doctype"] = "CRM Deal" deal["doctype"] = "CRM Deal"
deal["_form_script"] = get_form_script('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 return deal
@frappe.whitelist() @frappe.whitelist()

View File

@ -108,8 +108,8 @@ class CRMDeal(Document):
""" """
sla = get_sla(self) sla = get_sla(self)
if not sla: if not sla:
self.first_responded_on = None # self.first_responded_on = None
self.first_response_time = None # self.first_response_time = None
return return
self.sla = sla.name 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_fields"], lead["all_fields"] = get_doctype_fields("CRM Lead", name)
lead["doctype"] = "CRM Lead" lead["doctype"] = "CRM Lead"
lead["_form_script"] = get_form_script('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 return lead

View File

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

View File

@ -15,6 +15,16 @@
</template> </template>
<span>{{ __('New Email') }}</span> <span>{{ __('New Email') }}</span>
</Button> </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 <Button
v-else-if="title == 'Calls'" v-else-if="title == 'Calls'"
variant="solid" variant="solid"
@ -146,6 +156,62 @@
</div> </div>
</div> </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-else-if="title == 'Tasks'" class="activity px-10 pb-5">
<div v-for="(task, i) in activities"> <div v-for="(task, i) in activities">
<div <div
@ -756,6 +822,11 @@
:label="__('New Email')" :label="__('New Email')"
@click="$refs.emailBox.show = true" @click="$refs.emailBox.show = true"
/> />
<Button
v-else-if="title == 'Comments'"
:label="__('New Comment')"
@click="$refs.emailBox.showComment = true"
/>
<Button <Button
v-else-if="title == 'Tasks'" v-else-if="title == 'Tasks'"
:label="__('Create Task')" :label="__('Create Task')"
@ -764,7 +835,7 @@
</div> </div>
<CommunicationArea <CommunicationArea
ref="emailBox" ref="emailBox"
v-if="['Emails', 'Activity'].includes(title)" v-if="['Emails', 'Comments', 'Activity'].includes(title)"
v-model="doc" v-model="doc"
v-model:reload="reload_email" v-model:reload="reload_email"
:doctype="doctype" :doctype="doctype"
@ -983,6 +1054,11 @@ const defaultActions = computed(() => {
label: __('New Email'), label: __('New Email'),
onClick: () => (emailBox.value.show = true), 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' }), icon: h(PhoneIcon, { class: 'h-4 w-4' }),
label: __('Make a Call'), label: __('Make a Call'),
@ -1027,6 +1103,11 @@ const activities = computed(() => {
activities = all_activities.data.versions.filter( activities = all_activities.data.versions.filter(
(activity) => activity.activity_type === 'communication' (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') { } else if (props.title == 'Calls') {
if (!all_activities.data?.calls) return [] if (!all_activities.data?.calls) return []
return sortByCreation(all_activities.data.calls) return sortByCreation(all_activities.data.calls)
@ -1089,6 +1170,8 @@ const emptyText = computed(() => {
let text = 'No Activities' let text = 'No Activities'
if (props.title == 'Emails') { if (props.title == 'Emails') {
text = 'No Email Communications' text = 'No Email Communications'
} else if (props.title == 'Comments') {
text = 'No Comments'
} else if (props.title == 'Calls') { } else if (props.title == 'Calls') {
text = 'No Call Logs' text = 'No Call Logs'
} else if (props.title == 'Notes') { } else if (props.title == 'Notes') {
@ -1105,6 +1188,8 @@ const emptyTextIcon = computed(() => {
let icon = ActivityIcon let icon = ActivityIcon
if (props.title == 'Emails') { if (props.title == 'Emails') {
icon = EmailIcon icon = EmailIcon
} else if (props.title == 'Comments') {
icon = CommentIcon
} else if (props.title == 'Calls') { } else if (props.title == 'Calls') {
icon = PhoneIcon icon = PhoneIcon
} else if (props.title == 'Notes') { } else if (props.title == 'Notes') {

View File

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

View File

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

View File

@ -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> </ListRows>
<ListSelectBanner> <ListSelectBanner>
<template #actions="{ selections, unselectAll }"> <template #actions="{ selections, unselectAll }">
<Dropdown :options="bulkActions(selections, unselectAll)"> <Dropdown
:options="listBulkActionsRef.bulkActions(selections, unselectAll)"
>
<Button icon="more-horizontal" variant="ghost" /> <Button icon="more-horizontal" variant="ghost" />
</Dropdown> </Dropdown>
</template> </template>
@ -95,8 +97,18 @@
}" }"
@loadMore="emit('loadMore')" @loadMore="emit('loadMore')"
/> />
<ListBulkActions
ref="listBulkActionsRef"
v-model="list"
doctype="CRM Call Log"
:options="{
hideEdit: true,
hideAssign: true,
}"
/>
</template> </template>
<script setup> <script setup>
import ListBulkActions from '@/components/ListBulkActions.vue'
import { import {
Avatar, Avatar,
ListView, ListView,
@ -108,12 +120,8 @@ import {
ListFooter, ListFooter,
Tooltip, Tooltip,
Dropdown, Dropdown,
call,
} from 'frappe-ui' } from 'frappe-ui'
import { setupListActions, createToast } from '@/utils' import { ref, watch } from 'vue'
import { globalStore } from '@/stores/global'
import { onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
const props = defineProps({ const props = defineProps({
rows: { rows: {
@ -146,89 +154,14 @@ const emit = defineEmits([
const pageLengthCount = defineModel() const pageLengthCount = defineModel()
const list = defineModel('list') const list = defineModel('list')
const router = useRouter()
const { $dialog } = globalStore()
watch(pageLengthCount, (val, old_value) => { watch(pageLengthCount, (val, old_value) => {
if (val === old_value) return if (val === old_value) return
emit('updatePageCount', val) emit('updatePageCount', val)
}) })
function deleteValues(selections, unselectAll) { const listBulkActionsRef = ref(null)
$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 || []
})
defineExpose({ defineExpose({
customListActions, customListActions: listBulkActionsRef.value?.customListActions,
}) })
</script> </script>

View File

@ -82,7 +82,9 @@
</ListRows> </ListRows>
<ListSelectBanner> <ListSelectBanner>
<template #actions="{ selections, unselectAll }"> <template #actions="{ selections, unselectAll }">
<Dropdown :options="bulkActions(selections, unselectAll)"> <Dropdown
:options="listBulkActionsRef.bulkActions(selections, unselectAll)"
>
<Button icon="more-horizontal" variant="ghost" /> <Button icon="more-horizontal" variant="ghost" />
</Dropdown> </Dropdown>
</template> </template>
@ -98,19 +100,18 @@
}" }"
@loadMore="emit('loadMore')" @loadMore="emit('loadMore')"
/> />
<EditValueModal <ListBulkActions
v-model="showEditModal" ref="listBulkActionsRef"
v-model:unselectAll="unselectAllAction" v-model="list"
doctype="Contact" doctype="Contact"
:selectedValues="selectedValues" :options="{
@reload="list.reload()" hideAssign: true,
}"
/> />
</template> </template>
<script setup> <script setup>
import PhoneIcon from '@/components/Icons/PhoneIcon.vue' import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import EditValueModal from '@/components/Modals/EditValueModal.vue' import ListBulkActions from '@/components/ListBulkActions.vue'
import { globalStore } from '@/stores/global'
import { setupListActions, createToast } from '@/utils'
import { import {
Avatar, Avatar,
ListView, ListView,
@ -122,10 +123,8 @@ import {
ListFooter, ListFooter,
Tooltip, Tooltip,
Dropdown, Dropdown,
call,
} from 'frappe-ui' } from 'frappe-ui'
import { ref, watch, onMounted } from 'vue' import { ref, watch } from 'vue'
import { useRouter } from 'vue-router'
const props = defineProps({ const props = defineProps({
rows: { rows: {
@ -158,87 +157,14 @@ const emit = defineEmits([
const pageLengthCount = defineModel() const pageLengthCount = defineModel()
const list = defineModel('list') const list = defineModel('list')
const router = useRouter()
const { $dialog } = globalStore()
watch(pageLengthCount, (val, old_value) => { watch(pageLengthCount, (val, old_value) => {
if (val === old_value) return if (val === old_value) return
emit('updatePageCount', val) emit('updatePageCount', val)
}) })
const showEditModal = ref(false) const listBulkActionsRef = ref(null)
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 || []
})
defineExpose({ defineExpose({
customListActions, customListActions: listBulkActionsRef.value?.customListActions,
}) })
</script> </script>

View File

@ -114,7 +114,9 @@
</ListRows> </ListRows>
<ListSelectBanner> <ListSelectBanner>
<template #actions="{ selections, unselectAll }"> <template #actions="{ selections, unselectAll }">
<Dropdown :options="bulkActions(selections, unselectAll)"> <Dropdown
:options="listBulkActionsRef.bulkActions(selections, unselectAll)"
>
<Button icon="more-horizontal" variant="ghost" /> <Button icon="more-horizontal" variant="ghost" />
</Dropdown> </Dropdown>
</template> </template>
@ -130,20 +132,14 @@
}" }"
@loadMore="emit('loadMore')" @loadMore="emit('loadMore')"
/> />
<EditValueModal <ListBulkActions ref="listBulkActionsRef" v-model="list" doctype="CRM Deal" />
v-model="showEditModal"
v-model:unselectAll="unselectAllAction"
doctype="CRM Deal"
:selectedValues="selectedValues"
@reload="list.reload()"
/>
</template> </template>
<script setup> <script setup>
import MultipleAvatar from '@/components/MultipleAvatar.vue' import MultipleAvatar from '@/components/MultipleAvatar.vue'
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue' import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
import PhoneIcon from '@/components/Icons/PhoneIcon.vue' import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import EditValueModal from '@/components/Modals/EditValueModal.vue' import ListBulkActions from '@/components/ListBulkActions.vue'
import { import {
Avatar, Avatar,
ListView, ListView,
@ -154,13 +150,9 @@ import {
ListSelectBanner, ListSelectBanner,
ListFooter, ListFooter,
Dropdown, Dropdown,
call,
Tooltip, Tooltip,
} from 'frappe-ui' } from 'frappe-ui'
import { setupListActions, createToast } from '@/utils' import { ref, watch } from 'vue'
import { globalStore } from '@/stores/global'
import { onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
const props = defineProps({ const props = defineProps({
rows: { rows: {
@ -193,103 +185,14 @@ const emit = defineEmits([
const pageLengthCount = defineModel() const pageLengthCount = defineModel()
const list = defineModel('list') const list = defineModel('list')
const router = useRouter()
const { $dialog } = globalStore()
watch(pageLengthCount, (val, old_value) => { watch(pageLengthCount, (val, old_value) => {
if (val === old_value) return if (val === old_value) return
emit('updatePageCount', val) emit('updatePageCount', val)
}) })
const showEditModal = ref(false) const listBulkActionsRef = ref(null)
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 || []
})
defineExpose({ defineExpose({
customListActions, customListActions: listBulkActionsRef.value?.customListActions,
}) })
</script> </script>

View File

@ -69,7 +69,7 @@
</ListRows> </ListRows>
<ListSelectBanner> <ListSelectBanner>
<template #actions="{ selections, unselectAll }"> <template #actions="{ selections, unselectAll }">
<Dropdown :options="bulkActions(selections, unselectAll)"> <Dropdown :options="listBulkActionsRef.bulkActions(selections, unselectAll)">
<Button icon="more-horizontal" variant="ghost" /> <Button icon="more-horizontal" variant="ghost" />
</Dropdown> </Dropdown>
</template> </template>
@ -84,18 +84,17 @@
}" }"
@loadMore="emit('loadMore')" @loadMore="emit('loadMore')"
/> />
<EditValueModal <ListBulkActions
v-model="showEditModal" ref="listBulkActionsRef"
v-model:unselectAll="unselectAllAction" v-model="list"
doctype="Email Template" doctype="Email Template"
:selectedValues="selectedValues" :options="{
@reload="list.reload()" hideAssign: true,
}"
/> />
</template> </template>
<script setup> <script setup>
import EditValueModal from '@/components/Modals/EditValueModal.vue' import ListBulkActions from '@/components/ListBulkActions.vue'
import { globalStore } from '@/stores/global'
import { setupListActions, createToast } from '@/utils'
import { import {
ListView, ListView,
ListHeader, ListHeader,
@ -105,11 +104,9 @@ import {
ListRowItem, ListRowItem,
ListFooter, ListFooter,
Dropdown, Dropdown,
call,
Tooltip, Tooltip,
} from 'frappe-ui' } from 'frappe-ui'
import { ref, watch, onMounted } from 'vue' import { ref, watch } from 'vue'
import { useRouter } from 'vue-router'
const props = defineProps({ const props = defineProps({
rows: { rows: {
@ -143,87 +140,14 @@ const emit = defineEmits([
const pageLengthCount = defineModel() const pageLengthCount = defineModel()
const list = defineModel('list') const list = defineModel('list')
const router = useRouter()
const { $dialog } = globalStore()
watch(pageLengthCount, (val, old_value) => { watch(pageLengthCount, (val, old_value) => {
if (val === old_value) return if (val === old_value) return
emit('updatePageCount', val) emit('updatePageCount', val)
}) })
const showEditModal = ref(false) const listBulkActionsRef = ref(null)
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 || []
})
defineExpose({ defineExpose({
customListActions, customListActions: listBulkActionsRef.value?.customListActions,
}) })
</script> </script>

View File

@ -123,7 +123,7 @@
</ListRows> </ListRows>
<ListSelectBanner> <ListSelectBanner>
<template #actions="{ selections, unselectAll }"> <template #actions="{ selections, unselectAll }">
<Dropdown :options="bulkActions(selections, unselectAll)"> <Dropdown :options="listBulkActionsRef.bulkActions(selections, unselectAll)">
<Button icon="more-horizontal" variant="ghost" /> <Button icon="more-horizontal" variant="ghost" />
</Dropdown> </Dropdown>
</template> </template>
@ -139,20 +139,14 @@
}" }"
@loadMore="emit('loadMore')" @loadMore="emit('loadMore')"
/> />
<EditValueModal <ListBulkActions ref="listBulkActionsRef" v-model="list" doctype="CRM Lead" />
v-model="showEditModal"
v-model:unselectAll="unselectAllAction"
doctype="CRM Lead"
:selectedValues="selectedValues"
@reload="list.reload()"
/>
</template> </template>
<script setup> <script setup>
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue' import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
import PhoneIcon from '@/components/Icons/PhoneIcon.vue' import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
import MultipleAvatar from '@/components/MultipleAvatar.vue' import MultipleAvatar from '@/components/MultipleAvatar.vue'
import EditValueModal from '@/components/Modals/EditValueModal.vue' import ListBulkActions from '@/components/ListBulkActions.vue'
import { import {
Avatar, Avatar,
ListView, ListView,
@ -163,13 +157,9 @@ import {
ListRowItem, ListRowItem,
ListFooter, ListFooter,
Dropdown, Dropdown,
call,
Tooltip, Tooltip,
} from 'frappe-ui' } from 'frappe-ui'
import { setupListActions, createToast } from '@/utils' import { ref, watch } from 'vue'
import { globalStore } from '@/stores/global'
import { onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
const props = defineProps({ const props = defineProps({
rows: { rows: {
@ -202,103 +192,14 @@ const emit = defineEmits([
const pageLengthCount = defineModel() const pageLengthCount = defineModel()
const list = defineModel('list') const list = defineModel('list')
const router = useRouter()
const { $dialog } = globalStore()
watch(pageLengthCount, (val, old_value) => { watch(pageLengthCount, (val, old_value) => {
if (val === old_value) return if (val === old_value) return
emit('updatePageCount', val) emit('updatePageCount', val)
}) })
const showEditModal = ref(false) const listBulkActionsRef = ref(null)
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 || []
})
defineExpose({ defineExpose({
customListActions, customListActions: listBulkActionsRef.value?.customListActions,
}) })
</script> </script>

View File

@ -69,7 +69,9 @@
</ListRows> </ListRows>
<ListSelectBanner> <ListSelectBanner>
<template #actions="{ selections, unselectAll }"> <template #actions="{ selections, unselectAll }">
<Dropdown :options="bulkActions(selections, unselectAll)"> <Dropdown
:options="listBulkActionsRef.bulkActions(selections, unselectAll)"
>
<Button icon="more-horizontal" variant="ghost" /> <Button icon="more-horizontal" variant="ghost" />
</Dropdown> </Dropdown>
</template> </template>
@ -84,18 +86,17 @@
}" }"
@loadMore="emit('loadMore')" @loadMore="emit('loadMore')"
/> />
<EditValueModal <ListBulkActions
v-model="showEditModal" ref="listBulkActionsRef"
v-model:unselectAll="unselectAllAction" v-model="list"
doctype="CRM Organization" doctype="CRM Organization"
:selectedValues="selectedValues" :options="{
@reload="list.reload()" hideAssign: true,
}"
/> />
</template> </template>
<script setup> <script setup>
import EditValueModal from '@/components/Modals/EditValueModal.vue' import ListBulkActions from '@/components/ListBulkActions.vue'
import { globalStore } from '@/stores/global'
import { setupListActions, createToast } from '@/utils'
import { import {
Avatar, Avatar,
ListView, ListView,
@ -107,10 +108,8 @@ import {
ListFooter, ListFooter,
Tooltip, Tooltip,
Dropdown, Dropdown,
call,
} from 'frappe-ui' } from 'frappe-ui'
import { ref, watch, onMounted } from 'vue' import { ref, watch } from 'vue'
import { useRouter } from 'vue-router'
const props = defineProps({ const props = defineProps({
rows: { rows: {
@ -143,87 +142,14 @@ const emit = defineEmits([
const pageLengthCount = defineModel() const pageLengthCount = defineModel()
const list = defineModel('list') const list = defineModel('list')
const router = useRouter()
const { $dialog } = globalStore()
watch(pageLengthCount, (val, old_value) => { watch(pageLengthCount, (val, old_value) => {
if (val === old_value) return if (val === old_value) return
emit('updatePageCount', val) emit('updatePageCount', val)
}) })
const showEditModal = ref(false) const listBulkActionsRef = ref(null)
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 || []
})
defineExpose({ defineExpose({
customListActions, customListActions: listBulkActionsRef.value?.customListActions,
}) })
</script> </script>

View File

@ -82,7 +82,7 @@
</ListRows> </ListRows>
<ListSelectBanner> <ListSelectBanner>
<template #actions="{ selections, unselectAll }"> <template #actions="{ selections, unselectAll }">
<Dropdown :options="bulkActions(selections, unselectAll)"> <Dropdown :options="listBulkActionsRef.bulkActions(selections, unselectAll)">
<Button icon="more-horizontal" variant="ghost" /> <Button icon="more-horizontal" variant="ghost" />
</Dropdown> </Dropdown>
</template> </template>
@ -97,22 +97,21 @@
}" }"
@loadMore="emit('loadMore')" @loadMore="emit('loadMore')"
/> />
<EditValueModal <ListBulkActions
v-model="showEditModal" ref="listBulkActionsRef"
v-model:unselectAll="unselectAllAction" v-model="list"
doctype="CRM Task" doctype="CRM Task"
:selectedValues="selectedValues" :options="{
@reload="list.reload()" hideAssign: true,
}"
/> />
</template> </template>
<script setup> <script setup>
import TaskStatusIcon from '@/components/Icons/TaskStatusIcon.vue' import TaskStatusIcon from '@/components/Icons/TaskStatusIcon.vue'
import TaskPriorityIcon from '@/components/Icons/TaskPriorityIcon.vue' import TaskPriorityIcon from '@/components/Icons/TaskPriorityIcon.vue'
import CalendarIcon from '@/components/Icons/CalendarIcon.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 { dateFormat } from '@/utils'
import { globalStore } from '@/stores/global'
import { setupListActions, createToast } from '@/utils'
import { import {
Avatar, Avatar,
ListView, ListView,
@ -123,11 +122,9 @@ import {
ListRowItem, ListRowItem,
ListFooter, ListFooter,
Dropdown, Dropdown,
call,
Tooltip, Tooltip,
} from 'frappe-ui' } from 'frappe-ui'
import { ref, watch, onMounted } from 'vue' import { ref, watch } from 'vue'
import { useRouter } from 'vue-router'
const props = defineProps({ const props = defineProps({
rows: { rows: {
@ -161,87 +158,14 @@ const emit = defineEmits([
const pageLengthCount = defineModel() const pageLengthCount = defineModel()
const list = defineModel('list') const list = defineModel('list')
const router = useRouter()
const { $dialog } = globalStore()
watch(pageLengthCount, (val, old_value) => { watch(pageLengthCount, (val, old_value) => {
if (val === old_value) return if (val === old_value) return
emit('updatePageCount', val) emit('updatePageCount', val)
}) })
const showEditModal = ref(false) const listBulkActionsRef = ref(null)
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 || []
})
defineExpose({ defineExpose({
customListActions, customListActions: listBulkActionsRef.value?.customListActions,
}) })
</script> </script>

View File

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

View File

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

View File

@ -67,7 +67,7 @@ import IconPicker from '@/components/IconPicker.vue'
import SmileIcon from '@/components/Icons/SmileIcon.vue' import SmileIcon from '@/components/Icons/SmileIcon.vue'
import { createResource, Textarea, FileUploader, Dropdown } from 'frappe-ui' import { createResource, Textarea, FileUploader, Dropdown } from 'frappe-ui'
import FeatherIcon from 'frappe-ui/src/components/FeatherIcon.vue' 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({ const props = defineProps({
doctype: String, doctype: String,

View File

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

View File

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