Merge pull request #773 from frappe/main-hotfix
@ -30,15 +30,7 @@ def get_contact(name):
|
||||
|
||||
if not len(contact):
|
||||
frappe.throw(_("Contact not found"), frappe.DoesNotExistError)
|
||||
contact = contact.pop()
|
||||
|
||||
contact["doctype"] = "Contact"
|
||||
contact["email_ids"] = frappe.get_all(
|
||||
"Contact Email", filters={"parent": name}, fields=["name", "email_id", "is_primary"]
|
||||
)
|
||||
contact["phone_nos"] = frappe.get_all(
|
||||
"Contact Phone", filters={"parent": name}, fields=["name", "phone", "is_primary_mobile_no"]
|
||||
)
|
||||
return contact
|
||||
|
||||
|
||||
|
||||
99
crm/api/settings.py
Normal file
@ -0,0 +1,99 @@
|
||||
import frappe
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_email_account(data):
|
||||
service = data.get("service")
|
||||
service_config = email_service_config.get(service)
|
||||
if not service_config:
|
||||
return "Service not supported"
|
||||
|
||||
try:
|
||||
email_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Email Account",
|
||||
"email_id": data.get("email_id"),
|
||||
"email_account_name": data.get("email_account_name"),
|
||||
"service": service,
|
||||
"enable_incoming": data.get("enable_incoming"),
|
||||
"enable_outgoing": data.get("enable_outgoing"),
|
||||
"default_incoming": data.get("default_incoming"),
|
||||
"default_outgoing": data.get("default_outgoing"),
|
||||
"email_sync_option": "ALL",
|
||||
"initial_sync_count": 100,
|
||||
"create_contact": 1,
|
||||
"track_email_status": 1,
|
||||
"use_tls": 1,
|
||||
"use_imap": 1,
|
||||
"smtp_port": 587,
|
||||
**service_config,
|
||||
}
|
||||
)
|
||||
if service == "Frappe Mail":
|
||||
email_doc.api_key = data.get("api_key")
|
||||
email_doc.api_secret = data.get("api_secret")
|
||||
email_doc.frappe_mail_site = data.get("frappe_mail_site")
|
||||
email_doc.append_to = "CRM Lead"
|
||||
else:
|
||||
email_doc.append("imap_folder", {"append_to": "CRM Lead", "folder_name": "INBOX"})
|
||||
email_doc.password = data.get("password")
|
||||
# validate whether the credentials are correct
|
||||
email_doc.get_incoming_server()
|
||||
|
||||
# if correct credentials, save the email account
|
||||
email_doc.save()
|
||||
except Exception as e:
|
||||
frappe.throw(str(e))
|
||||
|
||||
|
||||
email_service_config = {
|
||||
"Frappe Mail": {
|
||||
"domain": None,
|
||||
"password": None,
|
||||
"awaiting_password": 0,
|
||||
"ascii_encode_password": 0,
|
||||
"login_id_is_different": 0,
|
||||
"login_id": None,
|
||||
"use_imap": 0,
|
||||
"use_ssl": 0,
|
||||
"validate_ssl_certificate": 0,
|
||||
"use_starttls": 0,
|
||||
"email_server": None,
|
||||
"incoming_port": 0,
|
||||
"always_use_account_email_id_as_sender": 1,
|
||||
"use_tls": 0,
|
||||
"use_ssl_for_outgoing": 0,
|
||||
"smtp_server": None,
|
||||
"smtp_port": None,
|
||||
"no_smtp_authentication": 0,
|
||||
},
|
||||
"GMail": {
|
||||
"email_server": "imap.gmail.com",
|
||||
"use_ssl": 1,
|
||||
"smtp_server": "smtp.gmail.com",
|
||||
},
|
||||
"Outlook": {
|
||||
"email_server": "imap-mail.outlook.com",
|
||||
"use_ssl": 1,
|
||||
"smtp_server": "smtp-mail.outlook.com",
|
||||
},
|
||||
"Sendgrid": {
|
||||
"smtp_server": "smtp.sendgrid.net",
|
||||
"smtp_port": 587,
|
||||
},
|
||||
"SparkPost": {
|
||||
"smtp_server": "smtp.sparkpostmail.com",
|
||||
},
|
||||
"Yahoo": {
|
||||
"email_server": "imap.mail.yahoo.com",
|
||||
"use_ssl": 1,
|
||||
"smtp_server": "smtp.mail.yahoo.com",
|
||||
"smtp_port": 587,
|
||||
},
|
||||
"Yandex": {
|
||||
"email_server": "imap.yandex.com",
|
||||
"use_ssl": 1,
|
||||
"smtp_server": "smtp.yandex.com",
|
||||
"smtp_port": 587,
|
||||
},
|
||||
}
|
||||
@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Frappe CRM VERSION\n"
|
||||
"Report-Msgid-Bugs-To: shariq@frappe.io\n"
|
||||
"POT-Creation-Date: 2025-04-13 09:36+0000\n"
|
||||
"PO-Revision-Date: 2025-04-13 09:36+0000\n"
|
||||
"POT-Creation-Date: 2025-04-20 09:35+0000\n"
|
||||
"PO-Revision-Date: 2025-04-20 09:35+0000\n"
|
||||
"Last-Translator: shariq@frappe.io\n"
|
||||
"Language-Team: shariq@frappe.io\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@ -16,7 +16,7 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.13.1\n"
|
||||
|
||||
#: frontend/src/components/ViewControls.vue:1206
|
||||
#: frontend/src/components/ViewControls.vue:1219
|
||||
msgid " (New)"
|
||||
msgstr ""
|
||||
|
||||
@ -159,8 +159,8 @@ msgid "Account SID"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/CustomActions.vue:73
|
||||
#: frontend/src/components/ViewControls.vue:669
|
||||
#: frontend/src/components/ViewControls.vue:1098
|
||||
#: frontend/src/components/ViewControls.vue:682
|
||||
#: frontend/src/components/ViewControls.vue:1111
|
||||
msgid "Actions"
|
||||
msgstr ""
|
||||
|
||||
@ -933,13 +933,13 @@ msgstr ""
|
||||
#: frontend/src/components/Modals/TaskModal.vue:8
|
||||
#: frontend/src/components/Modals/ViewModal.vue:16
|
||||
#: frontend/src/pages/CallLogs.vue:11 frontend/src/pages/Contacts.vue:13
|
||||
#: frontend/src/pages/Contacts.vue:57 frontend/src/pages/Deals.vue:13
|
||||
#: frontend/src/pages/Deals.vue:233 frontend/src/pages/EmailTemplates.vue:13
|
||||
#: frontend/src/pages/EmailTemplates.vue:58 frontend/src/pages/Leads.vue:13
|
||||
#: frontend/src/pages/Leads.vue:259 frontend/src/pages/Notes.vue:7
|
||||
#: frontend/src/pages/Contacts.vue:60 frontend/src/pages/Deals.vue:13
|
||||
#: frontend/src/pages/Deals.vue:236 frontend/src/pages/EmailTemplates.vue:13
|
||||
#: frontend/src/pages/EmailTemplates.vue:61 frontend/src/pages/Leads.vue:13
|
||||
#: frontend/src/pages/Leads.vue:262 frontend/src/pages/Notes.vue:7
|
||||
#: frontend/src/pages/Notes.vue:93 frontend/src/pages/Organizations.vue:13
|
||||
#: frontend/src/pages/Organizations.vue:57 frontend/src/pages/Tasks.vue:11
|
||||
#: frontend/src/pages/Tasks.vue:182
|
||||
#: frontend/src/pages/Organizations.vue:60 frontend/src/pages/Tasks.vue:11
|
||||
#: frontend/src/pages/Tasks.vue:185
|
||||
msgid "Create"
|
||||
msgstr ""
|
||||
|
||||
@ -979,7 +979,7 @@ msgid "Create Task"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/Modals/ViewModal.vue:9
|
||||
#: frontend/src/components/ViewControls.vue:673
|
||||
#: frontend/src/components/ViewControls.vue:686
|
||||
msgid "Create View"
|
||||
msgstr ""
|
||||
|
||||
@ -1155,8 +1155,8 @@ msgstr ""
|
||||
#: frontend/src/components/ListBulkActions.vue:96
|
||||
#: frontend/src/components/ListBulkActions.vue:104
|
||||
#: frontend/src/components/ListBulkActions.vue:186
|
||||
#: frontend/src/components/ViewControls.vue:1150
|
||||
#: frontend/src/components/ViewControls.vue:1161
|
||||
#: frontend/src/components/ViewControls.vue:1163
|
||||
#: frontend/src/components/ViewControls.vue:1174
|
||||
#: frontend/src/pages/Contact.vue:105 frontend/src/pages/Contact.vue:320
|
||||
#: frontend/src/pages/MobileContact.vue:81
|
||||
#: frontend/src/pages/MobileContact.vue:295
|
||||
@ -1164,7 +1164,7 @@ msgstr ""
|
||||
#: frontend/src/pages/MobileOrganization.vue:72
|
||||
#: frontend/src/pages/MobileOrganization.vue:289
|
||||
#: frontend/src/pages/Notes.vue:40 frontend/src/pages/Organization.vue:83
|
||||
#: frontend/src/pages/Organization.vue:327 frontend/src/pages/Tasks.vue:365
|
||||
#: frontend/src/pages/Organization.vue:327 frontend/src/pages/Tasks.vue:368
|
||||
msgid "Delete"
|
||||
msgstr ""
|
||||
|
||||
@ -1176,8 +1176,8 @@ msgstr ""
|
||||
msgid "Delete Task"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/ViewControls.vue:1146
|
||||
#: frontend/src/components/ViewControls.vue:1154
|
||||
#: frontend/src/components/ViewControls.vue:1159
|
||||
#: frontend/src/components/ViewControls.vue:1167
|
||||
msgid "Delete View"
|
||||
msgstr ""
|
||||
|
||||
@ -1288,7 +1288,7 @@ msgid "Due Date"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/Modals/ViewModal.vue:15
|
||||
#: frontend/src/components/ViewControls.vue:1102
|
||||
#: frontend/src/components/ViewControls.vue:1115
|
||||
msgid "Duplicate"
|
||||
msgstr ""
|
||||
|
||||
@ -1345,7 +1345,7 @@ msgstr ""
|
||||
#: frontend/src/components/FieldLayoutEditor.vue:319
|
||||
#: frontend/src/components/FieldLayoutEditor.vue:345
|
||||
#: frontend/src/components/ListBulkActions.vue:179
|
||||
#: frontend/src/components/ViewControls.vue:1120
|
||||
#: frontend/src/components/ViewControls.vue:1133
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
||||
@ -1799,7 +1799,7 @@ msgstr ""
|
||||
#. Label of the group_by_tab (Tab Break) field in DocType 'CRM View Settings'
|
||||
#: crm/fcrm/doctype/crm_view_settings/crm_view_settings.json
|
||||
#: frontend/src/components/ViewControls.vue:378
|
||||
#: frontend/src/components/ViewControls.vue:597 frontend/src/utils/view.js:16
|
||||
#: frontend/src/components/ViewControls.vue:610 frontend/src/utils/view.js:16
|
||||
msgid "Group By"
|
||||
msgstr ""
|
||||
|
||||
@ -2073,7 +2073,7 @@ msgstr ""
|
||||
#. Label of the kanban_tab (Tab Break) field in DocType 'CRM View Settings'
|
||||
#: crm/fcrm/doctype/crm_view_settings/crm_view_settings.json
|
||||
#: frontend/src/components/ViewControls.vue:383
|
||||
#: frontend/src/components/ViewControls.vue:586 frontend/src/utils/view.js:20
|
||||
#: frontend/src/components/ViewControls.vue:599 frontend/src/utils/view.js:20
|
||||
msgid "Kanban"
|
||||
msgstr ""
|
||||
|
||||
@ -2229,7 +2229,7 @@ msgstr ""
|
||||
#: crm/fcrm/doctype/crm_form_script/crm_form_script.json
|
||||
#: crm/fcrm/doctype/crm_view_settings/crm_view_settings.json
|
||||
#: frontend/src/components/ViewControls.vue:373
|
||||
#: frontend/src/components/ViewControls.vue:575 frontend/src/utils/view.js:12
|
||||
#: frontend/src/components/ViewControls.vue:588 frontend/src/utils/view.js:12
|
||||
msgid "List"
|
||||
msgstr ""
|
||||
|
||||
@ -2279,18 +2279,18 @@ msgstr ""
|
||||
msgid "Make Call"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/ViewControls.vue:1135
|
||||
#: frontend/src/components/ViewControls.vue:1148
|
||||
msgid "Make Private"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/ViewControls.vue:1135
|
||||
#: frontend/src/components/ViewControls.vue:1148
|
||||
msgid "Make Public"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/Activities/Activities.vue:792
|
||||
#: frontend/src/components/Activities/ActivityHeader.vue:142
|
||||
#: frontend/src/components/Activities/ActivityHeader.vue:185
|
||||
#: frontend/src/pages/Deals.vue:509 frontend/src/pages/Leads.vue:532
|
||||
#: frontend/src/pages/Deals.vue:512 frontend/src/pages/Leads.vue:535
|
||||
msgid "Make a Call"
|
||||
msgstr ""
|
||||
|
||||
@ -2433,7 +2433,7 @@ msgstr ""
|
||||
#: crm/fcrm/doctype/crm_dropdown_item/crm_dropdown_item.json
|
||||
#: crm/fcrm/doctype/fcrm_settings/fcrm_settings.json
|
||||
#: frontend/src/components/Modals/EmailTemplateModal.vue:24
|
||||
#: frontend/src/components/ViewControls.vue:768
|
||||
#: frontend/src/components/ViewControls.vue:781
|
||||
#: frontend/src/pages/MobileOrganization.vue:527
|
||||
#: frontend/src/pages/Organization.vue:562
|
||||
msgid "Name"
|
||||
@ -2478,7 +2478,7 @@ msgstr ""
|
||||
|
||||
#: frontend/src/components/Activities/ActivityHeader.vue:42
|
||||
#: frontend/src/components/Activities/ActivityHeader.vue:148
|
||||
#: frontend/src/pages/Deals.vue:515 frontend/src/pages/Leads.vue:538
|
||||
#: frontend/src/pages/Deals.vue:518 frontend/src/pages/Leads.vue:541
|
||||
msgid "New Note"
|
||||
msgstr ""
|
||||
|
||||
@ -2498,7 +2498,7 @@ msgstr ""
|
||||
|
||||
#: frontend/src/components/Activities/ActivityHeader.vue:52
|
||||
#: frontend/src/components/Activities/ActivityHeader.vue:153
|
||||
#: frontend/src/pages/Deals.vue:520 frontend/src/pages/Leads.vue:543
|
||||
#: frontend/src/pages/Deals.vue:523 frontend/src/pages/Leads.vue:546
|
||||
msgid "New Task"
|
||||
msgstr ""
|
||||
|
||||
@ -2601,13 +2601,13 @@ msgstr ""
|
||||
msgid "No {0} Available"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/CallLogs.vue:53 frontend/src/pages/Contact.vue:165
|
||||
#: frontend/src/pages/Contacts.vue:56 frontend/src/pages/Deals.vue:232
|
||||
#: frontend/src/pages/EmailTemplates.vue:57 frontend/src/pages/Leads.vue:258
|
||||
#: frontend/src/pages/CallLogs.vue:56 frontend/src/pages/Contact.vue:165
|
||||
#: frontend/src/pages/Contacts.vue:59 frontend/src/pages/Deals.vue:235
|
||||
#: frontend/src/pages/EmailTemplates.vue:60 frontend/src/pages/Leads.vue:261
|
||||
#: frontend/src/pages/MobileContact.vue:154
|
||||
#: frontend/src/pages/MobileOrganization.vue:143
|
||||
#: frontend/src/pages/Notes.vue:92 frontend/src/pages/Organization.vue:157
|
||||
#: frontend/src/pages/Organizations.vue:56 frontend/src/pages/Tasks.vue:181
|
||||
#: frontend/src/pages/Organizations.vue:59 frontend/src/pages/Tasks.vue:184
|
||||
msgid "No {0} Found"
|
||||
msgstr ""
|
||||
|
||||
@ -2912,7 +2912,7 @@ msgstr ""
|
||||
msgid "Phone Numbers"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/ViewControls.vue:1127
|
||||
#: frontend/src/components/ViewControls.vue:1140
|
||||
msgid "Pin View"
|
||||
msgstr ""
|
||||
|
||||
@ -2921,7 +2921,7 @@ msgstr ""
|
||||
msgid "Pinned"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/ViewControls.vue:663
|
||||
#: frontend/src/components/ViewControls.vue:676
|
||||
msgid "Pinned Views"
|
||||
msgstr ""
|
||||
|
||||
@ -3004,7 +3004,7 @@ msgstr ""
|
||||
msgid "Public"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/ViewControls.vue:658
|
||||
#: frontend/src/components/ViewControls.vue:671
|
||||
msgid "Public Views"
|
||||
msgstr ""
|
||||
|
||||
@ -3027,7 +3027,7 @@ msgstr ""
|
||||
msgid "Quick Filters"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/ViewControls.vue:719
|
||||
#: frontend/src/components/ViewControls.vue:732
|
||||
msgid "Quick Filters updated successfully"
|
||||
msgstr ""
|
||||
|
||||
@ -3358,7 +3358,7 @@ msgstr ""
|
||||
msgid "Save Changes"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/ViewControls.vue:653
|
||||
#: frontend/src/components/ViewControls.vue:666
|
||||
msgid "Saved Views"
|
||||
msgstr ""
|
||||
|
||||
@ -3437,7 +3437,7 @@ msgstr ""
|
||||
msgid "Set as Primary Contact"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/ViewControls.vue:1112
|
||||
#: frontend/src/components/ViewControls.vue:1125
|
||||
msgid "Set as default"
|
||||
msgstr ""
|
||||
|
||||
@ -3512,7 +3512,7 @@ msgstr ""
|
||||
msgid "Standard Form Scripts can not be modified, duplicate the Form Script instead."
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/ViewControls.vue:620
|
||||
#: frontend/src/components/ViewControls.vue:633
|
||||
msgid "Standard Views"
|
||||
msgstr ""
|
||||
|
||||
@ -3852,11 +3852,11 @@ msgstr ""
|
||||
msgid "Unknown"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/ViewControls.vue:1127
|
||||
#: frontend/src/components/ViewControls.vue:1140
|
||||
msgid "Unpin View"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/ViewControls.vue:964
|
||||
#: frontend/src/components/ViewControls.vue:977
|
||||
msgid "Unsaved Changes"
|
||||
msgstr ""
|
||||
|
||||
@ -3878,7 +3878,7 @@ msgstr ""
|
||||
#: frontend/src/components/Settings/SettingsPage.vue:31
|
||||
#: frontend/src/components/Settings/TelephonySettings.vue:70
|
||||
#: frontend/src/components/Telephony/ExotelCallUI.vue:219
|
||||
#: frontend/src/components/ViewControls.vue:969
|
||||
#: frontend/src/components/ViewControls.vue:982
|
||||
msgid "Update"
|
||||
msgstr ""
|
||||
|
||||
@ -4051,7 +4051,7 @@ msgstr ""
|
||||
msgid "You do not have mobile number set in your Telephony Agent"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/ViewControls.vue:965
|
||||
#: frontend/src/components/ViewControls.vue:978
|
||||
msgid "You have unsaved changes. Do you want to save them?"
|
||||
msgstr ""
|
||||
|
||||
|
||||
6
frontend/components.d.ts
vendored
@ -78,11 +78,17 @@ declare module 'vue' {
|
||||
EditIcon: typeof import('./src/components/Icons/EditIcon.vue')['default']
|
||||
EditValueModal: typeof import('./src/components/Modals/EditValueModal.vue')['default']
|
||||
Email2Icon: typeof import('./src/components/Icons/Email2Icon.vue')['default']
|
||||
EmailAccountCard: typeof import('./src/components/Settings/EmailAccountCard.vue')['default']
|
||||
EmailAccountList: typeof import('./src/components/Settings/EmailAccountList.vue')['default']
|
||||
EmailAdd: typeof import('./src/components/Settings/EmailAdd.vue')['default']
|
||||
EmailArea: typeof import('./src/components/Activities/EmailArea.vue')['default']
|
||||
EmailAtIcon: typeof import('./src/components/Icons/EmailAtIcon.vue')['default']
|
||||
EmailConfig: typeof import('./src/components/Settings/EmailConfig.vue')['default']
|
||||
EmailContent: typeof import('./src/components/Activities/EmailContent.vue')['default']
|
||||
EmailEdit: typeof import('./src/components/Settings/EmailEdit.vue')['default']
|
||||
EmailEditor: typeof import('./src/components/EmailEditor.vue')['default']
|
||||
EmailIcon: typeof import('./src/components/Icons/EmailIcon.vue')['default']
|
||||
EmailProviderIcon: typeof import('./src/components/Settings/EmailProviderIcon.vue')['default']
|
||||
EmailTemplateModal: typeof import('./src/components/Modals/EmailTemplateModal.vue')['default']
|
||||
EmailTemplateSelectorModal: typeof import('./src/components/Modals/EmailTemplateSelectorModal.vue')['default']
|
||||
EmailTemplatesListView: typeof import('./src/components/ListViews/EmailTemplatesListView.vue')['default']
|
||||
|
||||
4
frontend/public/images/frappe-mail.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.5714 44L31.4286 44C38.3716 44 44 38.3716 44 31.4286L44 12.5714C44 5.62842 38.3716 0 31.4286 0L12.5714 0C5.62842 0 0 5.62842 0 12.5714L0 31.4286C0 38.3716 5.62842 44 12.5714 44Z" fill="#0466DC"/>
|
||||
<path d="M9.42859 12.5715V14.8972L12.5714 17.4587L18.5743 22.3458C19.5329 23.1315 20.7586 23.5715 22 23.5715C23.2414 23.5715 24.4672 23.1315 25.4257 22.3458L31.4286 17.443V28.2701H12.5714V21.5287L9.42859 18.9672V27.4844C9.42859 29.653 11.1886 31.413 13.3572 31.413H30.6429C32.8115 31.413 34.5715 29.653 34.5715 27.4844V12.5715H9.42859ZM23.4457 19.9101C22.6286 20.5701 21.3714 20.5701 20.57 19.9101L15.4157 15.7144H28.6L23.4457 19.9101Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 763 B |
BIN
frontend/public/images/gmail.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
frontend/public/images/outlook.png
Normal file
|
After Width: | Height: | Size: 215 KiB |
BIN
frontend/public/images/sendgrid.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
frontend/public/images/sparkpost.webp
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
frontend/public/images/yahoo.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
frontend/public/images/yandex.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
@ -64,7 +64,7 @@ import LoadingIndicator from '@/components/Icons/LoadingIndicator.vue'
|
||||
import { createToast } from '@/utils'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { isMobileView } from '@/composables/settings'
|
||||
import { ref } from 'vue'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
doctype: {
|
||||
@ -114,4 +114,20 @@ const tabs = createResource({
|
||||
function saveChanges() {
|
||||
data.save.submit()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => data.doc,
|
||||
(newValue, oldValue) => {
|
||||
if (!oldValue) return
|
||||
if (newValue && oldValue) {
|
||||
const isDirty =
|
||||
JSON.stringify(newValue) !== JSON.stringify(data.originalDoc)
|
||||
data.isDirty = isDirty
|
||||
if (isDirty) {
|
||||
data.save.loading = false
|
||||
}
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
}"
|
||||
row-key="name"
|
||||
v-bind="$attrs"
|
||||
@update:selections="(selections) => emit('selectionsChanged', selections)"
|
||||
>
|
||||
<ListHeader
|
||||
class="sm:mx-5 mx-3"
|
||||
@ -205,6 +206,7 @@ const emit = defineEmits([
|
||||
'applyFilter',
|
||||
'applyLikeFilter',
|
||||
'likeDoc',
|
||||
'selectionsChanged',
|
||||
])
|
||||
|
||||
const pageLengthCount = defineModel()
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
resizeColumn: options.resizeColumn,
|
||||
}"
|
||||
row-key="name"
|
||||
@update:selections="(selections) => emit('selectionsChanged', selections)"
|
||||
>
|
||||
<ListHeader
|
||||
class="mx-3 sm:mx-5"
|
||||
@ -201,6 +202,7 @@ const emit = defineEmits([
|
||||
'applyFilter',
|
||||
'applyLikeFilter',
|
||||
'likeDoc',
|
||||
'selectionsChanged',
|
||||
])
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
resizeColumn: options.resizeColumn,
|
||||
}"
|
||||
row-key="name"
|
||||
@update:selections="(selections) => emit('selectionsChanged', selections)"
|
||||
>
|
||||
<ListHeader
|
||||
class="sm:mx-5 mx-3"
|
||||
@ -245,6 +246,7 @@ const emit = defineEmits([
|
||||
'applyFilter',
|
||||
'applyLikeFilter',
|
||||
'likeDoc',
|
||||
'selectionsChanged',
|
||||
])
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
resizeColumn: options.resizeColumn,
|
||||
}"
|
||||
row-key="name"
|
||||
@update:selections="(selections) => emit('selectionsChanged', selections)"
|
||||
>
|
||||
<ListHeader
|
||||
class="sm:mx-5 mx-3"
|
||||
@ -191,6 +192,7 @@ const emit = defineEmits([
|
||||
'applyFilter',
|
||||
'applyLikeFilter',
|
||||
'likeDoc',
|
||||
'selectionsChanged',
|
||||
])
|
||||
|
||||
const pageLengthCount = defineModel()
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
resizeColumn: options.resizeColumn,
|
||||
}"
|
||||
row-key="name"
|
||||
@update:selections="(selections) => emit('selectionsChanged', selections)"
|
||||
>
|
||||
<ListHeader
|
||||
class="sm:mx-5 mx-3"
|
||||
@ -250,7 +251,6 @@ const props = defineProps({
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'loadMore',
|
||||
'updatePageCount',
|
||||
@ -258,6 +258,7 @@ const emit = defineEmits([
|
||||
'applyFilter',
|
||||
'applyLikeFilter',
|
||||
'likeDoc',
|
||||
'selectionsChanged',
|
||||
])
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
resizeColumn: options.resizeColumn,
|
||||
}"
|
||||
row-key="name"
|
||||
@update:selections="(selections) => emit('selectionsChanged', selections)"
|
||||
>
|
||||
<ListHeader
|
||||
class="sm:mx-5 mx-3"
|
||||
@ -186,6 +187,7 @@ const emit = defineEmits([
|
||||
'applyFilter',
|
||||
'applyLikeFilter',
|
||||
'likeDoc',
|
||||
'selectionsChanged',
|
||||
])
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
resizeColumn: options.resizeColumn,
|
||||
}"
|
||||
row-key="name"
|
||||
@update:selections="(selections) => emit('selectionsChanged', selections)"
|
||||
>
|
||||
<ListHeader
|
||||
class="mx-3 sm:mx-5"
|
||||
@ -207,6 +208,7 @@ const emit = defineEmits([
|
||||
'applyFilter',
|
||||
'applyLikeFilter',
|
||||
'likeDoc',
|
||||
'selectionsChanged',
|
||||
])
|
||||
|
||||
const pageLengthCount = defineModel()
|
||||
|
||||
50
frontend/src/components/Settings/EmailAccountCard.vue
Normal file
@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between p-1 py-3 border-b border-gray-200 dark:border-gray-700 cursor-pointer"
|
||||
>
|
||||
<!-- avatar and name -->
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<EmailProviderIcon :logo="emailIcon[emailAccount.service]" />
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-ink-gray-9">
|
||||
{{ emailAccount.email_account_name }}
|
||||
</p>
|
||||
<div class="text-sm text-gray-500">{{ emailAccount.email_id }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Badge variant="subtle" :label="badgeTitle" :theme="gray" />
|
||||
</div>
|
||||
<!-- email id -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { emailIcon } from './emailConfig'
|
||||
import EmailProviderIcon from './EmailProviderIcon.vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
emailAccount: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const badgeTitle = computed(() => {
|
||||
if (
|
||||
props.emailAccount.default_incoming &&
|
||||
props.emailAccount.default_outgoing
|
||||
) {
|
||||
return __('Default Sending and Inbox')
|
||||
} else if (props.emailAccount.default_incoming) {
|
||||
return __('Default Inbox')
|
||||
} else if (props.emailAccount.default_outgoing) {
|
||||
return __('Default Sending')
|
||||
} else {
|
||||
return __('Inbox')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
64
frontend/src/components/Settings/EmailAccountList.vue
Normal file
@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- header -->
|
||||
<div class="flex items-center justify-between text-ink-gray-9">
|
||||
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5">
|
||||
{{ __('Email Accounts') }}
|
||||
</h2>
|
||||
<Button
|
||||
:label="__('Add Account')"
|
||||
theme="gray"
|
||||
variant="solid"
|
||||
@click="emit('update:step', 'email-add')"
|
||||
class="mr-8"
|
||||
>
|
||||
<template #prefix>
|
||||
<LucidePlus class="w-4 h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
<!-- list accounts -->
|
||||
<div
|
||||
v-if="!emailAccounts.loading && Boolean(emailAccounts.data?.length)"
|
||||
class="mt-4"
|
||||
>
|
||||
<div v-for="emailAccount in emailAccounts.data" :key="emailAccount.name">
|
||||
<EmailAccountCard
|
||||
:emailAccount="emailAccount"
|
||||
@click="emit('update:step', 'email-edit', emailAccount)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- fallback if no email accounts -->
|
||||
<div v-else class="flex items-center justify-center h-64 text-gray-500">
|
||||
{{ __('Please add an email account to continue.') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { createListResource } from 'frappe-ui'
|
||||
import EmailAccountCard from './EmailAccountCard.vue'
|
||||
|
||||
const emit = defineEmits(['update:step'])
|
||||
|
||||
const emailAccounts = createListResource({
|
||||
doctype: 'Email Account',
|
||||
cache: true,
|
||||
fields: ['*'],
|
||||
filters: {
|
||||
email_id: ['Not Like', '%example%'],
|
||||
},
|
||||
pageLength: 10,
|
||||
auto: true,
|
||||
onSuccess: (accounts) => {
|
||||
// convert 0 to false to handle boolean fields
|
||||
accounts.forEach((account) => {
|
||||
account.enable_incoming = Boolean(account.enable_incoming)
|
||||
account.enable_outgoing = Boolean(account.enable_outgoing)
|
||||
account.default_incoming = Boolean(account.default_incoming)
|
||||
account.default_outgoing = Boolean(account.default_outgoing)
|
||||
})
|
||||
},
|
||||
})
|
||||
</script>
|
||||
163
frontend/src/components/Settings/EmailAdd.vue
Normal file
@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full gap-4">
|
||||
<!-- title and desc -->
|
||||
<div role="heading" aria-level="1" class="flex flex-col gap-1">
|
||||
<h2 class="text-xl font-semibold text-ink-gray-9">
|
||||
{{ __('Setup Email') }}
|
||||
</h2>
|
||||
<p class="text-sm text-gray-600">
|
||||
{{ __('Choose the email service provider you want to configure.') }}
|
||||
</p>
|
||||
</div>
|
||||
<!-- email service provider selection -->
|
||||
<div class="flex flex-wrap items-center">
|
||||
<div
|
||||
v-for="s in services"
|
||||
:key="s.name"
|
||||
class="flex flex-col items-center gap-1 mt-4 w-[70px]"
|
||||
@click="handleSelect(s)"
|
||||
>
|
||||
<EmailProviderIcon
|
||||
:service-name="s.name"
|
||||
:logo="s.icon"
|
||||
:selected="selectedService?.name === s?.name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedService" class="flex flex-col gap-4">
|
||||
<!-- email service provider info -->
|
||||
<div
|
||||
class="flex items-center gap-2 p-2 rounded-md ring-1 ring-gray-400 dark:ring-gray-700 text-gray-700 dark:text-gray-500"
|
||||
>
|
||||
<CircleAlert class="w-5 h-6 w-min-5 w-max-5 min-h-5 max-w-5" />
|
||||
<div class="text-xs text-wrap">
|
||||
{{ selectedService.info }}
|
||||
<a :href="selectedService.link" target="_blank" class="underline"
|
||||
>here</a
|
||||
>.
|
||||
</div>
|
||||
</div>
|
||||
<!-- service provider fields -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div
|
||||
v-for="field in fields"
|
||||
:key="field.name"
|
||||
class="flex flex-col gap-1"
|
||||
>
|
||||
<FormControl
|
||||
v-model="state[field.name]"
|
||||
:label="field.label"
|
||||
:name="field.name"
|
||||
:type="field.type"
|
||||
:placeholder="field.placeholder"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div
|
||||
v-for="field in incomingOutgoingFields"
|
||||
:key="field.name"
|
||||
class="flex flex-col gap-1"
|
||||
>
|
||||
<FormControl
|
||||
v-model="state[field.name]"
|
||||
:label="field.label"
|
||||
:name="field.name"
|
||||
:type="field.type"
|
||||
/>
|
||||
<p class="text-gray-500 text-p-sm">{{ field.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<ErrorMessage v-if="error" class="ml-1" :message="error" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- action button -->
|
||||
<div v-if="selectedService" class="flex justify-between mt-auto">
|
||||
<Button
|
||||
label="Back"
|
||||
theme="gray"
|
||||
variant="outline"
|
||||
:disabled="addEmailRes.loading"
|
||||
@click="emit('update:step', 'email-list')"
|
||||
/>
|
||||
<Button
|
||||
label="Create"
|
||||
variant="solid"
|
||||
:loading="addEmailRes.loading"
|
||||
@click="createEmailAccount"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { createResource } from 'frappe-ui'
|
||||
import CircleAlert from '~icons/lucide/circle-alert'
|
||||
import { createToast } from '@/utils'
|
||||
import {
|
||||
customProviderFields,
|
||||
popularProviderFields,
|
||||
services,
|
||||
validateInputs,
|
||||
incomingOutgoingFields,
|
||||
} from './emailConfig'
|
||||
import EmailProviderIcon from './EmailProviderIcon.vue'
|
||||
|
||||
const emit = defineEmits()
|
||||
|
||||
const state = reactive({
|
||||
service: '',
|
||||
email_account_name: '',
|
||||
email_id: '',
|
||||
password: '',
|
||||
api_key: '',
|
||||
api_secret: '',
|
||||
frappe_mail_site: '',
|
||||
enable_incoming: false,
|
||||
enable_outgoing: false,
|
||||
default_incoming: false,
|
||||
default_outgoing: false,
|
||||
})
|
||||
|
||||
const selectedService = ref(null)
|
||||
const fields = computed(() =>
|
||||
selectedService.value.custom ? customProviderFields : popularProviderFields,
|
||||
)
|
||||
|
||||
function handleSelect(service) {
|
||||
selectedService.value = service
|
||||
state.service = service.name
|
||||
}
|
||||
|
||||
const addEmailRes = createResource({
|
||||
url: 'crm.api.settings.create_email_account',
|
||||
makeParams: (val) => {
|
||||
return {
|
||||
...val,
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
createToast({
|
||||
title: __('Email account created successfully'),
|
||||
icon: 'check',
|
||||
iconClasses: 'text-green-600',
|
||||
})
|
||||
emit('update:step', 'email-list')
|
||||
},
|
||||
onError: () => {
|
||||
error.value = __('Failed to create email account, Invalid credentials')
|
||||
},
|
||||
})
|
||||
|
||||
const error = ref()
|
||||
function createEmailAccount() {
|
||||
error.value = validateInputs(state, selectedService.value.custom)
|
||||
if (error.value) return
|
||||
|
||||
addEmailRes.submit({ data: state })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
27
frontend/src/components/Settings/EmailConfig.vue
Normal file
@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div class="flex-1 p-8">
|
||||
<div v-if="step === 'email-add'" class="h-full">
|
||||
<EmailAdd @update:step="updateStep" />
|
||||
</div>
|
||||
<div v-else-if="step === 'email-list'" class="h-full">
|
||||
<EmailAccountList @update:step="updateStep" />
|
||||
</div>
|
||||
<div v-else-if="step === 'email-edit'" class="h-full">
|
||||
<EmailEdit :account-data="accountData" @update:step="updateStep" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import EmailAdd from "./EmailAdd.vue";
|
||||
import EmailAccountList from "./EmailAccountList.vue";
|
||||
import EmailEdit from "./EmailEdit.vue";
|
||||
|
||||
const step = ref("email-list");
|
||||
const accountData = ref(null);
|
||||
function updateStep(newStep, data) {
|
||||
step.value = newStep;
|
||||
accountData.value = data;
|
||||
}
|
||||
</script>
|
||||
224
frontend/src/components/Settings/EmailEdit.vue
Normal file
@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full gap-4">
|
||||
<!-- title and desc -->
|
||||
<div role="heading" aria-level="1" class="flex justify-between gap-1">
|
||||
<h2 class="text-xl font-semibold text-ink-gray-9">
|
||||
{{ __('Edit Email') }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="w-fit">
|
||||
<EmailProviderIcon
|
||||
:logo="emailIcon[accountData.service]"
|
||||
:service-name="accountData.service"
|
||||
/>
|
||||
</div>
|
||||
<!-- banner for setting up email account -->
|
||||
<div
|
||||
class="flex items-center gap-2 p-2 rounded-md ring-1 ring-gray-400 dark:ring-gray-700"
|
||||
>
|
||||
<CircleAlert
|
||||
class="size-6 text-gray-500 w-min-5 w-max-5 min-h-5 max-w-5"
|
||||
/>
|
||||
<div class="text-xs text-gray-700 dark:text-gray-500 text-wrap">
|
||||
{{ info.description }}
|
||||
<a :href="info.link" target="_blank" class="underline">{{
|
||||
__('here')
|
||||
}}</a>
|
||||
.
|
||||
</div>
|
||||
</div>
|
||||
<!-- fields -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div
|
||||
v-for="field in fields"
|
||||
:key="field.name"
|
||||
class="flex flex-col gap-1"
|
||||
>
|
||||
<FormControl
|
||||
v-model="state[field.name]"
|
||||
:label="field.label"
|
||||
:name="field.name"
|
||||
:type="field.type"
|
||||
:placeholder="field.placeholder"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div
|
||||
v-for="field in incomingOutgoingFields"
|
||||
:key="field.name"
|
||||
class="flex flex-col gap-1"
|
||||
>
|
||||
<FormControl
|
||||
v-model="state[field.name]"
|
||||
:label="field.label"
|
||||
:name="field.name"
|
||||
:type="field.type"
|
||||
/>
|
||||
<p class="text-gray-500 text-p-sm">{{ field.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<ErrorMessage v-if="error" class="ml-1" :message="error" />
|
||||
</div>
|
||||
<!-- action buttons -->
|
||||
<div class="flex justify-between mt-auto">
|
||||
<Button
|
||||
:label="__('Back')"
|
||||
theme="gray"
|
||||
variant="outline"
|
||||
:disabled="loading"
|
||||
@click="emit('update:step', 'email-list')"
|
||||
/>
|
||||
<Button
|
||||
:label="__('Update Account')"
|
||||
variant="solid"
|
||||
@click="updateAccount"
|
||||
:loading="loading"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { call } from 'frappe-ui'
|
||||
import EmailProviderIcon from './EmailProviderIcon.vue'
|
||||
import {
|
||||
emailIcon,
|
||||
services,
|
||||
popularProviderFields,
|
||||
customProviderFields,
|
||||
validateInputs,
|
||||
incomingOutgoingFields,
|
||||
} from './emailConfig'
|
||||
import { createToast } from '@/utils'
|
||||
import CircleAlert from '~icons/lucide/circle-alert'
|
||||
|
||||
const props = defineProps({
|
||||
accountData: null,
|
||||
})
|
||||
|
||||
const emit = defineEmits()
|
||||
|
||||
const state = reactive({
|
||||
email_account_name: props.accountData.email_account_name || '',
|
||||
service: props.accountData.service || '',
|
||||
email_id: props.accountData.email_id || '',
|
||||
api_key: props.accountData?.api_key || null,
|
||||
api_secret: props.accountData?.api_secret || null,
|
||||
password: props.accountData?.password || null,
|
||||
frappe_mail_site: props.accountData?.frappe_mail_site || '',
|
||||
enable_incoming: props.accountData.enable_incoming || false,
|
||||
enable_outgoing: props.accountData.enable_outgoing || false,
|
||||
default_outgoing: props.accountData.default_outgoing || false,
|
||||
default_incoming: props.accountData.default_incoming || false,
|
||||
})
|
||||
|
||||
const info = {
|
||||
description: __('To know more about setting up email accounts, click'),
|
||||
link: 'https://docs.erpnext.com/docs/user/manual/en/email-account',
|
||||
}
|
||||
|
||||
const isCustomService = computed(() => {
|
||||
return services.find((s) => s.name === props.accountData.service).custom
|
||||
})
|
||||
|
||||
const fields = computed(() => {
|
||||
if (isCustomService.value) {
|
||||
return customProviderFields
|
||||
}
|
||||
return popularProviderFields
|
||||
})
|
||||
|
||||
const error = ref()
|
||||
const loading = ref(false)
|
||||
async function updateAccount() {
|
||||
error.value = validateInputs(state, isCustomService.value)
|
||||
if (error.value) return
|
||||
const old = { ...props.accountData }
|
||||
const updatedEmailAccount = { ...state }
|
||||
|
||||
const nameChanged =
|
||||
old.email_account_name !== updatedEmailAccount.email_account_name
|
||||
delete old.email_account_name
|
||||
delete updatedEmailAccount.email_account_name
|
||||
|
||||
const otherFieldsChanged = isDirty.value
|
||||
const values = updatedEmailAccount
|
||||
|
||||
if (!nameChanged && !otherFieldsChanged) {
|
||||
createToast({
|
||||
title: __('No changes made'),
|
||||
icon: 'info',
|
||||
iconClasses: 'text-blue-600',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (nameChanged) {
|
||||
try {
|
||||
loading.value = true
|
||||
await callRenameDoc()
|
||||
succesHandler()
|
||||
} catch (err) {
|
||||
errorHandler()
|
||||
}
|
||||
}
|
||||
if (otherFieldsChanged) {
|
||||
try {
|
||||
loading.value = true
|
||||
await callSetValue(values)
|
||||
succesHandler()
|
||||
} catch (err) {
|
||||
errorHandler()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isDirty = computed(() => {
|
||||
return (
|
||||
state.email_id !== props.accountData.email_id ||
|
||||
state.api_key !== props.accountData.api_key ||
|
||||
state.api_secret !== props.accountData.api_secret ||
|
||||
state.password !== props.accountData.password ||
|
||||
state.enable_incoming !== props.accountData.enable_incoming ||
|
||||
state.enable_outgoing !== props.accountData.enable_outgoing ||
|
||||
state.default_outgoing !== props.accountData.default_outgoing ||
|
||||
state.default_incoming !== props.accountData.default_incoming ||
|
||||
state.frappe_mail_site !== props.accountData.frappe_mail_site
|
||||
)
|
||||
})
|
||||
|
||||
async function callRenameDoc() {
|
||||
const d = await call('frappe.client.rename_doc', {
|
||||
doctype: 'Email Account',
|
||||
old_name: props.accountData.email_account_name,
|
||||
new_name: state.email_account_name,
|
||||
})
|
||||
return d
|
||||
}
|
||||
|
||||
async function callSetValue(values) {
|
||||
const d = await call('frappe.client.set_value', {
|
||||
doctype: 'Email Account',
|
||||
name: state.email_account_name,
|
||||
fieldname: values,
|
||||
})
|
||||
return d.name
|
||||
}
|
||||
|
||||
function succesHandler() {
|
||||
emit('update:step', 'email-list')
|
||||
createToast({
|
||||
title: __('Email account updated successfully'),
|
||||
icon: 'check',
|
||||
iconClasses: 'text-green-600',
|
||||
})
|
||||
}
|
||||
|
||||
function errorHandler() {
|
||||
loading.value = false
|
||||
error.value = __('Failed to update email account, Invalid credentials')
|
||||
}
|
||||
</script>
|
||||
33
frontend/src/components/Settings/EmailProviderIcon.vue
Normal file
@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-center w-8 h-8 bg-gray-100 cursor-pointer rounded-xl hover:bg-gray-200"
|
||||
:class="{ 'ring-2 ring-gray-500 dark:ring-gray-100': selected }"
|
||||
>
|
||||
<img :src="logo" class="w-4 h-4" />
|
||||
</div>
|
||||
<p
|
||||
v-if="serviceName"
|
||||
class="text-xs text-center text-gray-700 dark:text-gray-500 mt-2"
|
||||
>
|
||||
{{ serviceName }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
logo: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
serviceName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
selected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@ -6,8 +6,8 @@
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex h-[calc(100vh_-_8rem)]">
|
||||
<div class="flex w-52 shrink-0 flex-col bg-surface-gray-2 p-2">
|
||||
<h1 class="mb-3 px-2 pt-2 text-lg font-semibold text-ink-gray-9">
|
||||
<div class="flex flex-col p-2 w-52 shrink-0 bg-surface-gray-2">
|
||||
<h1 class="px-2 pt-2 mb-3 text-lg font-semibold text-ink-gray-9">
|
||||
{{ __('Settings') }}
|
||||
</h1>
|
||||
<div v-for="tab in tabs">
|
||||
@ -34,7 +34,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex relative flex-1 flex-col overflow-y-auto bg-surface-modal"
|
||||
class="relative flex flex-col flex-1 overflow-y-auto bg-surface-modal"
|
||||
>
|
||||
<Button
|
||||
class="absolute right-5 top-5"
|
||||
@ -53,12 +53,14 @@ import WhatsAppIcon from '@/components/Icons/WhatsAppIcon.vue'
|
||||
import ERPNextIcon from '@/components/Icons/ERPNextIcon.vue'
|
||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||
import InviteIcon from '@/components/Icons/InviteIcon.vue'
|
||||
import Email2Icon from '@/components/Icons/Email2Icon.vue'
|
||||
import GeneralSettings from '@/components/Settings/GeneralSettings.vue'
|
||||
import InviteMemberPage from '@/components/Settings/InviteMemberPage.vue'
|
||||
import ProfileSettings from '@/components/Settings/ProfileSettings.vue'
|
||||
import WhatsAppSettings from '@/components/Settings/WhatsAppSettings.vue'
|
||||
import ERPNextSettings from '@/components/Settings/ERPNextSettings.vue'
|
||||
import TelephonySettings from '@/components/Settings/TelephonySettings.vue'
|
||||
import EmailConfig from '@/components/Settings/EmailConfig.vue'
|
||||
import SidebarLink from '@/components/SidebarLink.vue'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import {
|
||||
@ -101,6 +103,12 @@ const tabs = computed(() => {
|
||||
component: markRaw(InviteMemberPage),
|
||||
condition: () => isManager(),
|
||||
},
|
||||
{
|
||||
label: __('Email Accounts'),
|
||||
icon: Email2Icon,
|
||||
component: markRaw(EmailConfig),
|
||||
condition: () => isManager(),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
186
frontend/src/components/Settings/emailConfig.js
Normal file
@ -0,0 +1,186 @@
|
||||
import { validateEmail } from '../../utils'
|
||||
|
||||
const LogoGmail = '/images/gmail.png'
|
||||
const LogoOutlook = '/images/outlook.png'
|
||||
const LogoSendgrid = '/images/sendgrid.png'
|
||||
const LogoSparkpost = '/images/sparkpost.webp'
|
||||
const LogoYahoo = '/images/yahoo.png'
|
||||
const LogoYandex = '/images/yandex.png'
|
||||
const LogoFrappeMail = '/images/frappe-mail.svg'
|
||||
|
||||
const fixedFields = [
|
||||
{
|
||||
label: __('Account Name'),
|
||||
name: 'email_account_name',
|
||||
type: 'text',
|
||||
placeholder: __('Support / Sales'),
|
||||
},
|
||||
{
|
||||
label: 'Email ID',
|
||||
name: 'email_id',
|
||||
type: 'email',
|
||||
placeholder: 'johndoe@example.com',
|
||||
},
|
||||
]
|
||||
|
||||
export const incomingOutgoingFields = [
|
||||
{
|
||||
label: __('Enable Incoming'),
|
||||
name: 'enable_incoming',
|
||||
type: 'checkbox',
|
||||
description: __(
|
||||
'If enabled, records can be created from the incoming emails on this account.',
|
||||
),
|
||||
},
|
||||
{
|
||||
label: __('Enable Outgoing'),
|
||||
name: 'enable_outgoing',
|
||||
type: 'checkbox',
|
||||
description: __(
|
||||
'If enabled, outgoing emails can be sent from this account.',
|
||||
),
|
||||
},
|
||||
{
|
||||
label: __('Default Incoming'),
|
||||
name: 'default_incoming',
|
||||
type: 'checkbox',
|
||||
description: __(
|
||||
'If enabled, all replies to your company (eg: replies@yourcomany.com) will come to this account. Note: Only one account can be default incoming.',
|
||||
),
|
||||
},
|
||||
{
|
||||
label: __('Default Outgoing'),
|
||||
name: 'default_outgoing',
|
||||
type: 'checkbox',
|
||||
description: __(
|
||||
'If enabled, all outgoing emails will be sent from this account. Note: Only one account can be default outgoing.',
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
export const popularProviderFields = [
|
||||
...fixedFields,
|
||||
{
|
||||
label: __('Password'),
|
||||
name: 'password',
|
||||
type: 'password',
|
||||
placeholder: '********',
|
||||
},
|
||||
]
|
||||
|
||||
export const customProviderFields = [
|
||||
...fixedFields,
|
||||
{
|
||||
label: 'Frappe Mail Site',
|
||||
name: 'frappe_mail_site',
|
||||
type: 'text',
|
||||
placeholder: 'https://frappemail.com',
|
||||
},
|
||||
{
|
||||
label: 'API Key',
|
||||
name: 'api_key',
|
||||
type: 'text',
|
||||
placeholder: '********',
|
||||
},
|
||||
{
|
||||
label: 'API Secret',
|
||||
name: 'api_secret',
|
||||
type: 'password',
|
||||
placeholder: '********',
|
||||
},
|
||||
]
|
||||
|
||||
export const services = [
|
||||
{
|
||||
name: 'GMail',
|
||||
icon: LogoGmail,
|
||||
info: __(`Setting up GMail requires you to enable two factor authentication
|
||||
and app specific passwords. Read more`),
|
||||
link: 'https://support.google.com/accounts/answer/185833',
|
||||
custom: false,
|
||||
},
|
||||
{
|
||||
name: 'Outlook',
|
||||
icon: LogoOutlook,
|
||||
info: __(`Setting up Outlook requires you to enable two factor authentication
|
||||
and app specific passwords. Read more`),
|
||||
link: 'https://support.microsoft.com/en-us/account-billing/how-to-get-and-use-app-passwords-5896ed9b-4263-e681-128a-a6f2979a7944',
|
||||
custom: false,
|
||||
},
|
||||
{
|
||||
name: 'Sendgrid',
|
||||
icon: LogoSendgrid,
|
||||
info: __(`Setting up Sendgrid requires you to enable two factor authentication
|
||||
and app specific passwords. Read more `),
|
||||
link: 'https://sendgrid.com/docs/ui/account-and-settings/two-factor-authentication/',
|
||||
custom: false,
|
||||
},
|
||||
{
|
||||
name: 'SparkPost',
|
||||
icon: LogoSparkpost,
|
||||
info: __(`Setting up SparkPost requires you to enable two factor authentication
|
||||
and app specific passwords. Read more `),
|
||||
link: 'https://support.sparkpost.com/docs/my-account-and-profile/enabling-two-factor-authentication',
|
||||
custom: false,
|
||||
},
|
||||
{
|
||||
name: 'Yahoo',
|
||||
icon: LogoYahoo,
|
||||
info: __(`Setting up Yahoo requires you to enable two factor authentication
|
||||
and app specific passwords. Read more `),
|
||||
link: 'https://help.yahoo.com/kb/SLN15241.html',
|
||||
custom: false,
|
||||
},
|
||||
{
|
||||
name: 'Yandex',
|
||||
icon: LogoYandex,
|
||||
info: __(`Setting up Yandex requires you to enable two factor authentication
|
||||
and app specific passwords. Read more `),
|
||||
link: 'https://yandex.com/support/id/authorization/app-passwords.html',
|
||||
custom: false,
|
||||
},
|
||||
{
|
||||
name: 'Frappe Mail',
|
||||
icon: LogoFrappeMail,
|
||||
info: __(
|
||||
`Setting up Frappe Mail requires you to have an API key and API Secret of your email account. Read more `,
|
||||
),
|
||||
link: 'https://github.com/frappe/mail',
|
||||
custom: true,
|
||||
},
|
||||
]
|
||||
|
||||
export const emailIcon = {
|
||||
GMail: LogoGmail,
|
||||
Outlook: LogoOutlook,
|
||||
Sendgrid: LogoSendgrid,
|
||||
SparkPost: LogoSparkpost,
|
||||
Yahoo: LogoYahoo,
|
||||
Yandex: LogoYandex,
|
||||
'Frappe Mail': LogoFrappeMail,
|
||||
}
|
||||
|
||||
export function validateInputs(state, isCustom) {
|
||||
if (!state.email_account_name) {
|
||||
return __('Account name is required')
|
||||
}
|
||||
if (!state.email_id) {
|
||||
return __('Email ID is required')
|
||||
}
|
||||
const validEmail = validateEmail(state.email_id)
|
||||
if (!validEmail) {
|
||||
return __('Invalid email ID')
|
||||
}
|
||||
if (!isCustom && !state.password) {
|
||||
return __('Password is required')
|
||||
}
|
||||
if (isCustom) {
|
||||
if (!state.api_key) {
|
||||
return __('API Key is required')
|
||||
}
|
||||
if (!state.api_secret) {
|
||||
return
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
@ -545,6 +545,11 @@ function reload() {
|
||||
const showExportDialog = ref(false)
|
||||
const export_type = ref('Excel')
|
||||
const export_all = ref(false)
|
||||
const selectedRows = ref([])
|
||||
|
||||
function updateSelections(selections) {
|
||||
selectedRows.value = Array.from(selections)
|
||||
}
|
||||
|
||||
async function exportRows() {
|
||||
let fields = JSON.stringify(list.value.data.columns.map((f) => f.key))
|
||||
@ -560,7 +565,15 @@ async function exportRows() {
|
||||
page_length = list.value.data.total_count
|
||||
}
|
||||
|
||||
window.location.href = `/api/method/frappe.desk.reportview.export_query?file_format_type=${export_type.value}&title=${props.doctype}&doctype=${props.doctype}&fields=${fields}&filters=${filters}&order_by=${order_by}&page_length=${page_length}&start=0&view=Report&with_comment_count=1`
|
||||
let url = `/api/method/frappe.desk.reportview.export_query?file_format_type=${export_type.value}&title=${props.doctype}&doctype=${props.doctype}&fields=${fields}&filters=${filters}&order_by=${order_by}&page_length=${page_length}&start=0&view=Report&with_comment_count=1`
|
||||
|
||||
// Add selected items parameter if rows are selected
|
||||
if (selectedRows.value?.length && !export_all.value) {
|
||||
url += `&selected_items=${JSON.stringify(selectedRows.value)}`
|
||||
}
|
||||
|
||||
window.location.href = url
|
||||
|
||||
showExportDialog.value = false
|
||||
export_all.value = false
|
||||
export_type.value = 'Excel'
|
||||
@ -1336,6 +1349,7 @@ defineExpose({
|
||||
viewActions,
|
||||
viewsDropdownOptions,
|
||||
currentView,
|
||||
updateSelections,
|
||||
})
|
||||
|
||||
// Watchers
|
||||
|
||||
@ -41,6 +41,9 @@
|
||||
@applyFilter="(data) => viewControls.applyFilter(data)"
|
||||
@applyLikeFilter="(data) => viewControls.applyLikeFilter(data)"
|
||||
@likeDoc="(data) => viewControls.likeDoc(data)"
|
||||
@selectionsChanged="
|
||||
(selections) => viewControls.updateSelections(selections)
|
||||
"
|
||||
/>
|
||||
<div
|
||||
v-else-if="callLogs.data"
|
||||
|
||||
@ -168,7 +168,11 @@
|
||||
</template>
|
||||
</Tabs>
|
||||
</div>
|
||||
<ErrorPage v-else :errorTitle="errorTitle" :errorMessage="errorMessage" />
|
||||
<ErrorPage
|
||||
v-else-if="errorTitle"
|
||||
:errorTitle="errorTitle"
|
||||
:errorMessage="errorMessage"
|
||||
/>
|
||||
<AddressModal v-model="showAddressModal" v-model:address="_address" />
|
||||
</template>
|
||||
|
||||
|
||||
@ -44,6 +44,9 @@
|
||||
@applyFilter="(data) => viewControls.applyFilter(data)"
|
||||
@applyLikeFilter="(data) => viewControls.applyLikeFilter(data)"
|
||||
@likeDoc="(data) => viewControls.likeDoc(data)"
|
||||
@selectionsChanged="
|
||||
(selections) => viewControls.updateSelections(selections)
|
||||
"
|
||||
/>
|
||||
<div
|
||||
v-else-if="contacts.data"
|
||||
|
||||
@ -267,7 +267,11 @@
|
||||
</div>
|
||||
</Resizer>
|
||||
</div>
|
||||
<ErrorPage v-else :errorTitle="errorTitle" :errorMessage="errorMessage" />
|
||||
<ErrorPage
|
||||
v-else-if="errorTitle"
|
||||
:errorTitle="errorTitle"
|
||||
:errorMessage="errorMessage"
|
||||
/>
|
||||
<OrganizationModal
|
||||
v-model="showOrganizationModal"
|
||||
v-model:organization="_organization"
|
||||
|
||||
@ -223,6 +223,9 @@
|
||||
@applyFilter="(data) => viewControls.applyFilter(data)"
|
||||
@applyLikeFilter="(data) => viewControls.applyLikeFilter(data)"
|
||||
@likeDoc="(data) => viewControls.likeDoc(data)"
|
||||
@selectionsChanged="
|
||||
(selections) => viewControls.updateSelections(selections)
|
||||
"
|
||||
/>
|
||||
<div v-else-if="deals.data" class="flex h-full items-center justify-center">
|
||||
<div
|
||||
|
||||
@ -45,6 +45,9 @@
|
||||
@applyFilter="(data) => viewControls.applyFilter(data)"
|
||||
@applyLikeFilter="(data) => viewControls.applyLikeFilter(data)"
|
||||
@likeDoc="(data) => viewControls.likeDoc(data)"
|
||||
@selectionsChanged="
|
||||
(selections) => viewControls.updateSelections(selections)
|
||||
"
|
||||
/>
|
||||
<div
|
||||
v-else-if="emailTemplates.data"
|
||||
|
||||
@ -191,7 +191,11 @@
|
||||
</div>
|
||||
</Resizer>
|
||||
</div>
|
||||
<ErrorPage v-else :errorTitle="errorTitle" :errorMessage="errorMessage" />
|
||||
<ErrorPage
|
||||
v-else-if="errorTitle"
|
||||
:errorTitle="errorTitle"
|
||||
:errorMessage="errorMessage"
|
||||
/>
|
||||
<Dialog
|
||||
v-model="showConvertToDealModal"
|
||||
:options="{
|
||||
|
||||
@ -249,6 +249,9 @@
|
||||
@applyFilter="(data) => viewControls.applyFilter(data)"
|
||||
@applyLikeFilter="(data) => viewControls.applyLikeFilter(data)"
|
||||
@likeDoc="(data) => viewControls.likeDoc(data)"
|
||||
@selectionsChanged="
|
||||
(selections) => viewControls.updateSelections(selections)
|
||||
"
|
||||
/>
|
||||
<div v-else-if="leads.data" class="flex h-full items-center justify-center">
|
||||
<div
|
||||
|
||||
@ -160,7 +160,11 @@
|
||||
</template>
|
||||
</Tabs>
|
||||
</div>
|
||||
<ErrorPage v-else :errorTitle="errorTitle" :errorMessage="errorMessage" />
|
||||
<ErrorPage
|
||||
v-else-if="errorTitle"
|
||||
:errorTitle="errorTitle"
|
||||
:errorMessage="errorMessage"
|
||||
/>
|
||||
<QuickEntryModal
|
||||
v-if="showQuickEntryModal"
|
||||
v-model="showQuickEntryModal"
|
||||
|
||||
@ -44,6 +44,9 @@
|
||||
@applyFilter="(data) => viewControls.applyFilter(data)"
|
||||
@applyLikeFilter="(data) => viewControls.applyLikeFilter(data)"
|
||||
@likeDoc="(data) => viewControls.likeDoc(data)"
|
||||
@selectionsChanged="
|
||||
(selections) => viewControls.updateSelections(selections)
|
||||
"
|
||||
/>
|
||||
<div
|
||||
v-else-if="organizations.data"
|
||||
|
||||
@ -172,6 +172,9 @@
|
||||
@applyFilter="(data) => viewControls.applyFilter(data)"
|
||||
@applyLikeFilter="(data) => viewControls.applyLikeFilter(data)"
|
||||
@likeDoc="(data) => viewControls.likeDoc(data)"
|
||||
@selectionsChanged="
|
||||
(selections) => viewControls.updateSelections(selections)
|
||||
"
|
||||
/>
|
||||
<div v-else-if="tasks.data" class="flex h-full items-center justify-center">
|
||||
<div
|
||||
|
||||
13
frontend/src/types.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export interface EmailAccount {
|
||||
email_account_name: string
|
||||
email_id: string
|
||||
service: string
|
||||
api_key?: string
|
||||
api_secret?: string
|
||||
password?: string
|
||||
frappe_mail_site?: string
|
||||
enable_outgoing?: boolean
|
||||
enable_incoming?: boolean
|
||||
default_outgoing?: boolean
|
||||
default_incoming?: boolean
|
||||
}
|
||||