From 0b6d071fdc2e47e8a6e85d93f44805f5dcd86207 Mon Sep 17 00:00:00 2001 From: gopikrishnan13 Date: Wed, 25 Sep 2024 17:42:51 +0530 Subject: [PATCH 1/7] feat: Rework preserve last visited tab --- .../src/composables/useActiveTabManager.js | 68 +++++++++++++++++++ frontend/src/pages/Deal.vue | 4 +- frontend/src/pages/Lead.vue | 5 +- 3 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 frontend/src/composables/useActiveTabManager.js diff --git a/frontend/src/composables/useActiveTabManager.js b/frontend/src/composables/useActiveTabManager.js new file mode 100644 index 000000000..9f99306bd --- /dev/null +++ b/frontend/src/composables/useActiveTabManager.js @@ -0,0 +1,68 @@ +import { ref, watch } from 'vue'; + +// Debounce function to delay updates +function debounce(fn, delay) { + let timeout; + return (...args) => { + clearTimeout(timeout); + timeout = setTimeout(() => fn(...args), delay); + }; +} + +export function useActiveTabManager(tabs, storageKey) { + + const preserveLastVisitedTab = debounce((tabName) => { + localStorage.setItem(storageKey, tabName.toLowerCase()); + }, 300); + + function setActiveTabInUrl(tabName) { + window.location.hash = '#' + tabName.toLowerCase(); + } + + function getActiveTabFromUrl() { + return window.location.hash.replace('#', ''); + } + + function findTabIndex(tabName){ + return tabs.value.findIndex(tabOptions => tabOptions.name.toLowerCase() === tabName); + } + + function getTabIndex(tabName) { + let index = findTabIndex(tabName) + return index !== -1 ? index : 0; // Default to the first tab if not found + } + + function getActiveTabFromLocalStorage() { + return localStorage.getItem(storageKey); + } + + function getActiveTab() { + let activeTab = getActiveTabFromUrl(); + if(activeTab){ + let index = findTabIndex(activeTab) + if(index !== -1){ + preserveLastVisitedTab(activeTab) + return index + } + return 0 + } + + let lastVisitedTab = getActiveTabFromLocalStorage(); + if(lastVisitedTab){ + setActiveTabInUrl(lastVisitedTab) + return getTabIndex(lastVisitedTab) + } + + return 0 // Default to the first tab if nothing is found + } + + const tabIndex = ref(getActiveTab()); + + watch(tabIndex, (tabIndexValue) => { + let currentTab = tabs.value[tabIndexValue].name; + setActiveTabInUrl(currentTab); + preserveLastVisitedTab(currentTab); + }); + + return { tabIndex }; +} diff --git a/frontend/src/pages/Deal.vue b/frontend/src/pages/Deal.vue index c9ef4fbb5..363cc179f 100644 --- a/frontend/src/pages/Deal.vue +++ b/frontend/src/pages/Deal.vue @@ -354,6 +354,8 @@ import { } from 'frappe-ui' 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() @@ -513,7 +515,6 @@ usePageMeta(() => { } }) -const tabIndex = ref(0) const tabs = computed(() => { let tabOptions = [ { @@ -556,6 +557,7 @@ const tabs = computed(() => { ] return tabOptions.filter((tab) => (tab.condition ? tab.condition() : true)) }) +const { tabIndex } = useActiveTabManager(tabs, 'lastDealTab') const fieldsLayout = createResource({ url: 'crm.api.doc.get_sidebar_fields', diff --git a/frontend/src/pages/Lead.vue b/frontend/src/pages/Lead.vue index 25fedfd5c..43c50441a 100644 --- a/frontend/src/pages/Lead.vue +++ b/frontend/src/pages/Lead.vue @@ -330,6 +330,7 @@ import { } from 'frappe-ui' import { ref, computed, onMounted, watch } from 'vue' import { useRouter, useRoute } from 'vue-router' +import {useActiveTabManager} from '@/composables/useActiveTabManager' const { $dialog, $socket, makeCall } = globalStore() const { getContactByName, contacts } = contactsStore() @@ -463,8 +464,6 @@ usePageMeta(() => { } }) -const tabIndex = ref(0) - const tabs = computed(() => { let tabOptions = [ { @@ -508,6 +507,8 @@ const tabs = computed(() => { return tabOptions.filter((tab) => (tab.condition ? tab.condition() : true)) }) +const { tabIndex } = useActiveTabManager(tabs, 'lastLeadTab') + watch(tabs, (value) => { if (value && route.params.tabName) { let index = value.findIndex( From 200c931f05948f6a26c842b31e2787b23d7d198a Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Fri, 27 Sep 2024 20:34:14 +0530 Subject: [PATCH 2/7] feat: allow link filter to work in Link Field component --- frontend/src/components/Controls/Link.vue | 6 ++++++ frontend/src/components/SectionFields.vue | 3 +++ 2 files changed, 9 insertions(+) diff --git a/frontend/src/components/Controls/Link.vue b/frontend/src/components/Controls/Link.vue index 30b0b282f..3bcb3d7f7 100644 --- a/frontend/src/components/Controls/Link.vue +++ b/frontend/src/components/Controls/Link.vue @@ -69,6 +69,10 @@ const props = defineProps({ type: String, required: true, }, + filters: { + type: Array, + default: () => [], + }, modelValue: { type: String, default: '', @@ -122,6 +126,7 @@ const options = createResource({ params: { txt: text.value, doctype: props.doctype, + filters: props.filters, }, transform: (data) => { let allData = data.map((option) => { @@ -152,6 +157,7 @@ function reload(val) { params: { txt: val, doctype: props.doctype, + filters: props.filters, }, }) options.reload() diff --git a/frontend/src/components/SectionFields.vue b/frontend/src/components/SectionFields.vue index 24b14f96d..f2029aa20 100644 --- a/frontend/src/components/SectionFields.vue +++ b/frontend/src/components/SectionFields.vue @@ -65,6 +65,7 @@ class="form-control" :value="data[field.name] && getUser(data[field.name]).full_name" doctype="User" + :filters="field.filters" @change="(data) => emit('update', field.name, data)" :placeholder="'Select' + ' ' + field.label + '...'" :hideMe="true" @@ -88,6 +89,7 @@ class="form-control select-text" :value="data[field.name]" :doctype="field.doctype" + :filters="field.filters" :placeholder="field.placeholder" @change="(data) => emit('update', field.name, data)" :onCreate="field.create" @@ -144,6 +146,7 @@ const _fields = computed(() => { if (df?.depends_on) evaluate_depends_on(df.depends_on, field) all_fields.push({ ...field, + filters: df.link_filters && JSON.parse(df.link_filters), placeholder: field.placeholder || field.label, }) }) From 45b006b580953f8fadf77c60599b87b3b5e56a99 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Fri, 27 Sep 2024 20:34:54 +0530 Subject: [PATCH 3/7] fix: only fetch org if org exist --- frontend/src/pages/Deal.vue | 11 +++++++---- frontend/src/pages/MobileDeal.vue | 11 +++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/frontend/src/pages/Deal.vue b/frontend/src/pages/Deal.vue index c9ef4fbb5..a04922627 100644 --- a/frontend/src/pages/Deal.vue +++ b/frontend/src/pages/Deal.vue @@ -376,10 +376,13 @@ const deal = createResource({ params: { name: props.dealId }, cache: ['deal', props.dealId], onSuccess: async (data) => { - organization.update({ - params: { doctype: 'CRM Organization', name: data.organization }, - }) - organization.fetch() + if (data.organization) { + organization.update({ + params: { doctype: 'CRM Organization', name: data.organization }, + }) + organization.fetch() + } + let obj = { doc: data, $dialog, diff --git a/frontend/src/pages/MobileDeal.vue b/frontend/src/pages/MobileDeal.vue index 63b567437..08b8a54d2 100644 --- a/frontend/src/pages/MobileDeal.vue +++ b/frontend/src/pages/MobileDeal.vue @@ -309,10 +309,13 @@ const deal = createResource({ params: { name: props.dealId }, cache: ['deal', props.dealId], onSuccess: async (data) => { - organization.update({ - params: { doctype: 'CRM Organization', name: data.organization }, - }) - organization.fetch() + if (data.organization) { + organization.update({ + params: { doctype: 'CRM Organization', name: data.organization }, + }) + organization.fetch() + } + let obj = { doc: data, $dialog, From 575d85dc657442927e27f7f1a8a597f0476b65bf Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Fri, 27 Sep 2024 20:48:10 +0530 Subject: [PATCH 4/7] fix: allow link filters to work in settings page --- crm/api/doc.py | 1 + frontend/src/components/Fields.vue | 2 ++ frontend/src/components/Settings/SettingsPage.vue | 1 + 3 files changed, 4 insertions(+) diff --git a/crm/api/doc.py b/crm/api/doc.py index a5a7c51e4..84a84458f 100644 --- a/crm/api/doc.py +++ b/crm/api/doc.py @@ -685,6 +685,7 @@ def get_fields(doctype: str, allow_all_fieldtypes: bool = False): "depends_on": field.depends_on, "mandatory_depends_on": field.mandatory_depends_on, "read_only_depends_on": field.read_only_depends_on, + "link_filters": field.get("link_filters"), }) return _fields diff --git a/frontend/src/components/Fields.vue b/frontend/src/components/Fields.vue index c2bcd3466..ac27882d9 100644 --- a/frontend/src/components/Fields.vue +++ b/frontend/src/components/Fields.vue @@ -89,6 +89,7 @@ class="form-control flex-1" :value="data[field.name]" :doctype="field.options" + :filters="field.filters" @change="(v) => (data[field.name] = v)" :placeholder="__(field.placeholder || field.label)" :onCreate="field.create" @@ -110,6 +111,7 @@ class="form-control" :value="getUser(data[field.name]).full_name" :doctype="field.options" + :filters="field.filters" @change="(v) => (data[field.name] = v)" :placeholder="__(field.placeholder || field.label)" :hideMe="true" diff --git a/frontend/src/components/Settings/SettingsPage.vue b/frontend/src/components/Settings/SettingsPage.vue index e9a53bf08..90c49e706 100644 --- a/frontend/src/components/Settings/SettingsPage.vue +++ b/frontend/src/components/Settings/SettingsPage.vue @@ -122,6 +122,7 @@ const sections = computed(() => { } else { _sections[_sections.length - 1].fields.push({ ...field, + filters: field.link_filters && JSON.parse(field.link_filters), display_via_depends_on: evaluate_depends_on_value( field.depends_on, data.doc, From dd441fbfce1ad6cbc218367a3805ec47f963ce04 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Fri, 27 Sep 2024 21:19:37 +0530 Subject: [PATCH 5/7] fix: show email status read/sent on UI --- crm/api/activities.py | 2 ++ .../src/components/Activities/EmailArea.vue | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/crm/api/activities.py b/crm/api/activities.py index ead84b1f4..31d745802 100644 --- a/crm/api/activities.py +++ b/crm/api/activities.py @@ -125,6 +125,7 @@ def get_deal_activities(name): "bcc": communication.bcc, "attachments": get_attachments('Communication', communication.name), "read_by_recipient": communication.read_by_recipient, + "delivery_status": communication.delivery_status, }, "is_lead": False, } @@ -238,6 +239,7 @@ def get_lead_activities(name): "bcc": communication.bcc, "attachments": get_attachments('Communication', communication.name), "read_by_recipient": communication.read_by_recipient, + "delivery_status": communication.delivery_status, }, "is_lead": True, } diff --git a/frontend/src/components/Activities/EmailArea.vue b/frontend/src/components/Activities/EmailArea.vue index d42db18cf..33e11f16b 100644 --- a/frontend/src/components/Activities/EmailArea.vue +++ b/frontend/src/components/Activities/EmailArea.vue @@ -16,6 +16,12 @@ />
+
{{ __(timeAgo(activity.creation)) }} @@ -87,6 +93,7 @@ import AttachmentItem from '@/components/AttachmentItem.vue' import EmailContent from '@/components/Activities/EmailContent.vue' import { Badge, Tooltip } from 'frappe-ui' import { timeAgo, dateFormat, dateTooltipFormat } from '@/utils' +import { computed } from 'vue' const props = defineProps({ activity: Object, @@ -140,4 +147,19 @@ function reply(email, reply_all = false) { .focus('start') .run() } + +const status = computed(() => { + let _status = props.activity?.data?.delivery_status + let indicator_color = 'red' + if (['Sent', 'Clicked'].includes(_status)) { + indicator_color = 'green' + } else if (['Sending', 'Scheduled'].includes(_status)) { + indicator_color = 'orange' + } else if (['Opened', 'Read'].includes(_status)) { + indicator_color = 'blue' + } else if (_status == 'Error') { + indicator_color = 'red' + } + return { label: _status, color: indicator_color } +}) From 97af42fad10684e313ebb61f50507feb6dfa3810 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Fri, 27 Sep 2024 22:08:48 +0530 Subject: [PATCH 6/7] chore: used debounce function from vueuse and useStorage instead of localStorage --- .../src/composables/useActiveTabManager.js | 57 +++++++++---------- frontend/src/pages/Deal.vue | 2 +- frontend/src/pages/Lead.vue | 2 +- 3 files changed, 28 insertions(+), 33 deletions(-) diff --git a/frontend/src/composables/useActiveTabManager.js b/frontend/src/composables/useActiveTabManager.js index 9f99306bd..3a134869e 100644 --- a/frontend/src/composables/useActiveTabManager.js +++ b/frontend/src/composables/useActiveTabManager.js @@ -1,54 +1,49 @@ -import { ref, watch } from 'vue'; - -// Debounce function to delay updates -function debounce(fn, delay) { - let timeout; - return (...args) => { - clearTimeout(timeout); - timeout = setTimeout(() => fn(...args), delay); - }; -} +import { ref, watch } from 'vue' +import { useDebounceFn, useStorage } from '@vueuse/core' export function useActiveTabManager(tabs, storageKey) { - - const preserveLastVisitedTab = debounce((tabName) => { - localStorage.setItem(storageKey, tabName.toLowerCase()); - }, 300); + const activieTab = useStorage(storageKey, 'activity') + + const preserveLastVisitedTab = useDebounceFn((tabName) => { + activieTab.value = tabName.toLowerCase() + }, 300) function setActiveTabInUrl(tabName) { - window.location.hash = '#' + tabName.toLowerCase(); + window.location.hash = '#' + tabName.toLowerCase() } function getActiveTabFromUrl() { - return window.location.hash.replace('#', ''); + return window.location.hash.replace('#', '') } - function findTabIndex(tabName){ - return tabs.value.findIndex(tabOptions => tabOptions.name.toLowerCase() === tabName); + function findTabIndex(tabName) { + return tabs.value.findIndex( + (tabOptions) => tabOptions.name.toLowerCase() === tabName, + ) } function getTabIndex(tabName) { let index = findTabIndex(tabName) - return index !== -1 ? index : 0; // Default to the first tab if not found + return index !== -1 ? index : 0 // Default to the first tab if not found } function getActiveTabFromLocalStorage() { - return localStorage.getItem(storageKey); + return activieTab.value } function getActiveTab() { - let activeTab = getActiveTabFromUrl(); - if(activeTab){ + let activeTab = getActiveTabFromUrl() + if (activeTab) { let index = findTabIndex(activeTab) - if(index !== -1){ + if (index !== -1) { preserveLastVisitedTab(activeTab) return index } return 0 } - let lastVisitedTab = getActiveTabFromLocalStorage(); - if(lastVisitedTab){ + let lastVisitedTab = getActiveTabFromLocalStorage() + if (lastVisitedTab) { setActiveTabInUrl(lastVisitedTab) return getTabIndex(lastVisitedTab) } @@ -56,13 +51,13 @@ export function useActiveTabManager(tabs, storageKey) { return 0 // Default to the first tab if nothing is found } - const tabIndex = ref(getActiveTab()); + const tabIndex = ref(getActiveTab()) watch(tabIndex, (tabIndexValue) => { - let currentTab = tabs.value[tabIndexValue].name; - setActiveTabInUrl(currentTab); - preserveLastVisitedTab(currentTab); - }); + let currentTab = tabs.value[tabIndexValue].name + setActiveTabInUrl(currentTab) + preserveLastVisitedTab(currentTab) + }) - return { tabIndex }; + return { tabIndex } } diff --git a/frontend/src/pages/Deal.vue b/frontend/src/pages/Deal.vue index 363cc179f..9c73e7beb 100644 --- a/frontend/src/pages/Deal.vue +++ b/frontend/src/pages/Deal.vue @@ -354,7 +354,7 @@ import { } from 'frappe-ui' import { ref, computed, h, onMounted, onBeforeUnmount } from 'vue' import { useRoute, useRouter } from 'vue-router' -import {useActiveTabManager} from '@/composables/useActiveTabManager' +import { useActiveTabManager } from '@/composables/useActiveTabManager' const { $dialog, $socket, makeCall } = globalStore() diff --git a/frontend/src/pages/Lead.vue b/frontend/src/pages/Lead.vue index 43c50441a..03e106705 100644 --- a/frontend/src/pages/Lead.vue +++ b/frontend/src/pages/Lead.vue @@ -330,7 +330,7 @@ import { } from 'frappe-ui' import { ref, computed, onMounted, watch } from 'vue' import { useRouter, useRoute } from 'vue-router' -import {useActiveTabManager} from '@/composables/useActiveTabManager' +import { useActiveTabManager } from '@/composables/useActiveTabManager' const { $dialog, $socket, makeCall } = globalStore() const { getContactByName, contacts } = contactsStore() From 028ad81d7f875c8efeb530d02000d6ef556a0b3c Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Fri, 27 Sep 2024 22:36:30 +0530 Subject: [PATCH 7/7] fix: also watch hash change and change tab accordingly --- .../src/composables/useActiveTabManager.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/frontend/src/composables/useActiveTabManager.js b/frontend/src/composables/useActiveTabManager.js index 3a134869e..5b9cd4848 100644 --- a/frontend/src/composables/useActiveTabManager.js +++ b/frontend/src/composables/useActiveTabManager.js @@ -1,8 +1,10 @@ import { ref, watch } from 'vue' +import { useRoute } from 'vue-router' import { useDebounceFn, useStorage } from '@vueuse/core' export function useActiveTabManager(tabs, storageKey) { const activieTab = useStorage(storageKey, 'activity') + const route = useRoute() const preserveLastVisitedTab = useDebounceFn((tabName) => { activieTab.value = tabName.toLowerCase() @@ -13,7 +15,7 @@ export function useActiveTabManager(tabs, storageKey) { } function getActiveTabFromUrl() { - return window.location.hash.replace('#', '') + return route.hash.replace('#', '') } function findTabIndex(tabName) { @@ -59,5 +61,20 @@ export function useActiveTabManager(tabs, storageKey) { preserveLastVisitedTab(currentTab) }) + watch( + () => route.hash, + (tabValue) => { + if (!tabValue) return + + let tabName = tabValue.replace('#', '') + let index = findTabIndex(tabName) + if (index === -1) index = 0 + + let currentTab = tabs.value[index].name + preserveLastVisitedTab(currentTab) + tabIndex.value = index + }, + ) + return { tabIndex } }