Merge pull request #389 from frappe/develop

chore: Merge develop to main
This commit is contained in:
Shariq Ansari 2024-10-01 12:22:42 +05:30 committed by GitHub
commit c6edba6622
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 2009 additions and 121 deletions

View File

@ -18,16 +18,18 @@ def notify_mentions(doc):
if not content:
return
mentions = extract_mentions(content)
reference_doc = frappe.get_doc(doc.reference_doctype, doc.reference_name)
for mention in mentions:
owner = frappe.get_cached_value("User", doc.owner, "full_name")
doctype = doc.reference_doctype
if doctype.startswith("CRM "):
doctype = doctype[4:].lower()
name = reference_doc.lead_name or name if doctype == "lead" else reference_doc.organization or reference_doc.lead_name or name
notification_text = f"""
<div class="mb-2 leading-5 text-gray-600">
<span class="font-medium text-gray-900">{ owner }</span>
<span>{ _('mentioned you in {0}').format(doctype) }</span>
<span class="font-medium text-gray-900">{ doc.reference_name }</span>
<span class="font-medium text-gray-900">{ name }</span>
</div>
"""
notify_user({

View File

@ -617,6 +617,7 @@ def get_field_obj(field):
obj["placeholder"] = field.get("placeholder") or "Select " + field.label + "..."
obj["doctype"] = field.options
elif field.fieldtype == "Select" and field.options:
obj["placeholder"] = field.get("placeholder") or "Select " + field.label + "..."
obj["options"] = [{"label": option, "value": option} for option in field.options.split("\n")]
if field.read_only:

View File

@ -27,7 +27,7 @@ def get_notifications():
"type": notification.type,
"to_user": notification.to_user,
"read": notification.read,
"comment": notification.comment,
"hash": get_hash(notification),
"notification_text": notification.notification_text,
"notification_type_doctype": notification.notification_type_doctype,
"notification_type_doc": notification.notification_type_doc,
@ -58,3 +58,17 @@ def mark_as_read(user=None, doc=None):
d = frappe.get_doc("CRM Notification", n.name)
d.read = True
d.save()
def get_hash(notification):
_hash = ""
if notification.type == "Mention" and notification.notification_type_doc:
_hash = "#" + notification.notification_type_doc
if notification.type == "WhatsApp":
_hash = "#whatsapp"
if notification.type == "Assignment" and notification.notification_type_doctype == "CRM Task":
_hash = "#tasks"
if "has been removed by" in notification.message:
_hash = ""
return _hash

View File

@ -52,8 +52,8 @@ def get_notification_text(owner, doc, reference_doc, is_cancelled=False):
if doctype.startswith("CRM "):
doctype = doctype[4:].lower()
if doctype in ["CRM Lead", "CRM Deal"]:
name = reference_doc.lead_name or name if doctype == "CRM Lead" else reference_doc.organization or reference_doc.lead_name or name
if doctype in ["lead", "deal"]:
name = reference_doc.lead_name or name if doctype == "lead" else reference_doc.organization or reference_doc.lead_name or name
if is_cancelled:
return f"""
@ -76,7 +76,7 @@ def get_notification_text(owner, doc, reference_doc, is_cancelled=False):
</div>
"""
if doc.reference_type == "CRM Task":
if doctype == "task":
if is_cancelled:
return f"""
<div class="mb-2 leading-5 text-gray-600">

@ -1 +1 @@
Subproject commit cf4e7d347237c23ebde5f5c890abdf7e81284961
Subproject commit 427b76188fe8b20e683bccf9bb4003821253259f

View File

@ -14,7 +14,7 @@
"@vueuse/core": "^10.3.0",
"@vueuse/integrations": "^10.3.0",
"feather-icons": "^4.28.0",
"frappe-ui": "^0.1.66",
"frappe-ui": "^0.1.70",
"gemoji": "^8.1.0",
"lodash": "^4.17.21",
"mime": "^4.0.1",

View File

@ -457,10 +457,6 @@ const { getUser } = usersStore()
const { getContact, getLeadContact } = contactsStore()
const props = defineProps({
title: {
type: String,
default: 'Activity',
},
doctype: {
type: String,
default: 'CRM Lead',
@ -471,6 +467,8 @@ const props = defineProps({
},
})
const route = useRoute()
const doc = defineModel()
const reload = defineModel('reload')
const tabIndex = defineModel('tabIndex')
@ -478,6 +476,8 @@ const tabIndex = defineModel('tabIndex')
const reload_email = ref(false)
const modalRef = ref(null)
const title = computed(() => props.tabs?.[tabIndex.value]?.name || 'Activity')
const all_activities = createResource({
url: 'crm.api.activities.get_activities',
params: { name: doc.value.data.name },
@ -549,6 +549,14 @@ onMounted(() => {
whatsappMessages.reload()
}
})
nextTick(() => {
const hash = route.hash.slice(1) || null
let tabNames = props.tabs?.map((tab) => tab.name)
if (!tabNames?.includes(hash)) {
scroll(hash)
}
})
})
function sendTemplate(template) {
@ -577,25 +585,25 @@ function get_activities() {
const activities = computed(() => {
let activities = []
if (props.title == 'Activity') {
if (title.value == 'Activity') {
activities = get_activities()
} else if (props.title == 'Emails') {
} else if (title.value == 'Emails') {
if (!all_activities.data?.versions) return []
activities = all_activities.data.versions.filter(
(activity) => activity.activity_type === 'communication',
)
} else if (props.title == 'Comments') {
} else if (title.value == '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 (title.value == 'Calls') {
if (!all_activities.data?.calls) return []
return sortByCreation(all_activities.data.calls)
} else if (props.title == 'Tasks') {
} else if (title.value == 'Tasks') {
if (!all_activities.data?.tasks) return []
return sortByCreation(all_activities.data.tasks)
} else if (props.title == 'Notes') {
} else if (title.value == 'Notes') {
if (!all_activities.data?.notes) return []
return sortByCreation(all_activities.data.notes)
}
@ -649,17 +657,17 @@ function update_activities_details(activity) {
const emptyText = computed(() => {
let text = 'No Activities'
if (props.title == 'Emails') {
if (title.value == 'Emails') {
text = 'No Email Communications'
} else if (props.title == 'Comments') {
} else if (title.value == 'Comments') {
text = 'No Comments'
} else if (props.title == 'Calls') {
} else if (title.value == 'Calls') {
text = 'No Call Logs'
} else if (props.title == 'Notes') {
} else if (title.value == 'Notes') {
text = 'No Notes'
} else if (props.title == 'Tasks') {
} else if (title.value == 'Tasks') {
text = 'No Tasks'
} else if (props.title == 'WhatsApp') {
} else if (title.value == 'WhatsApp') {
text = 'No WhatsApp Messages'
}
return text
@ -667,17 +675,17 @@ const emptyText = computed(() => {
const emptyTextIcon = computed(() => {
let icon = ActivityIcon
if (props.title == 'Emails') {
if (title.value == 'Emails') {
icon = Email2Icon
} else if (props.title == 'Comments') {
} else if (title.value == 'Comments') {
icon = CommentIcon
} else if (props.title == 'Calls') {
} else if (title.value == 'Calls') {
icon = PhoneIcon
} else if (props.title == 'Notes') {
} else if (title.value == 'Notes') {
icon = NoteIcon
} else if (props.title == 'Tasks') {
} else if (title.value == 'Tasks') {
icon = TaskIcon
} else if (props.title == 'WhatsApp') {
} else if (title.value == 'WhatsApp') {
icon = WhatsAppIcon
}
return h(icon, { class: 'text-gray-500' })
@ -720,6 +728,7 @@ watch([reload, reload_email], ([reload_value, reload_email_value]) => {
})
function scroll(hash) {
if (['tasks', 'notes'].includes(route.hash?.slice(1))) return
setTimeout(() => {
let el
if (!hash) {
@ -736,11 +745,4 @@ function scroll(hash) {
}
defineExpose({ emailBox })
const route = useRoute()
nextTick(() => {
const hash = route.hash.slice(1) || null
scroll(hash)
})
</script>

View File

@ -5,6 +5,7 @@
:task="task"
:doctype="doctype"
:doc="doc.data?.name"
@after="redirect('tasks')"
/>
<NoteModal
v-model="showNoteModal"
@ -12,6 +13,7 @@
:note="note"
:doctype="doctype"
:doc="doc.data?.name"
@after="redirect('notes')"
/>
</template>
<script setup>
@ -19,6 +21,7 @@ import TaskModal from '@/components/Modals/TaskModal.vue'
import NoteModal from '@/components/Modals/NoteModal.vue'
import { call } from 'frappe-ui'
import { ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const props = defineProps({
doctype: String,
@ -74,6 +77,19 @@ function showNote(n) {
showNoteModal.value = true
}
// common
const route = useRoute()
const router = useRouter()
function redirect(tabName) {
if (route.name == 'Lead' || route.name == 'Deal') {
let hash = '#' + tabName
if (route.hash != hash) {
router.push({ ...route, hash })
}
}
}
defineExpose({
showTask,
deleteTask,

View File

@ -38,7 +38,7 @@
@update:modelValue="
() => {
content += emoji
$refs.textarea.$el.focus()
$refs.textareaRef.el.focus()
capture('whatsapp_emoji_added')
}
"
@ -50,7 +50,7 @@
</IconPicker>
</div>
<Textarea
ref="textarea"
ref="textareaRef"
type="textarea"
class="min-h-8 w-full"
:rows="rows"
@ -58,7 +58,7 @@
:placeholder="placeholder"
@focus="rows = 6"
@blur="rows = 1"
@keydown.enter="(e) => sendTextMessage(e)"
@keydown.enter.stop="(e) => sendTextMessage(e)"
/>
</div>
</template>
@ -78,7 +78,7 @@ const doc = defineModel()
const whatsapp = defineModel('whatsapp')
const reply = defineModel('reply')
const rows = ref(1)
const textarea = ref(null)
const textareaRef = ref(null)
const emoji = ref('')
const content = ref('')
@ -86,7 +86,7 @@ const placeholder = ref(__('Type your message here...'))
const fileType = ref('')
function show() {
nextTick(() => textarea.value.$el.focus())
nextTick(() => textareaRef.value.el.focus())
}
function uploadFile(file) {
@ -99,7 +99,7 @@ function uploadFile(file) {
function sendTextMessage(event) {
if (event.shiftKey) return
sendWhatsAppMessage()
textarea.value.$el.blur()
textareaRef.value.el?.blur()
content.value = ''
capture('whatsapp_send_message')
}

View File

@ -48,7 +48,7 @@
<FormControl
v-if="field.read_only && field.type !== 'Check'"
type="text"
:placeholder="__(field.placeholder || field.label)"
:placeholder="getPlaceholder(field)"
v-model="data[field.name]"
:disabled="true"
/>
@ -59,7 +59,7 @@
:class="field.prefix ? 'prefix' : ''"
:options="field.options"
v-model="data[field.name]"
:placeholder="__(field.placeholder || field.label)"
:placeholder="getPlaceholder(field)"
>
<template v-if="field.prefix" #prefix>
<IndicatorIcon :class="field.prefix" />
@ -91,7 +91,7 @@
:doctype="field.options"
:filters="field.filters"
@change="(v) => (data[field.name] = v)"
:placeholder="__(field.placeholder || field.label)"
:placeholder="getPlaceholder(field)"
:onCreate="field.create"
/>
<Button
@ -113,7 +113,7 @@
:doctype="field.options"
:filters="field.filters"
@change="(v) => (data[field.name] = v)"
:placeholder="__(field.placeholder || field.label)"
:placeholder="getPlaceholder(field)"
:hideMe="true"
>
<template #prefix>
@ -182,13 +182,13 @@
<DateTimePicker
v-else-if="field.type === 'Datetime'"
v-model="data[field.name]"
:placeholder="__(field.placeholder || field.label)"
:placeholder="getPlaceholder(field)"
input-class="border-none"
/>
<DatePicker
v-else-if="field.type === 'Date'"
v-model="data[field.name]"
:placeholder="__(field.placeholder || field.label)"
:placeholder="getPlaceholder(field)"
input-class="border-none"
/>
<FormControl
@ -196,19 +196,19 @@
['Small Text', 'Text', 'Long Text'].includes(field.type)
"
type="textarea"
:placeholder="__(field.placeholder || field.label)"
:placeholder="getPlaceholder(field)"
v-model="data[field.name]"
/>
<FormControl
v-else-if="['Int'].includes(field.type)"
type="number"
:placeholder="__(field.placeholder || field.label)"
:placeholder="getPlaceholder(field)"
v-model="data[field.name]"
/>
<FormControl
v-else
type="text"
:placeholder="__(field.placeholder || field.label)"
:placeholder="getPlaceholder(field)"
v-model="data[field.name]"
:disabled="Boolean(field.read_only)"
/>
@ -235,6 +235,17 @@ const props = defineProps({
sections: Array,
data: Object,
})
const getPlaceholder = (field) => {
if (field.placeholder) {
return __(field.placeholder)
}
if (['Select', 'Link'].includes(field.type)) {
return __('Select {0}', [__(field.label)])
} else {
return __('Enter {0}', [__(field.label)])
}
}
</script>
<style scoped>

View File

@ -141,7 +141,7 @@ const props = defineProps({
const show = defineModel()
const tasks = defineModel('reloadTasks')
const emit = defineEmits(['updateTask'])
const emit = defineEmits(['updateTask', 'after'])
const router = useRouter()
const { getUser } = usersStore()
@ -202,6 +202,7 @@ async function updateTask() {
if (d.name) {
capture('task_created')
tasks.value.reload()
emit('after')
}
}
show.value = false

View File

@ -147,10 +147,11 @@ function getRoute(notification) {
dealId: notification.reference_name,
}
}
return {
name: notification.route_name,
params: params,
hash: '#' + notification.comment || notification.notification_type_doc,
hash: notification.hash,
}
}

View File

@ -55,9 +55,9 @@
v-else-if="field.type === 'select'"
class="form-control cursor-pointer [&_select]:cursor-pointer"
type="select"
:value="data[field.name]"
v-model="data[field.name]"
:options="field.options"
:debounce="500"
:placeholder="field.placeholder"
@change.stop="emit('update', field.name, $event.target.value)"
/>
<Link
@ -141,7 +141,7 @@ const data = defineModel()
const _fields = computed(() => {
let all_fields = []
props.fields?.forEach((field) => {
let df = field.all_properties
let df = field?.all_properties
if (df?.depends_on) evaluate_depends_on(df.depends_on, field)
all_fields.push({
...field,

View File

@ -133,9 +133,9 @@ function saveChanges() {
let _sections = JSON.parse(JSON.stringify(sections.data))
_sections.forEach((section) => {
if (!section.fields) return
section.fields = section.fields.map(
(field) => field.fieldname || field.name,
)
section.fields = section.fields
.map((field) => field.name || field.fieldname)
.filter(Boolean)
})
loading.value = true
call(

View File

@ -3,16 +3,17 @@ import { useRoute, useRouter } from 'vue-router'
import { useDebounceFn, useStorage } from '@vueuse/core'
export function useActiveTabManager(tabs, storageKey) {
const activieTab = useStorage(storageKey, 'activity')
const activeTab = useStorage(storageKey, 'activity')
const route = useRoute()
const router = useRouter()
const preserveLastVisitedTab = useDebounceFn((tabName) => {
activieTab.value = tabName.toLowerCase()
activeTab.value = tabName.toLowerCase()
}, 300)
function setActiveTabInUrl(tabName) {
let hash = '#' + tabName.toLowerCase()
if (route.hash === hash) return
router.push({ ...route, hash })
}
@ -21,7 +22,7 @@ export function useActiveTabManager(tabs, storageKey) {
}
function findTabIndex(tabName) {
return tabs.value.findIndex(
return tabs.value?.findIndex(
(tabOptions) => tabOptions.name.toLowerCase() === tabName,
)
}
@ -31,22 +32,18 @@ export function useActiveTabManager(tabs, storageKey) {
return index !== -1 ? index : 0 // Default to the first tab if not found
}
function getActiveTabFromLocalStorage() {
return activieTab.value
}
function getActiveTab() {
let activeTab = getActiveTabFromUrl()
if (activeTab) {
let index = findTabIndex(activeTab)
let _activeTab = getActiveTabFromUrl()
if (_activeTab) {
let index = findTabIndex(_activeTab)
if (index !== -1) {
preserveLastVisitedTab(activeTab)
preserveLastVisitedTab(_activeTab)
return index
}
return 0
}
let lastVisitedTab = getActiveTabFromLocalStorage()
let lastVisitedTab = activeTab.value
if (lastVisitedTab) {
return getTabIndex(lastVisitedTab)
}
@ -57,7 +54,7 @@ export function useActiveTabManager(tabs, storageKey) {
const tabIndex = ref(getActiveTab())
watch(tabIndex, (tabIndexValue) => {
let currentTab = tabs.value[tabIndexValue].name
let currentTab = tabs.value?.[tabIndexValue].name
setActiveTabInUrl(currentTab)
preserveLastVisitedTab(currentTab)
})
@ -71,11 +68,15 @@ export function useActiveTabManager(tabs, storageKey) {
let index = findTabIndex(tabName)
if (index === -1) index = 0
let currentTab = tabs.value[index].name
let currentTab = tabs.value?.[index].name
preserveLastVisitedTab(currentTab)
tabIndex.value = index
},
)
watch(tabs, () => {
tabIndex.value = getActiveTab()
})
return { tabIndex }
}

View File

@ -40,7 +40,7 @@
<Activities
ref="activities"
doctype="CRM Deal"
:title="tab.name"
:tabs="tabs"
v-model:reload="reload"
v-model:tabIndex="tabIndex"
v-model="deal"
@ -356,7 +356,6 @@ import { ref, computed, h, onMounted, onBeforeUnmount } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useActiveTabManager } from '@/composables/useActiveTabManager'
const { $dialog, $socket, makeCall } = globalStore()
const { statusOptions, getDealStatus } = statusesStore()
const { isManager } = usersStore()

View File

@ -45,7 +45,6 @@
<Activities
ref="activities"
doctype="CRM Lead"
:title="tab.name"
:tabs="tabs"
v-model:reload="reload"
v-model:tabIndex="tabIndex"

1931
yarn.lock

File diff suppressed because it is too large Load Diff