chore: bunch of improvements
All checks were successful
JS Unit Tests / test (pull_request) Successful in 33s
Build Test / test (pull_request) Successful in 36s
PHP Unit Tests / test (pull_request) Successful in 1m12s

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-04-23 22:00:50 -04:00
parent b617234b40
commit 3362afb7ec
28 changed files with 1717 additions and 1297 deletions

View File

@@ -2,17 +2,12 @@
import { ref, computed, watch } from 'vue'
import { useServicesStore } from '@MailManager/stores/servicesStore'
import { useProvidersStore } from '@MailManager/stores/providersStore'
import type { ServiceLocation, ServiceIdentity } from '@MailManager/types'
import type { ServiceObject } from '@MailManager/models/service'
import ProviderConfigStep from '@MailManager/components/steps/ProviderConfigStep.vue'
import ProviderAuthStep from '@MailManager/components/steps/ProviderAuthStep.vue'
import TestAndSaveStep from '@MailManager/components/steps/TestAndSaveStep.vue'
import type { ProviderObject, ServiceObject } from '@MailManager/models'
import ProviderProtocolPanel from '@MailManager/components/steps/ProviderProtocolPanel.vue'
import ProviderAuthPanel from '@MailManager/components/steps/ProviderAuthPanel.vue'
import TestAndSavePanel from '@MailManager/components/steps/TestAndSavePanel.vue'
const EDIT_STEPS = {
CONFIG: 1,
AUTH: 2,
TEST: 3
} as const
type EditTab = 'general' | 'protocol' | 'auth'
const props = defineProps<{
modelValue: boolean
@@ -33,146 +28,82 @@ const dialogOpen = computed({
set: (val) => emit('update:modelValue', val)
})
const currentStep = ref<number>(EDIT_STEPS.CONFIG)
const currentTab = ref<EditTab>('general')
const saving = ref(false)
const loading = ref(false)
const loadError = ref<string | null>(null)
// Service data
const service = ref<ServiceObject | null>(null)
const providerLabel = ref<string>('')
// Editable fields
const accountLabel = ref<string>('')
const accountEnabled = ref(true)
const configuredLocation = ref<ServiceLocation | null>(null)
const configuredIdentity = ref<ServiceIdentity | null>(null)
const localProvider = ref<ProviderObject | null>(null)
const localService = ref<ServiceObject | null>(null)
const mutated = ref(false)
// Validation states
const configValid = ref(false)
const authValid = ref(false)
const testAndSaveValid = ref(false)
// Load service data when dialog opens
watch(dialogOpen, async (isOpen) => {
if (isOpen) {
await loadService()
const tabItems = [
{
title: 'General',
icon: 'mdi-view-dashboard-outline',
value: 'general' as const
},
{
title: 'Protocol',
icon: 'mdi-tune-vertical',
value: 'protocol' as const
},
{
title: 'Authentication',
icon: 'mdi-shield-key-outline',
value: 'auth' as const
}
})
async function loadService() {
loading.value = true
try {
// Load providers if not already loaded
if (!providersStore.has) {
await providersStore.list()
}
// Fetch the service
service.value = await servicesStore.fetch(props.serviceProvider, props.serviceIdentifier)
// Set initial values
accountLabel.value = service.value.label || ''
accountEnabled.value = service.value.enabled
configuredLocation.value = service.value.location
configuredIdentity.value = service.value.identity
// Get provider label
const provider = providersStore.provider(props.serviceProvider)
providerLabel.value = provider?.label || props.serviceProvider
// Mark config as valid if location exists
configValid.value = !!configuredLocation.value
authValid.value = !!configuredIdentity.value
} catch (error) {
console.error('Failed to load service:', error)
} finally {
loading.value = false
}
}
// Stepper configuration
const stepperItems = [
{ title: 'Protocol', value: EDIT_STEPS.CONFIG },
{ title: 'Authentication', value: EDIT_STEPS.AUTH },
{ title: 'Test & Save', value: EDIT_STEPS.TEST }
]
const canSave = computed(() => {
return testAndSaveValid.value
})
// Navigation button visibility
const showPreviousButton = computed(() => currentStep.value > EDIT_STEPS.CONFIG)
const showNextButton = computed(() => currentStep.value < EDIT_STEPS.TEST)
const showSaveButton = computed(() => currentStep.value === EDIT_STEPS.TEST)
const showSaveButton = computed(() => currentTab.value === 'general')
const accountReady = computed(() => localProvider.value !== null && localService.value !== null)
const canProceedToNext = computed(() => {
if (currentStep.value === EDIT_STEPS.CONFIG) {
return configValid.value && !!configuredLocation.value
}
if (currentStep.value === EDIT_STEPS.AUTH) {
return authValid.value
}
return false
})
// Navigation methods
function handlePreviousStep() {
if (currentStep.value > EDIT_STEPS.CONFIG) {
currentStep.value--
}
}
function handleNextStep() {
if (currentStep.value < EDIT_STEPS.TEST) {
currentStep.value++
}
}
async function testConnection() {
if (!service.value || !configuredLocation.value || !configuredIdentity.value) {
return {
success: false,
message: 'Missing configuration'
// 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
}
const testResult = await servicesStore.test(
service.value.provider,
service.value.identifier,
configuredLocation.value,
configuredIdentity.value
)
return testResult
}
async function saveAccount() {
if (!service.value || !configuredLocation.value || !configuredIdentity.value) return
saving.value = true
try {
const accountData = {
label: accountLabel.value || service.value.label,
enabled: accountEnabled.value,
location: configuredLocation.value,
identity: configuredIdentity.value
}
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)
])
await servicesStore.update(
service.value.provider,
service.value.identifier as string | number,
accountData
)
emit('saved')
close()
localProvider.value = provider.clone()
localService.value = service.clone()
} catch (error) {
console.error('Failed to save account:', error)
// TODO: Show error message to user
console.error('[Mail Manager][Edit Account Dialog] - Failed to load service:', error)
loadError.value = 'Failed to load service details'
} finally {
saving.value = false
loading.value = false
}
}
@@ -183,21 +114,89 @@ function close() {
}
function resetForm() {
currentStep.value = EDIT_STEPS.CONFIG
service.value = null
accountLabel.value = ''
accountEnabled.value = true
configuredLocation.value = null
configuredIdentity.value = null
configValid.value = false
authValid.value = false
currentTab.value = 'general'
localService.value = null
localProvider.value = null
loadError.value = null
testAndSaveValid.value = false
}
// Watch for location changes
watch(configuredLocation, (newLocation) => {
configValid.value = !!newLocation
})
function isTabDisabled(tab: EditTab) {
if (tab === 'auth') {
return !localService.value?.location
}
return false
}
function handleUpdate(mutatedService: ServiceObject) {
localService.value = mutatedService
mutated.value = true
}
async function testConnection() {
try {
let testResult = null
if (mutated.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 (!mutated.value) {
close()
return
}
if (!localService.value?.location || !localService.value?.identity) return
saving.value = true
try {
const accountData = {
label: accountLabel.value || localService.value.label,
enabled: accountEnabled.value,
location: localService.value.location,
identity: localService.value.identity
}
await servicesStore.update(
localService.value.provider,
localService.value.identifier as string | number,
accountData
)
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>
@@ -219,105 +218,77 @@ watch(configuredLocation, (newLocation) => {
<v-divider />
<v-card-text v-if="loading" class="text-center py-8">
<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">
<!-- Account Info Header -->
<div v-if="service" class="pa-6 bg-surface-variant">
<div class="d-flex align-center gap-3">
<v-avatar color="primary">
<v-icon>mdi-email</v-icon>
</v-avatar>
<div>
<div class="text-subtitle-1 font-weight-medium">
{{ service.label || 'Unnamed Account' }}
</div>
<div class="text-caption text-medium-emphasis">
{{ service.primaryAddress || service.identifier }}
</div>
<div class="text-caption text-medium-emphasis">
Provider: {{ providerLabel }}
</div>
</div>
</div>
</div>
<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-stepper
v-model="currentStep"
:items="stepperItems"
alt-labels
flat
hide-actions
>
<!-- Step 1: Protocol Configuration -->
<template #item.1>
<v-window v-model="currentTab">
<v-window-item value="general">
<v-card flat class="pa-6">
<ProviderConfigStep
v-if="service"
:provider-id="service.provider"
:discovered-location="configuredLocation || undefined"
v-model="configuredLocation"
@valid="(valid) => configValid = valid"
/>
</v-card>
</template>
<!-- Step 2: Authentication -->
<template #item.2>
<v-card flat class="pa-6">
<ProviderAuthStep
v-if="service"
:provider-id="service.provider"
:provider-label="providerLabel"
:email-address="service.primaryAddress || ''"
:discovered-location="configuredLocation || undefined"
:prefilled-identity="service.primaryAddress || ''"
:prefilled-secret="undefined"
v-model="configuredIdentity"
@valid="(valid) => authValid = valid"
/>
</v-card>
</template>
<!-- Step 3: Test & Save -->
<template #item.3>
<v-card flat class="pa-6">
<TestAndSaveStep
v-if="service"
:provider-id="service.provider"
:provider-label="providerLabel"
:email-address="service.primaryAddress || ''"
:location="configuredLocation"
:identity="configuredIdentity"
:prefilled-label="accountLabel"
<TestAndSavePanel
v-if="localProvider && localService"
:provider="localProvider"
:service="localService"
:on-test="testConnection"
@update:label="(val) => accountLabel = val"
@update:enabled="(val) => accountEnabled = val"
@valid="(valid) => testAndSaveValid = valid"
@update:service="handleUpdate"
/>
</v-card>
</template>
</v-stepper>
</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">
<!-- Previous Button -->
<v-btn
v-if="showPreviousButton"
variant="text"
prepend-icon="mdi-arrow-left"
@click="handlePreviousStep"
>
Previous
</v-btn>
<v-spacer />
<v-btn
@@ -326,18 +297,7 @@ watch(configuredLocation, (newLocation) => {
>
Cancel
</v-btn>
<!-- Next Button -->
<v-btn
v-if="showNextButton"
color="primary"
append-icon="mdi-arrow-right"
:disabled="!canProceedToNext"
@click="handleNextStep"
>
Next
</v-btn>
<!-- Save Button -->
<v-btn
v-if="showSaveButton"