fix: dark mode email account css
(cherry picked from commit 9c45877999234e72fed113639b767fb7594b66f6) # Conflicts: # frontend/src/components/Settings/EmailAccountCard.vue # frontend/src/components/Settings/EmailAccountList.vue # frontend/src/components/Settings/EmailAdd.vue # frontend/src/components/Settings/EmailEdit.vue # frontend/src/components/Settings/EmailProviderIcon.vue
This commit is contained in:
parent
4e3a85e03b
commit
7c4a697b9f
50
frontend/src/components/Settings/EmailAccountCard.vue
Normal file
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="badgeTitleColor" :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 badgeTitleColor = 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
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>
|
||||||
161
frontend/src/components/Settings/EmailAdd.vue
Normal file
161
frontend/src/components/Settings/EmailAdd.vue
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
<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>
|
||||||
220
frontend/src/components/Settings/EmailEdit.vue
Normal file
220
frontend/src/components/Settings/EmailEdit.vue
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
<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
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>
|
||||||
Loading…
x
Reference in New Issue
Block a user