refactor: split stores and use events
Signed-off-by: Sebastian <krupinski01@gmail.com>
This commit is contained in:
@@ -1,22 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onBeforeUnmount } from 'vue'
|
||||
import { useEditor, EditorContent } from '@tiptap/vue-3'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useEditor } from '@tiptap/vue-3'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import Link from '@tiptap/extension-link'
|
||||
import Underline from '@tiptap/extension-underline'
|
||||
import TextAlign from '@tiptap/extension-text-align'
|
||||
import Placeholder from '@tiptap/extension-placeholder'
|
||||
import { entityService } from '@MailManager/services'
|
||||
import type { EntityInterface } from '@MailManager/types/entity'
|
||||
import type { MessageInterface } from '@MailManager/types/message'
|
||||
import type { CollectionInterface } from '@MailManager/types/collection'
|
||||
import { MessageObject } from '@MailManager/models/message'
|
||||
import { EntityObject } from '@MailManager/models/entity'
|
||||
import type { CollectionObject } from '@MailManager/models'
|
||||
import { useMailStore } from '@/stores/mailStore'
|
||||
import ComposerToolbar from '@/components/composer/ComposerToolbar.vue'
|
||||
import ComposerRecipients from '@/components/composer/ComposerRecipients.vue'
|
||||
import ComposerEditor from '@/components/composer/ComposerEditor.vue'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
replyTo?: EntityInterface<MessageInterface> | null
|
||||
folder?: CollectionInterface | null
|
||||
mode: 'new' | 'reply' | 'forward'
|
||||
source?: EntityObject | null
|
||||
folder?: CollectionObject | null
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
@@ -27,6 +29,13 @@ const emit = defineEmits<{
|
||||
sent: []
|
||||
}>()
|
||||
|
||||
const mailStore = useMailStore()
|
||||
const {
|
||||
composerSending: sending,
|
||||
composerSaving: saving,
|
||||
composerLastSaved: lastSaved,
|
||||
} = storeToRefs(mailStore)
|
||||
|
||||
// State
|
||||
const to = ref<string[]>([])
|
||||
const cc = ref<string[]>([])
|
||||
@@ -34,10 +43,6 @@ const bcc = ref<string[]>([])
|
||||
const subject = ref('')
|
||||
const showCc = ref(false)
|
||||
const showBcc = ref(false)
|
||||
const sending = ref(false)
|
||||
const saving = ref(false)
|
||||
const lastSaved = ref<Date | null>(null)
|
||||
const draftId = ref<string | null>(null)
|
||||
|
||||
// Auto-save timer
|
||||
let autoSaveTimer: ReturnType<typeof setTimeout> | null = null
|
||||
@@ -65,25 +70,65 @@ const editor = useEditor({
|
||||
},
|
||||
})
|
||||
|
||||
// Initialize from reply-to message
|
||||
if (props.replyTo) {
|
||||
const replyMessage = new MessageObject(props.replyTo.properties)
|
||||
|
||||
const fromEmail = replyMessage.from?.address
|
||||
to.value = fromEmail ? [fromEmail] : []
|
||||
|
||||
const originalSubject = replyMessage.subject || ''
|
||||
subject.value = originalSubject.startsWith('Re:')
|
||||
? originalSubject
|
||||
: `Re: ${originalSubject}`
|
||||
|
||||
// Add quoted reply - prefer HTML content, fallback to text
|
||||
const originalBody = replyMessage.getHtmlContent() || replyMessage.getTextContent() || ''
|
||||
const senderName = replyMessage.from?.label || replyMessage.from?.address || 'Unknown'
|
||||
const quotedReply = `<p><br></p><p>On ${new Date(replyMessage.date || '').toLocaleString()}, ${senderName} wrote:</p><blockquote>${originalBody}</blockquote>`
|
||||
editor.value?.commands.setContent(quotedReply)
|
||||
function resetComposerFields() {
|
||||
to.value = []
|
||||
cc.value = []
|
||||
bcc.value = []
|
||||
subject.value = ''
|
||||
showCc.value = false
|
||||
showBcc.value = false
|
||||
editor.value?.commands.setContent('')
|
||||
}
|
||||
|
||||
function initializeComposerFromProps() {
|
||||
mailStore.resetComposerState()
|
||||
resetComposerFields()
|
||||
|
||||
if (!props.source) {
|
||||
return
|
||||
}
|
||||
|
||||
const sourceMessage = props.source.properties
|
||||
const originalSubject = sourceMessage.subject || ''
|
||||
const originalBody = sourceMessage.getHtmlContent() || sourceMessage.getTextContent() || ''
|
||||
const senderName = sourceMessage.from?.label || sourceMessage.from?.address || 'Unknown'
|
||||
const sentAt = sourceMessage.sent || props.source.created || ''
|
||||
const sentLabel = sentAt ? new Date(sentAt).toLocaleString() : 'an unknown time'
|
||||
|
||||
if (props.mode === 'reply') {
|
||||
const fromEmail = sourceMessage.replyTo?.[0]?.address || sourceMessage.from?.address
|
||||
to.value = fromEmail ? [fromEmail] : []
|
||||
subject.value = /^Re:/i.test(originalSubject)
|
||||
? originalSubject
|
||||
: `Re: ${originalSubject}`
|
||||
editor.value?.commands.setContent(
|
||||
`<p><br></p><p>On ${sentLabel}, ${senderName} wrote:</p><blockquote>${originalBody}</blockquote>`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (props.mode === 'forward') {
|
||||
subject.value = /^Fwd:/i.test(originalSubject)
|
||||
? originalSubject
|
||||
: `Fwd: ${originalSubject}`
|
||||
editor.value?.commands.setContent(
|
||||
`<p><br></p><p>---------- Forwarded message ---------</p><p>From: ${senderName}</p><p>Date: ${sentLabel}</p><p>Subject: ${originalSubject}</p><blockquote>${originalBody}</blockquote>`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
[() => props.mode, () => props.source, () => editor.value],
|
||||
([, , currentEditor]) => {
|
||||
if (!currentEditor) {
|
||||
return
|
||||
}
|
||||
|
||||
initializeComposerFromProps()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// Computed
|
||||
const canSend = computed(() => {
|
||||
return to.value.length > 0 && subject.value.trim().length > 0
|
||||
@@ -110,10 +155,8 @@ const saveDraft = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
|
||||
try {
|
||||
const draftData = {
|
||||
await mailStore.saveComposerDraft(props.folder, {
|
||||
to: to.value,
|
||||
cc: cc.value,
|
||||
bcc: bcc.value,
|
||||
@@ -122,27 +165,9 @@ const saveDraft = async () => {
|
||||
html: editor.value?.getHTML() || '',
|
||||
text: editor.value?.getText() || '',
|
||||
},
|
||||
}
|
||||
|
||||
// Find drafts folder for this service
|
||||
// For now, we'll use the current folder's service
|
||||
// In a real implementation, you'd find the actual Drafts folder
|
||||
const response = await entityService.create({
|
||||
provider: props.folder.provider,
|
||||
service: props.folder.service,
|
||||
collection: props.folder.identifier, // Should be drafts folder ID
|
||||
properties: draftData,
|
||||
})
|
||||
|
||||
if (response) {
|
||||
draftId.value = String(response.identifier)
|
||||
}
|
||||
|
||||
lastSaved.value = new Date()
|
||||
} catch (error) {
|
||||
console.error('[MessageComposer] Failed to save draft:', error)
|
||||
} finally {
|
||||
saving.value = false
|
||||
console.error('[Mail][Composer] Failed to save draft:', error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,53 +198,33 @@ onBeforeUnmount(() => {
|
||||
if (autoSaveTimer) {
|
||||
clearTimeout(autoSaveTimer)
|
||||
}
|
||||
mailStore.resetComposerState()
|
||||
editor.value?.destroy()
|
||||
})
|
||||
|
||||
// Handlers
|
||||
const handleClose = () => {
|
||||
mailStore.resetComposerState()
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!canSend.value || sending.value) return
|
||||
|
||||
sending.value = true
|
||||
|
||||
|
||||
try {
|
||||
await entityService.transmit({
|
||||
message: {
|
||||
to: to.value,
|
||||
cc: cc.value.length > 0 ? cc.value : undefined,
|
||||
bcc: bcc.value.length > 0 ? bcc.value : undefined,
|
||||
subject: subject.value,
|
||||
body: {
|
||||
html: editor.value?.getHTML() || '',
|
||||
text: editor.value?.getText() || '',
|
||||
},
|
||||
await mailStore.sendComposerMessage({
|
||||
to: to.value,
|
||||
cc: cc.value,
|
||||
bcc: bcc.value,
|
||||
subject: subject.value,
|
||||
body: {
|
||||
html: editor.value?.getHTML() || '',
|
||||
text: editor.value?.getText() || '',
|
||||
},
|
||||
})
|
||||
|
||||
// Delete draft if it was saved
|
||||
if (draftId.value && props.folder) {
|
||||
try {
|
||||
await entityService.delete({
|
||||
provider: props.folder.provider,
|
||||
service: props.folder.service,
|
||||
collection: props.folder.identifier,
|
||||
identifier: draftId.value,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[MessageComposer] Failed to delete draft:', error)
|
||||
}
|
||||
}
|
||||
|
||||
emit('sent')
|
||||
} catch (error) {
|
||||
console.error('[MessageComposer] Failed to send message:', error)
|
||||
alert('Failed to send message. Please try again.')
|
||||
} finally {
|
||||
sending.value = false
|
||||
console.error('[Mail][Composer] Failed to send message:', error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,192 +253,61 @@ const removeLink = () => editor.value?.chain().focus().unsetLink().run()
|
||||
const isActive = (name: string, attrs?: any) => {
|
||||
return editor.value?.isActive(name, attrs) || false
|
||||
}
|
||||
|
||||
const toggleLink = () => {
|
||||
if (isActive('link')) {
|
||||
removeLink()
|
||||
return
|
||||
}
|
||||
|
||||
setLink()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="message-composer">
|
||||
<!-- Toolbar -->
|
||||
<v-toolbar density="compact" elevation="0" class="composer-toolbar">
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="handleClose"
|
||||
icon="mdi-close"
|
||||
>
|
||||
<v-icon>mdi-close</v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">Close</v-tooltip>
|
||||
</v-btn>
|
||||
<ComposerToolbar
|
||||
:mode="mode"
|
||||
:save-status="saveStatus"
|
||||
:can-send="canSend"
|
||||
:sending="sending"
|
||||
@close="handleClose"
|
||||
@send="handleSend"
|
||||
/>
|
||||
|
||||
<v-toolbar-title>
|
||||
{{ replyTo ? 'Reply' : 'New Message' }}
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer />
|
||||
|
||||
<span v-if="saveStatus" class="text-caption text-medium-emphasis mr-4">
|
||||
{{ saveStatus }}
|
||||
</span>
|
||||
|
||||
<v-btn
|
||||
color="primary"
|
||||
:disabled="!canSend"
|
||||
:loading="sending"
|
||||
@click="handleSend"
|
||||
prepend-icon="mdi-send"
|
||||
>
|
||||
Send
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<!-- Composer content -->
|
||||
<div class="composer-content">
|
||||
<!-- Recipients -->
|
||||
<div class="composer-fields pa-4">
|
||||
<v-combobox
|
||||
v-model="to"
|
||||
label="To"
|
||||
chips
|
||||
multiple
|
||||
closable-chips
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
class="mb-2"
|
||||
>
|
||||
<template v-slot:append-inner>
|
||||
<v-btn
|
||||
size="x-small"
|
||||
variant="text"
|
||||
@click="toggleCc"
|
||||
class="mr-1"
|
||||
>
|
||||
Cc
|
||||
</v-btn>
|
||||
<v-btn
|
||||
size="x-small"
|
||||
variant="text"
|
||||
@click="toggleBcc"
|
||||
>
|
||||
Bcc
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-combobox>
|
||||
|
||||
<v-combobox
|
||||
v-if="showCc"
|
||||
v-model="cc"
|
||||
label="Cc"
|
||||
chips
|
||||
multiple
|
||||
closable-chips
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
class="mb-2"
|
||||
/>
|
||||
|
||||
<v-combobox
|
||||
v-if="showBcc"
|
||||
v-model="bcc"
|
||||
label="Bcc"
|
||||
chips
|
||||
multiple
|
||||
closable-chips
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
class="mb-2"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="subject"
|
||||
label="Subject"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</div>
|
||||
<ComposerRecipients
|
||||
:to="to"
|
||||
:cc="cc"
|
||||
:bcc="bcc"
|
||||
:subject="subject"
|
||||
:show-cc="showCc"
|
||||
:show-bcc="showBcc"
|
||||
@update:to="to = $event"
|
||||
@update:cc="cc = $event"
|
||||
@update:bcc="bcc = $event"
|
||||
@update:subject="subject = $event"
|
||||
@toggle:cc="toggleCc"
|
||||
@toggle:bcc="toggleBcc"
|
||||
/>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<!-- Editor toolbar -->
|
||||
<v-toolbar density="compact" elevation="0" class="editor-toolbar">
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
:class="{ 'v-btn--active': isActive('bold') }"
|
||||
@click="toggleBold"
|
||||
>
|
||||
<v-icon>mdi-format-bold</v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">Bold</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
:class="{ 'v-btn--active': isActive('italic') }"
|
||||
@click="toggleItalic"
|
||||
>
|
||||
<v-icon>mdi-format-italic</v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">Italic</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
:class="{ 'v-btn--active': isActive('underline') }"
|
||||
@click="toggleUnderline"
|
||||
>
|
||||
<v-icon>mdi-format-underline</v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">Underline</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-divider vertical class="mx-2" />
|
||||
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
:class="{ 'v-btn--active': isActive('bulletList') }"
|
||||
@click="toggleBulletList"
|
||||
>
|
||||
<v-icon>mdi-format-list-bulleted</v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">Bullet List</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
:class="{ 'v-btn--active': isActive('orderedList') }"
|
||||
@click="toggleOrderedList"
|
||||
>
|
||||
<v-icon>mdi-format-list-numbered</v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">Numbered List</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-divider vertical class="mx-2" />
|
||||
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
:class="{ 'v-btn--active': isActive('link') }"
|
||||
@click="isActive('link') ? removeLink() : setLink()"
|
||||
>
|
||||
<v-icon>mdi-link</v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">Link</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-spacer />
|
||||
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
>
|
||||
<v-icon>mdi-paperclip</v-icon>
|
||||
<v-tooltip activator="parent" location="bottom">Attach Files</v-tooltip>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<!-- Editor -->
|
||||
<div class="editor-container">
|
||||
<editor-content :editor="editor" />
|
||||
</div>
|
||||
<ComposerEditor
|
||||
:editor="editor"
|
||||
:is-bold-active="isActive('bold')"
|
||||
:is-italic-active="isActive('italic')"
|
||||
:is-underline-active="isActive('underline')"
|
||||
:is-bullet-list-active="isActive('bulletList')"
|
||||
:is-ordered-list-active="isActive('orderedList')"
|
||||
:is-link-active="isActive('link')"
|
||||
@bold="toggleBold"
|
||||
@italic="toggleItalic"
|
||||
@underline="toggleUnderline"
|
||||
@bullet-list="toggleBulletList"
|
||||
@ordered-list="toggleOrderedList"
|
||||
@link="toggleLink"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -457,65 +331,4 @@ const isActive = (name: string, attrs?: any) => {
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.composer-fields {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.editor-toolbar {
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid rgb(var(--v-border-color));
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background-color: rgb(var(--v-theme-background));
|
||||
}
|
||||
|
||||
.v-btn--active {
|
||||
background-color: rgba(var(--v-theme-primary), 0.12);
|
||||
color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
|
||||
:deep(.tiptap-editor) {
|
||||
outline: none;
|
||||
min-height: 300px;
|
||||
|
||||
p.is-editor-empty:first-child::before {
|
||||
color: rgb(var(--v-theme-on-surface-variant));
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
line-height: 1.3;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
padding-left: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 3px solid rgb(var(--v-border-color));
|
||||
padding-left: 1em;
|
||||
margin-left: 0;
|
||||
margin-bottom: 0.5em;
|
||||
color: rgb(var(--v-theme-on-surface-variant));
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user