Files
provider_imap/src/components/ImapAuxiliaryPanel.vue
2026-04-25 15:43:56 -04:00

268 lines
7.9 KiB
Vue

<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { ServiceObject } from '@KTXM/MailManager/models/service'
type AuxiliaryTab = 'addresses' | 'messages' | 'sync'
type DeleteMode = 'soft' | 'hard'
const props = defineProps<{
service?: ServiceObject
}>()
const emit = defineEmits<{
'update:service': [value: ServiceObject]
}>()
const activeTab = ref<AuxiliaryTab>('addresses')
const deleteMode = ref<DeleteMode>('soft')
const deleteDestination = ref('Trash')
const primaryAddress = ref('')
const secondaryAddresses = ref('')
const settingGroups = [
{
title: 'Addresses',
value: 'addresses' as const,
icon: 'mdi-at',
description: 'Configure the primary mailbox identity and any sender aliases exposed by this service.'
},
{
title: 'Messages',
value: 'messages' as const,
icon: 'mdi-email-outline',
description: 'Control how message actions should be translated to operations.'
},
{
title: 'Sync',
value: 'sync' as const,
icon: 'mdi-sync',
description: 'Reserved for future synchronization settings.'
},
]
const deleteModeOptions = [
{
title: 'Move to another mailbox',
value: 'soft' as const,
subtitle: 'Marks delete as a move operation and keeps the message in a destination mailbox.'
},
{
title: 'Permanently delete',
value: 'hard' as const,
subtitle: 'Removes the message immediately without moving it first.'
}
]
const destinationHint = computed(() => {
if (deleteMode.value === 'hard') {
return 'Not used when messages are deleted permanently.'
}
return 'Mailbox identifier or well-known role target, for example Trash.'
})
const secondaryAddressesHint = computed(() => {
return 'Use one address per line. Commas are also accepted.'
})
watch(
() => props.service,
service => {
syncFromService(service)
},
{ immediate: true }
)
watch(
[deleteMode, deleteDestination, primaryAddress, secondaryAddresses],
() => {
const nextService = props.service ?? new ServiceObject()
const nextAuxiliary = {
...(nextService.auxiliary ?? {}),
deleteMode: deleteMode.value,
deleteDestination: deleteMode.value === 'soft'
? normalizeDeleteDestination(deleteDestination.value)
: undefined,
}
if (sameAuxiliary(nextService.auxiliary ?? {}, nextAuxiliary)) {
if (sameAddresses(nextService, primaryAddress.value, secondaryAddresses.value)) {
return
}
}
nextService.primaryAddress = normalizePrimaryAddress(primaryAddress.value)
nextService.secondaryAddresses = normalizeSecondaryAddresses(secondaryAddresses.value)
nextService.auxiliary = nextAuxiliary
emit('update:service', nextService)
},
{ immediate: true }
)
function syncFromService(service?: ServiceObject) {
const auxiliary = service?.auxiliary ?? {}
deleteMode.value = auxiliary.deleteMode === 'hard' ? 'hard' : 'soft'
deleteDestination.value = typeof auxiliary.deleteDestination === 'string' && auxiliary.deleteDestination.length > 0
? auxiliary.deleteDestination
: 'Trash'
primaryAddress.value = service?.primaryAddress ?? ''
secondaryAddresses.value = (service?.secondaryAddresses ?? []).join('\n')
}
function normalizeDeleteDestination(value: string): string {
const trimmedValue = value.trim()
return trimmedValue.length > 0 ? trimmedValue : 'Trash'
}
function normalizePrimaryAddress(value: string): string | null {
const trimmedValue = value.trim()
return trimmedValue.length > 0 ? trimmedValue : null
}
function normalizeSecondaryAddresses(value: string): string[] {
return value
.split(/\r?\n|,/)
.map(entry => entry.trim())
.filter((entry, index, entries) => entry.length > 0 && entries.indexOf(entry) === index)
}
function sameAuxiliary(current: Record<string, any>, next: Record<string, any>): boolean {
return (current.deleteMode === 'hard' ? 'hard' : 'soft') === next.deleteMode
&& (current.deleteDestination ?? undefined) === (next.deleteDestination ?? undefined)
}
function sameAddresses(service: ServiceObject, nextPrimaryAddress: string, nextSecondaryAddresses: string): boolean {
return (service.primaryAddress ?? null) === normalizePrimaryAddress(nextPrimaryAddress)
&& JSON.stringify(service.secondaryAddresses) === JSON.stringify(normalizeSecondaryAddresses(nextSecondaryAddresses))
}
</script>
<template>
<div class="imap-auxiliary-panel">
<div class="imap-auxiliary-shell">
<v-tabs
v-model="activeTab"
direction="vertical"
color="primary"
class="imap-auxiliary-tabs"
>
<v-tab
v-for="group in settingGroups"
:key="group.value"
:value="group.value"
class="justify-start"
>
<v-icon start>{{ group.icon }}</v-icon>
{{ group.title }}
</v-tab>
</v-tabs>
<v-window v-model="activeTab" class="flex-1-1">
<v-window-item value="addresses">
<div class="imap-settings-card">
<h3 class="text-h6 mb-2">Addresses</h3>
<p class="text-body-2 text-medium-emphasis mb-6">
Configure the primary mailbox identity and any additional sender aliases exposed by this service.
</p>
<v-text-field
v-model="primaryAddress"
label="Primary Address"
variant="outlined"
prepend-inner-icon="mdi-email-outline"
class="mb-4"
/>
<v-textarea
v-model="secondaryAddresses"
label="Secondary Addresses"
variant="outlined"
prepend-inner-icon="mdi-email-multiple-outline"
rows="4"
:hint="secondaryAddressesHint"
persistent-hint
/>
</div>
</v-window-item>
<v-window-item value="messages">
<div class="imap-settings-card">
<h3 class="text-h6 mb-2">Message Deletion</h3>
<p class="text-body-2 text-medium-emphasis mb-6">
Choose how the mail system should react when a delete command is issued.
</p>
<v-radio-group
v-model="deleteMode"
color="primary"
class="mb-4"
>
<v-radio
v-for="option in deleteModeOptions"
:key="option.value"
:value="option.value"
>
<template #label>
<div>
<div class="text-body-1">{{ option.title }}</div>
<div class="text-caption text-medium-emphasis">{{ option.subtitle }}</div>
</div>
</template>
</v-radio>
</v-radio-group>
<v-text-field
v-model="deleteDestination"
label="Delete Destination"
variant="outlined"
prepend-inner-icon="mdi-folder-move-outline"
:disabled="deleteMode === 'hard'"
:hint="destinationHint"
persistent-hint
/>
</div>
</v-window-item>
<v-window-item value="sync">
<div class="imap-settings-card">
<h3 class="text-h6 mb-2">Sync Settings</h3>
<p class="text-body-2 text-medium-emphasis mb-0">
Additional synchronization controls can be added here as the provider grows.
</p>
</div>
</v-window-item>
</v-window>
</div>
</div>
</template>
<style scoped>
.imap-auxiliary-shell {
display: grid;
grid-template-columns: minmax(180px, 220px) minmax(0, 1fr);
gap: 24px;
align-items: start;
}
.imap-auxiliary-tabs {
border-right: 1px solid rgba(var(--v-theme-outline), 0.16);
padding-right: 12px;
}
.imap-settings-card {
padding: 4px 4px 4px 0;
}
@media (max-width: 768px) {
.imap-auxiliary-shell {
grid-template-columns: 1fr;
}
.imap-auxiliary-tabs {
border-right: 0;
border-bottom: 1px solid rgba(var(--v-theme-outline), 0.16);
padding-right: 0;
padding-bottom: 12px;
}
}
</style>