Merge pull request #754 from frappe/mergify/bp/main-hotfix/pr-753

fix: dark mode email account css (backport #753)
This commit is contained in:
Pratik Badhe 2025-04-22 14:34:04 +05:30 committed by GitHub
commit 1f1090c2e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 259 additions and 173 deletions

View File

@ -1,55 +1,50 @@
<template> <template>
<div class="flex items-center justify-between p-1 border-b border-gray-200 cursor-pointer"> <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 --> <!-- avatar and name -->
<div class="flex items-center justify-between gap-2"> <div class="flex items-center justify-between gap-2">
<EmailProviderIcon :logo="emailIcon[emailAccount.service]" /> <EmailProviderIcon :logo="emailIcon[emailAccount.service]" />
<div> <div>
<p class="text-sm font-semibold text-gray-700"> <p class="text-sm font-semibold text-ink-gray-9">
{{ emailAccount.email_account_name }} {{ emailAccount.email_account_name }}
</p> </p>
<div class="text-sm text-gray-500">{{ emailAccount.email_id }}</div> <div class="text-sm text-gray-500">{{ emailAccount.email_id }}</div>
</div> </div>
</div> </div>
<div> <div>
<Badge variant="subtle" :label="badgeTitleColor[0]" :theme="badgeTitleColor[1]" /> <Badge variant="subtle" :label="badgeTitleColor" :theme="gray" />
</div> </div>
<!-- email id --> <!-- email id -->
</div> </div>
</template> </template>
<script setup> <script setup>
import { emailIcon } from "./emailConfig"; import { emailIcon } from './emailConfig'
import EmailProviderIcon from "./EmailProviderIcon.vue"; import EmailProviderIcon from './EmailProviderIcon.vue'
import { computed } from "vue"; import { computed } from 'vue'
const props = defineProps({ const props = defineProps({
emailAccount: { emailAccount: {
type: Object, type: Object,
required: true required: true,
} },
}); })
const badgeTitleColor = computed(() => { const badgeTitleColor = computed(() => {
if ( if (
props.emailAccount.default_incoming && props.emailAccount.default_incoming &&
props.emailAccount.default_outgoing props.emailAccount.default_outgoing
) { ) {
const color = return 'Default Sending and Inbox'
props.emailAccount.enable_incoming && props.emailAccount.enable_outgoing
? "blue"
: "gray";
return ["Default Sending and Inbox", color];
} else if (props.emailAccount.default_incoming) { } else if (props.emailAccount.default_incoming) {
const color = props.emailAccount.enable_incoming ? "blue" : "gray"; return 'Default Inbox'
return ["Default Inbox", color];
} else if (props.emailAccount.default_outgoing) { } else if (props.emailAccount.default_outgoing) {
const color = props.emailAccount.enable_outgoing ? "blue" : "gray"; return 'Default Sending'
return ["Default Sending", color];
} else { } else {
const color = props.emailAccount.enable_incoming ? "blue" : "gray"; return 'Inbox'
return ["Inbox", color];
} }
}); })
</script> </script>
<style scoped></style> <style scoped></style>

View File

@ -1,18 +1,32 @@
<template> <template>
<div> <div>
<!-- header --> <!-- header -->
<div class="flex items-center justify-between"> <div class="flex items-center justify-between text-ink-gray-9">
<h1 class="text-xl font-semibold">Email Accounts</h1> <h2 class="flex gap-2 text-xl font-semibold leading-none h-5">
<Button label="Add Account" theme="gray" variant="solid" @click="emit('update:step', 'email-add')" class="mr-8"> Email Accounts
</h2>
<Button
label="Add Account"
theme="gray"
variant="solid"
@click="emit('update:step', 'email-add')"
class="mr-8"
>
<template #prefix> <template #prefix>
<LucidePlus class="w-4 h-4" /> <LucidePlus class="w-4 h-4" />
</template> </template>
</Button> </Button>
</div> </div>
<!-- list accounts --> <!-- list accounts -->
<div v-if="!emailAccounts.loading && Boolean(emailAccounts.data?.length)" class="mt-4"> <div
v-if="!emailAccounts.loading && Boolean(emailAccounts.data?.length)"
class="mt-4"
>
<div v-for="emailAccount in emailAccounts.data" :key="emailAccount.name"> <div v-for="emailAccount in emailAccounts.data" :key="emailAccount.name">
<EmailAccountCard :emailAccount="emailAccount" @click="emit('update:step', 'email-edit', emailAccount)" /> <EmailAccountCard
:emailAccount="emailAccount"
@click="emit('update:step', 'email-edit', emailAccount)"
/>
</div> </div>
</div> </div>
<!-- fallback if no email accounts --> <!-- fallback if no email accounts -->
@ -23,28 +37,28 @@
</template> </template>
<script setup> <script setup>
import { createListResource } from "frappe-ui"; import { createListResource } from 'frappe-ui'
import EmailAccountCard from "./EmailAccountCard.vue"; import EmailAccountCard from './EmailAccountCard.vue'
const emit = defineEmits(["update:step"]); const emit = defineEmits(['update:step'])
const emailAccounts = createListResource({ const emailAccounts = createListResource({
doctype: "Email Account", doctype: 'Email Account',
cache: true, cache: true,
fields: ["*"], fields: ['*'],
filters: { filters: {
email_id: ["Not Like", "%example%"], email_id: ['Not Like', '%example%'],
}, },
pageLength: 10, pageLength: 10,
auto: true, auto: true,
onSuccess: (accounts) => { onSuccess: (accounts) => {
// convert 0 to false to handle boolean fields // convert 0 to false to handle boolean fields
accounts.forEach((account) => { accounts.forEach((account) => {
account.enable_incoming = Boolean(account.enable_incoming); account.enable_incoming = Boolean(account.enable_incoming)
account.enable_outgoing = Boolean(account.enable_outgoing); account.enable_outgoing = Boolean(account.enable_outgoing)
account.default_incoming = Boolean(account.default_incoming); account.default_incoming = Boolean(account.default_incoming)
account.default_outgoing = Boolean(account.default_outgoing); account.default_outgoing = Boolean(account.default_outgoing)
}); })
}, },
}); })
</script> </script>

View File

@ -2,39 +2,68 @@
<div class="flex flex-col h-full gap-4"> <div class="flex flex-col h-full gap-4">
<!-- title and desc --> <!-- title and desc -->
<div role="heading" aria-level="1" class="flex flex-col gap-1"> <div role="heading" aria-level="1" class="flex flex-col gap-1">
<h5 class="text-xl font-semibold">Setup Email</h5> <h2 class="text-xl font-semibold text-ink-gray-9">Setup Email</h2>
<p class="text-sm text-gray-600"> <p class="text-sm text-gray-600">
Choose the email service provider you want to configure. Choose the email service provider you want to configure.
</p> </p>
</div> </div>
<!-- email service provider selection --> <!-- email service provider selection -->
<div class="flex flex-wrap items-center gap-4"> <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 min-w-3" <div
@click="handleSelect(s)"> v-for="s in services"
<EmailProviderIcon :service-name="s.name" :logo="s.icon" :selected="selectedService?.name === s?.name" /> :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> </div>
<div v-if="selectedService" class="flex flex-col gap-4"> <div v-if="selectedService" class="flex flex-col gap-4">
<!-- email service provider info --> <!-- email service provider info -->
<div class="flex items-center gap-2 p-2 rounded-md ring-1 ring-gray-200"> <div
<CircleAlert class="w-5 h-6 text-blue-500 w-min-5 w-max-5 min-h-5 max-w-5" /> 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"
<div class="text-xs text-gray-700 text-wrap"> >
<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 }} {{ selectedService.info }}
<a :href="selectedService.link" target="_blank" class="text-blue-500 underline">here</a> <a :href="selectedService.link" target="_blank" class="underline"
. >here</a
>.
</div> </div>
</div> </div>
<!-- service provider fields --> <!-- service provider fields -->
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="grid grid-cols-1 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"> <div
<FormControl v-model="state[field.name]" :label="field.label" :name="field.name" :type="field.type" v-for="field in fields"
:placeholder="field.placeholder" /> :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> </div>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<div v-for="field in incomingOutgoingFields" :key="field.name" class="flex flex-col gap-1"> <div
<FormControl v-model="state[field.name]" :label="field.label" :name="field.name" :type="field.type" /> 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> <p class="text-gray-500 text-p-sm">{{ field.description }}</p>
</div> </div>
</div> </div>
@ -43,79 +72,89 @@
</div> </div>
<!-- action button --> <!-- action button -->
<div v-if="selectedService" class="flex justify-between mt-auto"> <div v-if="selectedService" class="flex justify-between mt-auto">
<Button label="Back" theme="gray" variant="outline" :disabled="addEmailRes.loading" <Button
@click="emit('update:step', 'email-list')" /> label="Back"
<Button label="Create" variant="solid" :loading="addEmailRes.loading" @click="createEmailAccount" /> 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>
</div> </div>
</template> </template>
<script setup> <script setup>
import { computed, reactive, ref } from "vue"; import { computed, reactive, ref } from 'vue'
import { createResource } from "frappe-ui"; import { createResource } from 'frappe-ui'
import CircleAlert from "~icons/lucide/circle-alert"; import CircleAlert from '~icons/lucide/circle-alert'
import { createToast } from "@/utils"; import { createToast } from '@/utils'
import { import {
customProviderFields, customProviderFields,
popularProviderFields, popularProviderFields,
services, services,
validateInputs, validateInputs,
incomingOutgoingFields, incomingOutgoingFields,
} from "./emailConfig"; } from './emailConfig'
import EmailProviderIcon from "./EmailProviderIcon.vue"; import EmailProviderIcon from './EmailProviderIcon.vue'
const emit = defineEmits(); const emit = defineEmits()
const state = reactive({ const state = reactive({
service: "", service: '',
email_account_name: "", email_account_name: '',
email_id: "", email_id: '',
password: "", password: '',
api_key: "", api_key: '',
api_secret: "", api_secret: '',
frappe_mail_site: "", frappe_mail_site: '',
enable_incoming: false, enable_incoming: false,
enable_outgoing: false, enable_outgoing: false,
default_incoming: false, default_incoming: false,
default_outgoing: false, default_outgoing: false,
}); })
const selectedService = ref(null); const selectedService = ref(null)
const fields = computed(() => const fields = computed(() =>
selectedService.value.custom ? customProviderFields : popularProviderFields selectedService.value.custom ? customProviderFields : popularProviderFields,
); )
function handleSelect(service) { function handleSelect(service) {
selectedService.value = service; selectedService.value = service
state.service = service.name; state.service = service.name
} }
const addEmailRes = createResource({ const addEmailRes = createResource({
url: "crm.api.settings.create_email_account", url: 'crm.api.settings.create_email_account',
makeParams: (val) => { makeParams: (val) => {
return { return {
...val, ...val,
}; }
}, },
onSuccess: () => { onSuccess: () => {
createToast({ createToast({
title: "Email account created successfully", title: 'Email account created successfully',
icon: "check", icon: 'check',
iconClasses: "text-green-600", iconClasses: 'text-green-600',
}); })
emit("update:step", "email-list"); emit('update:step', 'email-list')
}, },
onError: () => { onError: () => {
error.value = "Failed to create email account, Invalid credentials"; error.value = 'Failed to create email account, Invalid credentials'
}, },
}); })
const error = ref(); const error = ref()
function createEmailAccount() { function createEmailAccount() {
error.value = validateInputs(state, selectedService.value.custom); error.value = validateInputs(state, selectedService.value.custom)
if (error.value) return; if (error.value) return
addEmailRes.submit({ data: state }); addEmailRes.submit({ data: state })
} }
</script> </script>

View File

@ -2,31 +2,56 @@
<div class="flex flex-col h-full gap-4"> <div class="flex flex-col h-full gap-4">
<!-- title and desc --> <!-- title and desc -->
<div role="heading" aria-level="1" class="flex justify-between gap-1"> <div role="heading" aria-level="1" class="flex justify-between gap-1">
<h5 class="text-lg font-semibold">Edit Email</h5> <h2 class="text-xl font-semibold text-ink-gray-9">Edit Email</h2>
</div> </div>
<div class="w-fit"> <div class="w-fit">
<EmailProviderIcon :logo="emailIcon[accountData.service]" :service-name="accountData.service" /> <EmailProviderIcon
:logo="emailIcon[accountData.service]"
:service-name="accountData.service"
/>
</div> </div>
<!-- banner for setting up email account --> <!-- banner for setting up email account -->
<div class="flex items-center gap-2 p-2 rounded-md ring-1 ring-gray-200"> <div
<CircleAlert class="w-5 h-6 text-blue-500 w-min-5 w-max-5 min-h-5 max-w-5" /> class="flex items-center gap-2 p-2 rounded-md ring-1 ring-gray-400 dark:ring-gray-700"
<div class="text-xs text-gray-700 text-wrap"> >
<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 }} {{ info.description }}
<a :href="info.link" target="_blank" class="text-blue-500 underline">here</a> <a :href="info.link" target="_blank" class="underline">here</a>
. .
</div> </div>
</div> </div>
<!-- fields --> <!-- fields -->
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="grid grid-cols-1 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"> <div
<FormControl v-model="state[field.name]" :label="field.label" :name="field.name" :type="field.type" v-for="field in fields"
:placeholder="field.placeholder" /> :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> </div>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<div v-for="field in incomingOutgoingFields" :key="field.name" class="flex flex-col gap-1"> <div
<FormControl v-model="state[field.name]" :label="field.label" :name="field.name" :type="field.type" /> 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> <p class="text-gray-500 text-p-sm">{{ field.description }}</p>
</div> </div>
</div> </div>
@ -34,17 +59,27 @@
</div> </div>
<!-- action buttons --> <!-- action buttons -->
<div class="flex justify-between mt-auto"> <div class="flex justify-between mt-auto">
<Button label="Back" theme="gray" variant="outline" :disabled="loading" <Button
@click="emit('update:step', 'email-list')" /> label="Back"
<Button label="Update Account" variant="solid" @click="updateAccount" :loading="loading" /> theme="gray"
variant="outline"
:disabled="loading"
@click="emit('update:step', 'email-list')"
/>
<Button
label="Update Account"
variant="solid"
@click="updateAccount"
:loading="loading"
/>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { computed, reactive, ref } from "vue"; import { computed, reactive, ref } from 'vue'
import { call } from "frappe-ui"; import { call } from 'frappe-ui'
import EmailProviderIcon from "./EmailProviderIcon.vue"; import EmailProviderIcon from './EmailProviderIcon.vue'
import { import {
emailIcon, emailIcon,
services, services,
@ -52,88 +87,87 @@ import {
customProviderFields, customProviderFields,
validateInputs, validateInputs,
incomingOutgoingFields, incomingOutgoingFields,
} from "./emailConfig"; } from './emailConfig'
import { createToast } from "@/utils"; import { createToast } from '@/utils'
import CircleAlert from "~icons/lucide/circle-alert"; import CircleAlert from '~icons/lucide/circle-alert'
const props = defineProps({ const props = defineProps({
accountData: null, accountData: null,
}) })
const emit = defineEmits(); const emit = defineEmits()
const state = reactive({ const state = reactive({
email_account_name: props.accountData.email_account_name || "", email_account_name: props.accountData.email_account_name || '',
service: props.accountData.service || "", service: props.accountData.service || '',
email_id: props.accountData.email_id || "", email_id: props.accountData.email_id || '',
api_key: props.accountData?.api_key || null, api_key: props.accountData?.api_key || null,
api_secret: props.accountData?.api_secret || null, api_secret: props.accountData?.api_secret || null,
password: props.accountData?.password || null, password: props.accountData?.password || null,
frappe_mail_site: props.accountData?.frappe_mail_site || "", frappe_mail_site: props.accountData?.frappe_mail_site || '',
enable_incoming: props.accountData.enable_incoming || false, enable_incoming: props.accountData.enable_incoming || false,
enable_outgoing: props.accountData.enable_outgoing || false, enable_outgoing: props.accountData.enable_outgoing || false,
default_outgoing: props.accountData.default_outgoing || false, default_outgoing: props.accountData.default_outgoing || false,
default_incoming: props.accountData.default_incoming || false, default_incoming: props.accountData.default_incoming || false,
}); })
const info = { const info = {
description: "To know more about setting up email accounts, click", description: 'To know more about setting up email accounts, click',
link: "https://docs.erpnext.com/docs/user/manual/en/email-account", link: 'https://docs.erpnext.com/docs/user/manual/en/email-account',
}; }
const isCustomService = computed(() => { const isCustomService = computed(() => {
return services.find((s) => s.name === props.accountData.service).custom; return services.find((s) => s.name === props.accountData.service).custom
}); })
const fields = computed(() => { const fields = computed(() => {
if (isCustomService.value) { if (isCustomService.value) {
return customProviderFields; return customProviderFields
} }
return popularProviderFields; return popularProviderFields
}); })
const error = ref(); const error = ref()
const loading = ref(false); const loading = ref(false)
async function updateAccount() { async function updateAccount() {
error.value = validateInputs(state, isCustomService.value); error.value = validateInputs(state, isCustomService.value)
if (error.value) return; if (error.value) return
const old = { ...props.accountData }; const old = { ...props.accountData }
const updatedEmailAccount = { ...state }; const updatedEmailAccount = { ...state }
const nameChanged = const nameChanged =
old.email_account_name !== updatedEmailAccount.email_account_name; old.email_account_name !== updatedEmailAccount.email_account_name
delete old.email_account_name; delete old.email_account_name
delete updatedEmailAccount.email_account_name; delete updatedEmailAccount.email_account_name
const otherFieldsChanged = isDirty.value; const otherFieldsChanged = isDirty.value
const values = updatedEmailAccount; const values = updatedEmailAccount
if (!nameChanged && !otherFieldsChanged) { if (!nameChanged && !otherFieldsChanged) {
createToast({ createToast({
title: "No changes made", title: 'No changes made',
icon: "info", icon: 'info',
iconClasses: "text-blue-600", iconClasses: 'text-blue-600',
}); })
return; return
} }
if (nameChanged) { if (nameChanged) {
try { try {
loading.value = true; loading.value = true
await callRenameDoc(); await callRenameDoc()
succesHandler(); succesHandler()
} catch (err) { } catch (err) {
errorHandler(); errorHandler()
} }
} }
if (otherFieldsChanged) { if (otherFieldsChanged) {
try { try {
loading.value = true; loading.value = true
await callSetValue(values); await callSetValue(values)
succesHandler(); succesHandler()
} catch (err) { } catch (err) {
errorHandler(); errorHandler()
} }
} }
} }
@ -149,38 +183,38 @@ const isDirty = computed(() => {
state.default_outgoing !== props.accountData.default_outgoing || state.default_outgoing !== props.accountData.default_outgoing ||
state.default_incoming !== props.accountData.default_incoming || state.default_incoming !== props.accountData.default_incoming ||
state.frappe_mail_site !== props.accountData.frappe_mail_site state.frappe_mail_site !== props.accountData.frappe_mail_site
); )
}); })
async function callRenameDoc() { async function callRenameDoc() {
const d = await call("frappe.client.rename_doc", { const d = await call('frappe.client.rename_doc', {
doctype: "Email Account", doctype: 'Email Account',
old_name: props.accountData.email_account_name, old_name: props.accountData.email_account_name,
new_name: state.email_account_name, new_name: state.email_account_name,
}); })
return d; return d
} }
async function callSetValue(values) { async function callSetValue(values) {
const d = await call("frappe.client.set_value", { const d = await call('frappe.client.set_value', {
doctype: "Email Account", doctype: 'Email Account',
name: state.email_account_name, name: state.email_account_name,
fieldname: values, fieldname: values,
}); })
return d.name; return d.name
} }
function succesHandler() { function succesHandler() {
emit("update:step", "email-list"); emit('update:step', 'email-list')
createToast({ createToast({
title: "Email account updated successfully", title: 'Email account updated successfully',
icon: "check", icon: 'check',
iconClasses: "text-green-600", iconClasses: 'text-green-600',
}); })
} }
function errorHandler() { function errorHandler() {
loading.value = false; loading.value = false
error.value = "Failed to update email account, Invalid credentials"; error.value = 'Failed to update email account, Invalid credentials'
} }
</script> </script>

View File

@ -1,28 +1,32 @@
<template> <template>
<div class="flex items-center justify-center w-8 h-8 bg-gray-100 cursor-pointer rounded-xl hover:bg-gray-200" <div
:class="{ 'ring-2 ring-blue-500': selected }"> 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" /> <img :src="logo" class="w-4 h-4" />
</div> </div>
<p v-if="serviceName" class="text-xs text-center text-gray-700"> <p
v-if="serviceName"
class="text-xs text-center text-gray-700 dark:text-gray-500 mt-2"
>
{{ serviceName }} {{ serviceName }}
</p> </p>
</template> </template>
<script setup> <script setup>
defineProps({ defineProps({
logo: { logo: {
type: String, type: String,
required: true required: true,
}, },
serviceName: { serviceName: {
type: String, type: String,
default: '' default: '',
}, },
selected: { selected: {
type: Boolean, type: Boolean,
default: false default: false,
} },
}) })
</script> </script>