Files
mail_manager/src/components/EditAccountDialog.vue
Sebastian Krupinski 99a68737d1
Some checks failed
JS Unit Tests / test (pull_request) Failing after 29s
Build Test / test (pull_request) Successful in 31s
PHP Unit Tests / test (pull_request) Successful in 1m12s
feat: lots more improvements
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-04-25 15:41:16 -04:00

337 lines
9.0 KiB
Vue

<script setup lang="ts">
import { ref, shallowRef, computed, watch } from 'vue'
import { useServicesStore } from '@MailManager/stores/servicesStore'
import { useProvidersStore } from '@MailManager/stores/providersStore'
import type { ProviderObject, ServiceObject } from '@MailManager/models'
import ProviderAuxiliaryPanel from '@MailManager/components/steps/ProviderAuxiliaryPanel.vue'
import ProviderProtocolPanel from '@MailManager/components/steps/ProviderProtocolPanel.vue'
import ProviderAuthPanel from '@MailManager/components/steps/ProviderAuthPanel.vue'
import TestAndSavePanel from '@MailManager/components/steps/TestAndSavePanel.vue'
type EditTab = 'general' | 'auxiliary' | 'protocol' | 'auth'
const props = defineProps<{
modelValue: boolean
serviceProvider: string
serviceIdentifier: string | number
}>()
const emit = defineEmits<{
'update:modelValue': [value: boolean]
'saved': []
}>()
const servicesStore = useServicesStore()
const providersStore = useProvidersStore()
const dialogOpen = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const currentTab = ref<EditTab>('general')
const saving = ref(false)
const loading = ref(false)
const loadError = ref<string | null>(null)
const localProvider = shallowRef<ProviderObject | null>(null)
const localService = shallowRef<ServiceObject | null>(null)
// Validation states
const testAndSaveValid = ref(false)
function serviceRequiresConnectionTest(service: ServiceObject | null): boolean {
return !!(service?.location?.mutated() || service?.identity?.mutated())
}
const tabItems = [
{
title: 'General',
icon: 'mdi-view-dashboard-outline',
value: 'general' as const
},
{
title: 'Auxiliary Settings',
icon: 'mdi-tune-variant',
value: 'auxiliary' as const
},
{
title: 'Protocol',
icon: 'mdi-tune-vertical',
value: 'protocol' as const
},
{
title: 'Authentication',
icon: 'mdi-shield-key-outline',
value: 'auth' as const
}
]
const canSave = computed(() => {
return !serviceRequiresConnectionTest(localService.value) || testAndSaveValid.value
})
const showSaveButton = computed(() => currentTab.value === 'general')
const accountReady = computed(() => localProvider.value !== null && localService.value !== null)
// Load service data when the dialog is open and the target account is available.
watch(
() => [props.modelValue, props.serviceProvider, props.serviceIdentifier] as const,
async ([isOpen, serviceProvider, serviceIdentifier]) => {
if (!isOpen || !serviceProvider || !serviceIdentifier) {
return
}
await load()
},
{ immediate: true }
)
async function load() {
loading.value = true
loadError.value = null
localProvider.value = null
localService.value = null
if (!props.serviceProvider || !props.serviceIdentifier) {
console.error('[Mail Manager][Edit Account Dialog] - Cannot open dialog missing service or provider identifier')
loadError.value = 'missing service or provider identifier'
loading.value = false
return
}
try {
const [provider, service] = await Promise.all([
providersStore.provider(props.serviceProvider) ?? providersStore.fetch(props.serviceProvider),
servicesStore.service(props.serviceProvider, props.serviceIdentifier) ?? servicesStore.fetch(props.serviceProvider, props.serviceIdentifier)
])
localProvider.value = provider.clone()
localService.value = service.clone()
} catch (error) {
console.error('[Mail Manager][Edit Account Dialog] - Failed to load service:', error)
loadError.value = 'Failed to load service details'
} finally {
loading.value = false
}
}
function close() {
dialogOpen.value = false
// Reset state after animation
setTimeout(resetForm, 300)
}
function resetForm() {
currentTab.value = 'general'
localService.value = null
localProvider.value = null
loadError.value = null
}
function isTabDisabled(tab: EditTab) {
if (tab === 'auth') {
return !localService.value?.location
}
return false
}
function handleUpdate(mutatedService: ServiceObject) {
localService.value = mutatedService
if (serviceRequiresConnectionTest(mutatedService)) {
testAndSaveValid.value = false
}
}
async function testConnection() {
try {
if (!localService.value) {
return {
success: false,
message: 'Missing service configuration'
}
}
let testResult = null
if (serviceRequiresConnectionTest(localService.value)) {
testResult = await servicesStore.test(
localService.value.provider,
null,
localService.value.location,
localService.value.identity
)
} else {
testResult = await servicesStore.test(
localService.value.provider,
localService.value.identifier
)
}
testAndSaveValid.value = testResult.success
return testResult
} catch (error) {
console.error('[Mail Manager][Edit Account Dialog] - Test connection failed:', error)
return {
success: false,
message: 'Test failed due to an unexpected error'
}
}
}
async function saveAccount() {
// No changes made, just close the dialog
if (!localService.value.mutated() && !localService.value.location?.mutated() && !localService.value.identity?.mutated()) {
close()
return
}
saving.value = true
try {
await servicesStore.update(
localService.value.provider,
localService.value.identifier as string | number,
true, // delta update
localService.value
)
emit('saved')
close()
} catch (error) {
console.error('[Mail Manager][Edit Account Dialog] - Failed to save service:', error)
// TODO: Show error message to user
} finally {
saving.value = false
}
}
</script>
<template>
<v-dialog
v-model="dialogOpen"
max-width="900"
persistent
scrollable
>
<v-card>
<v-card-title class="d-flex justify-space-between align-center pa-6">
<span class="text-h5">Edit Mail Account</span>
<v-btn
icon="mdi-close"
variant="text"
@click="close"
/>
</v-card-title>
<v-divider />
<v-card-text v-if="loading || (!loadError && !accountReady)" class="text-center py-8">
<v-progress-circular indeterminate color="primary" />
<p class="text-caption text-medium-emphasis mt-2">Loading account...</p>
</v-card-text>
<v-card-text v-else-if="loadError" class="pa-6">
<v-alert type="error" variant="tonal">
{{ loadError }}
</v-alert>
</v-card-text>
<v-card-text v-else class="pa-0">
<v-tabs
v-model="currentTab"
bg-color="transparent"
grow
class="px-4 pt-2"
>
<v-tab
v-for="item in tabItems"
:key="item.value"
:value="item.value"
:disabled="isTabDisabled(item.value)"
>
<v-icon start>{{ item.icon }}</v-icon>
{{ item.title }}
</v-tab>
</v-tabs>
<v-divider />
<v-window v-model="currentTab">
<v-window-item value="general">
<v-card flat class="pa-6">
<TestAndSavePanel
v-if="localProvider && localService"
:provider="localProvider!"
:service="localService!"
:on-test="testConnection"
@update:service="handleUpdate"
/>
</v-card>
</v-window-item>
<v-window-item value="auxiliary">
<v-card flat class="pa-6">
<ProviderAuxiliaryPanel
v-if="localProvider && localService"
:provider="localProvider!"
:service="localService!"
@update:service="handleUpdate"
/>
</v-card>
</v-window-item>
<v-window-item value="protocol">
<v-card flat class="pa-6">
<ProviderProtocolPanel
v-if="localProvider && localService"
:provider="localProvider!"
:service="localService!"
@update:service="handleUpdate"
/>
</v-card>
</v-window-item>
<v-window-item value="auth">
<v-card flat class="pa-6">
<ProviderAuthPanel
v-if="localProvider && localService"
:provider="localProvider!"
:service="localService!"
@update:service="handleUpdate"
/>
</v-card>
</v-window-item>
</v-window>
</v-card-text>
<v-divider />
<v-card-actions class="pa-6">
<v-spacer />
<v-btn
variant="text"
@click="close"
>
Cancel
</v-btn>
<!-- Save Button -->
<v-btn
v-if="showSaveButton"
color="primary"
:loading="saving"
:disabled="!canSave"
@click="saveAccount"
>
<v-icon start>mdi-content-save</v-icon>
Save Changes
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>