612 lines
18 KiB
Vue
612 lines
18 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, watch } from 'vue'
|
|
import { useIntegrationStore } from '@KTXC/stores/integrationStore'
|
|
import { useServicesStore } from '@MailManager/stores/servicesStore'
|
|
import { useProvidersStore } from '@MailManager/stores/providersStore'
|
|
import { ServiceObject, type ProviderObject } from '@MailManager/models'
|
|
import type { ProviderDiscoveryStatus, ServiceInterface, ServiceLocation } from '@MailManager/types'
|
|
import DiscoveryEntryPanel from '@MailManager/components/steps/DiscoveryEntryPanel.vue'
|
|
import DiscoveryStatusPanel from '@MailManager/components/steps/DiscoveryStatusPanel.vue'
|
|
import ProviderSelectionPanel from '@MailManager/components/steps/ProviderSelectionPanel.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'
|
|
|
|
// ==================== Step Constants ====================
|
|
// Discovery flow: Entry → Discovery → Auth → Test
|
|
const DISCOVERY_STEPS = {
|
|
ENTRY: 1,
|
|
DISCOVERY: 2,
|
|
AUTH: 3,
|
|
TEST: 4
|
|
} as const
|
|
|
|
// Manual flow: Entry → Provider Select → Config → Auth → Test
|
|
const MANUAL_STEPS = {
|
|
ENTRY: 1,
|
|
PROVIDER_SELECT: 2,
|
|
CONFIG: 3,
|
|
AUTH: 4,
|
|
TEST: 5
|
|
} as const
|
|
|
|
const props = defineProps<{
|
|
modelValue: boolean
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: boolean]
|
|
'saved': []
|
|
}>()
|
|
|
|
const integrationStore = useIntegrationStore()
|
|
const servicesStore = useServicesStore()
|
|
const providersStore = useProvidersStore()
|
|
|
|
const dialogOpen = computed({
|
|
get: () => props.modelValue,
|
|
set: (val) => emit('update:modelValue', val)
|
|
})
|
|
|
|
const currentStep = ref<number>(DISCOVERY_STEPS.ENTRY)
|
|
const saving = ref(false)
|
|
const isManualMode = ref(false)
|
|
|
|
// Step 1: Entry
|
|
const discoverAddress = ref<string>('')
|
|
const discoverSecret = ref<string | null>(null)
|
|
const discoverHostname = ref<string | null>(null)
|
|
|
|
// Step 2: Discovery Status / Provider Selection
|
|
const selectedProvider = ref<ProviderObject | null>(null)
|
|
const selectedService = ref<ServiceObject | null>(null)
|
|
|
|
// Step 5: Test & Save
|
|
const testAndSaveValid = ref(false)
|
|
|
|
// Local discovery state (not stored in global store)
|
|
const discoveredServices = ref<ServiceObject[]>([])
|
|
const discoveryStatus = ref<Record<string, ProviderDiscoveryStatus>>({})
|
|
|
|
// Load providers when dialog opens
|
|
watch(dialogOpen, async (isOpen) => {
|
|
if (isOpen && !providersStore.has) {
|
|
await providersStore.list()
|
|
}
|
|
})
|
|
|
|
// Stepper configuration
|
|
const stepperItems = computed(() => {
|
|
if (isManualMode.value) {
|
|
// Manual: Entry → Selection → Config → Auth → Test
|
|
return [
|
|
{ title: 'Email', value: MANUAL_STEPS.ENTRY },
|
|
{ title: 'Provider', value: MANUAL_STEPS.PROVIDER_SELECT },
|
|
{ title: 'Protocol', value: MANUAL_STEPS.CONFIG },
|
|
{ title: 'Authentication', value: MANUAL_STEPS.AUTH },
|
|
{ title: 'Test & Save', value: MANUAL_STEPS.TEST }
|
|
]
|
|
}
|
|
|
|
// Discovery: Entry → Discovery → Auth → Test
|
|
if (currentStep.value >= DISCOVERY_STEPS.AUTH) {
|
|
return [
|
|
{ title: 'Email', value: DISCOVERY_STEPS.ENTRY },
|
|
{ title: 'Discovery', value: DISCOVERY_STEPS.DISCOVERY },
|
|
{ title: 'Authentication', value: DISCOVERY_STEPS.AUTH },
|
|
{ title: 'Test & Save', value: DISCOVERY_STEPS.TEST }
|
|
]
|
|
}
|
|
|
|
return [
|
|
{ title: 'Email', value: DISCOVERY_STEPS.ENTRY },
|
|
{ title: 'Discovery', value: DISCOVERY_STEPS.DISCOVERY }
|
|
]
|
|
})
|
|
|
|
const canSave = computed(() => {
|
|
return testAndSaveValid.value
|
|
})
|
|
|
|
// Navigation button visibility
|
|
const showNextButton = computed(() => {
|
|
if (isManualMode.value) {
|
|
// Manual: Show Next on Config (3) and Auth (4)
|
|
return currentStep.value === MANUAL_STEPS.CONFIG || currentStep.value === MANUAL_STEPS.AUTH
|
|
} else {
|
|
// Discovery: Show Next on Auth (3)
|
|
return currentStep.value === DISCOVERY_STEPS.AUTH
|
|
}
|
|
})
|
|
|
|
const showSaveButton = computed(() => {
|
|
if (isManualMode.value) {
|
|
return currentStep.value === MANUAL_STEPS.TEST
|
|
} else {
|
|
return currentStep.value === DISCOVERY_STEPS.TEST
|
|
}
|
|
})
|
|
|
|
const canProceedToNext = computed(() => {
|
|
if (isManualMode.value) {
|
|
if (currentStep.value === MANUAL_STEPS.CONFIG) {
|
|
return !!selectedService.value?.location
|
|
}
|
|
if (currentStep.value === MANUAL_STEPS.AUTH) {
|
|
return !!selectedService.value?.identity
|
|
}
|
|
} else {
|
|
if (currentStep.value === DISCOVERY_STEPS.AUTH) {
|
|
return !!selectedService.value?.identity
|
|
}
|
|
}
|
|
return false
|
|
})
|
|
|
|
function createServiceObject(
|
|
providerId: string,
|
|
data: Partial<ServiceInterface> = {}
|
|
): ServiceObject {
|
|
const model: ServiceInterface = {
|
|
'@type': 'mail:service',
|
|
version: 1,
|
|
provider: providerId,
|
|
identifier: null,
|
|
label: data.label ?? null,
|
|
enabled: data.enabled ?? true,
|
|
primaryAddress: data.primaryAddress ?? (discoverAddress.value || null),
|
|
secondaryAddresses: data.secondaryAddresses ?? null,
|
|
location: data.location ?? null,
|
|
identity: data.identity ?? null,
|
|
capabilities: data.capabilities ?? {},
|
|
auxiliary: data.auxiliary ?? {}
|
|
}
|
|
|
|
const factoryItem = integrationStore.getItemById('mail_service_factory', providerId) as any
|
|
const factory = factoryItem?.factory
|
|
return factory ? factory(model) : new ServiceObject().fromJson(model)
|
|
}
|
|
|
|
function setSelectedProviderAndService(providerId: string, service: ServiceObject) {
|
|
selectedProvider.value = providersStore.provider(providerId)
|
|
selectedService.value = service
|
|
testAndSaveValid.value = false
|
|
}
|
|
|
|
function handleServiceUpdate(service: ServiceObject) {
|
|
selectedService.value = service
|
|
}
|
|
|
|
function handleServiceTested(success: boolean) {
|
|
testAndSaveValid.value = success
|
|
}
|
|
|
|
watch(selectedService, () => {
|
|
testAndSaveValid.value = false
|
|
}, { deep: true })
|
|
|
|
// Navigation methods
|
|
function handlePreviousStep() {
|
|
if (currentStep.value > 1) {
|
|
currentStep.value--
|
|
}
|
|
}
|
|
|
|
function handleNextStep() {
|
|
if (isManualMode.value) {
|
|
if (currentStep.value < MANUAL_STEPS.TEST) {
|
|
currentStep.value++
|
|
}
|
|
} else {
|
|
if (currentStep.value < DISCOVERY_STEPS.TEST) {
|
|
currentStep.value++
|
|
}
|
|
}
|
|
}
|
|
|
|
async function handleDiscover() {
|
|
// Move to discovery status screen
|
|
currentStep.value = DISCOVERY_STEPS.DISCOVERY
|
|
|
|
// Extract provider IDs
|
|
const providerIds = Object.values(providersStore.providers).map(p => p.identifier)
|
|
|
|
if (providerIds.length === 0) {
|
|
console.error('No providers available')
|
|
return
|
|
}
|
|
|
|
// Initialize status for all providers
|
|
discoveryStatus.value = providerIds.reduce((acc, identifier) => {
|
|
acc[identifier] = {
|
|
provider: identifier,
|
|
status: 'pending'
|
|
}
|
|
return acc
|
|
}, {} as Record<string, ProviderDiscoveryStatus>)
|
|
|
|
discoveredServices.value = []
|
|
|
|
// Start discovery for each provider in parallel
|
|
const promises = providerIds.map(async (identifier) => {
|
|
// Mark as discovering
|
|
discoveryStatus.value[identifier].status = 'discovering'
|
|
|
|
try {
|
|
let discoveredService: any = undefined
|
|
await servicesStore.discover(
|
|
discoverAddress.value,
|
|
discoverSecret.value || undefined,
|
|
discoverHostname.value || undefined,
|
|
identifier,
|
|
(service) => { discoveredService = service }
|
|
)
|
|
|
|
// Success - check if we got results for this provider
|
|
if (discoveredService && discoveredService.location) {
|
|
discoveryStatus.value[identifier] = {
|
|
provider: identifier,
|
|
status: 'success',
|
|
location: discoveredService.location,
|
|
metadata: extractLocationMetadata(discoveredService.location)
|
|
}
|
|
discoveredServices.value.push(discoveredService)
|
|
} else {
|
|
// No configuration found for this provider
|
|
discoveryStatus.value[identifier].status = 'failed'
|
|
discoveryStatus.value[identifier].error = 'Not configured'
|
|
}
|
|
} catch (error: any) {
|
|
// Failed - update status with error
|
|
discoveryStatus.value[identifier] = {
|
|
provider: identifier,
|
|
status: 'failed',
|
|
error: error.message || 'Discovery failed'
|
|
}
|
|
}
|
|
})
|
|
|
|
// Wait for all discoveries to complete
|
|
await Promise.allSettled(promises)
|
|
}
|
|
|
|
/**
|
|
* Extract display metadata from location for UI
|
|
*/
|
|
function extractLocationMetadata(location: ServiceLocation) {
|
|
switch (location.type) {
|
|
case 'URI':
|
|
return {
|
|
host: location.host,
|
|
port: location.port,
|
|
protocol: location.scheme
|
|
}
|
|
case 'SOCKET_SOLE':
|
|
return {
|
|
host: location.host,
|
|
port: location.port,
|
|
protocol: location.encryption
|
|
}
|
|
default:
|
|
return {}
|
|
}
|
|
}
|
|
|
|
async function handleProviderSelect(identifier: string) {
|
|
// User clicked "Select" on discovered provider - skip config, go to auth
|
|
const discovered = discoveredServices.value.find(s => s.provider === identifier)
|
|
if (!discovered || !discovered.location) return
|
|
|
|
const discoveredJson = discovered.toJson()
|
|
const service = createServiceObject(identifier, {
|
|
...discoveredJson,
|
|
label: discoveredJson.label || discoverAddress.value,
|
|
enabled: discoveredJson.enabled ?? true,
|
|
primaryAddress: discoveredJson.primaryAddress || discoverAddress.value,
|
|
location: discoveredJson.location
|
|
})
|
|
setSelectedProviderAndService(identifier, service)
|
|
|
|
// Discovery path: Entry → Discovery → Auth → Test
|
|
currentStep.value = DISCOVERY_STEPS.AUTH // Go to auth step
|
|
}
|
|
|
|
function handleProviderAdvanced(identifier: string) {
|
|
// User clicked "Advanced" - show manual config with pre-filled values
|
|
const discovered = discoveredServices.value.find(s => s.provider === identifier)
|
|
const discoveredJson = discovered?.toJson()
|
|
const service = createServiceObject(identifier, {
|
|
...discoveredJson,
|
|
label: discoveredJson?.label || discoverAddress.value,
|
|
enabled: discoveredJson?.enabled ?? true,
|
|
primaryAddress: discoveredJson?.primaryAddress || discoverAddress.value,
|
|
location: discoveredJson?.location ?? null
|
|
})
|
|
|
|
setSelectedProviderAndService(identifier, service)
|
|
isManualMode.value = true
|
|
|
|
// Manual path: Entry → Discovery → Config → Auth → Test
|
|
currentStep.value = MANUAL_STEPS.CONFIG // Go to config step
|
|
}
|
|
|
|
function handleManualMode() {
|
|
// User clicked "Manual Configuration" - show provider picker
|
|
isManualMode.value = true
|
|
discoveredServices.value = []
|
|
discoveryStatus.value = {}
|
|
currentStep.value = MANUAL_STEPS.PROVIDER_SELECT // Go to provider selection
|
|
}
|
|
|
|
function handleProviderManualSelect(identifier: string) {
|
|
// User selected a provider in manual mode
|
|
const service = createServiceObject(identifier, {
|
|
label: discoverAddress.value,
|
|
enabled: true,
|
|
primaryAddress: discoverAddress.value,
|
|
location: null,
|
|
identity: null
|
|
})
|
|
|
|
setSelectedProviderAndService(identifier, service)
|
|
currentStep.value = MANUAL_STEPS.CONFIG // Go to manual config
|
|
}
|
|
|
|
function goBackToIdentity() {
|
|
currentStep.value = DISCOVERY_STEPS.ENTRY
|
|
isManualMode.value = false
|
|
discoveredServices.value = []
|
|
discoveryStatus.value = {}
|
|
selectedProvider.value = null
|
|
selectedService.value = null
|
|
testAndSaveValid.value = false
|
|
}
|
|
|
|
async function testConnection() {
|
|
if (!selectedProvider.value || !selectedService.value) {
|
|
return {
|
|
success: false,
|
|
message: 'Missing configuration'
|
|
}
|
|
}
|
|
|
|
const serviceData = selectedService.value.toJson()
|
|
if (!serviceData.location || !serviceData.identity) {
|
|
return {
|
|
success: false,
|
|
message: 'Missing configuration'
|
|
}
|
|
}
|
|
|
|
const testResult = await servicesStore.test(
|
|
selectedProvider.value.identifier,
|
|
null,
|
|
serviceData.location,
|
|
serviceData.identity
|
|
)
|
|
|
|
return testResult
|
|
}
|
|
|
|
async function saveAccount() {
|
|
if (!selectedProvider.value || !selectedService.value) return
|
|
|
|
const serviceData = selectedService.value.toJson()
|
|
if (!serviceData.location || !serviceData.identity) return
|
|
|
|
saving.value = true
|
|
|
|
try {
|
|
const accountData = {
|
|
label: serviceData.label || discoverAddress.value,
|
|
primaryAddress: serviceData.primaryAddress || discoverAddress.value,
|
|
enabled: serviceData.enabled,
|
|
location: serviceData.location,
|
|
identity: serviceData.identity,
|
|
auxiliary: serviceData.auxiliary
|
|
}
|
|
|
|
await servicesStore.create(
|
|
selectedProvider.value.identifier,
|
|
accountData
|
|
)
|
|
|
|
emit('saved')
|
|
close()
|
|
} catch (error) {
|
|
console.error('Failed to save account:', error)
|
|
// TODO: Show error message to user
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
function close() {
|
|
dialogOpen.value = false
|
|
// Reset state after animation
|
|
setTimeout(resetForm, 300)
|
|
}
|
|
|
|
function resetForm() {
|
|
currentStep.value = DISCOVERY_STEPS.ENTRY
|
|
isManualMode.value = false
|
|
discoverAddress.value = ''
|
|
discoverSecret.value = null
|
|
discoverHostname.value = null
|
|
selectedProvider.value = null
|
|
selectedService.value = null
|
|
testAndSaveValid.value = false
|
|
discoveredServices.value = []
|
|
discoveryStatus.value = {}
|
|
}
|
|
</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">Add Mail Account</span>
|
|
<v-btn
|
|
icon="mdi-close"
|
|
variant="text"
|
|
@click="close"
|
|
/>
|
|
</v-card-title>
|
|
|
|
<v-divider />
|
|
|
|
<v-card-text class="pa-0">
|
|
<v-stepper
|
|
v-model="currentStep"
|
|
:items="stepperItems"
|
|
alt-labels
|
|
flat
|
|
hide-actions
|
|
>
|
|
<!-- Step 1: Discovery Entry -->
|
|
<template #item.1>
|
|
<v-card flat class="pa-6">
|
|
<DiscoveryEntryPanel
|
|
v-model:address="discoverAddress"
|
|
v-model:secret="discoverSecret"
|
|
v-model:hostname="discoverHostname"
|
|
@discover="handleDiscover"
|
|
@manual="handleManualMode"
|
|
/>
|
|
</v-card>
|
|
</template>
|
|
|
|
<!-- Step 2: Discovery Status OR Provider Selection -->
|
|
<template #item.2>
|
|
<v-card flat class="pa-6">
|
|
<!-- Discovery path -->
|
|
<DiscoveryStatusPanel
|
|
v-if="!isManualMode"
|
|
:address="discoverAddress"
|
|
:status="discoveryStatus"
|
|
@select="handleProviderSelect"
|
|
@advanced="handleProviderAdvanced"
|
|
@manual="handleManualMode"
|
|
@back="goBackToIdentity"
|
|
/>
|
|
|
|
<!-- Manual path - provider picker -->
|
|
<ProviderSelectionPanel
|
|
v-else
|
|
@select="handleProviderManualSelect"
|
|
@back="goBackToIdentity"
|
|
/>
|
|
</v-card>
|
|
</template>
|
|
|
|
<!-- Step 3: Config (manual) OR Auth (discovery) -->
|
|
<template #item.3>
|
|
<v-card flat class="pa-6">
|
|
<!-- Manual path: Protocol Configuration -->
|
|
<ProviderProtocolPanel
|
|
v-if="isManualMode && selectedProvider && selectedService"
|
|
:provider="selectedProvider"
|
|
:service="selectedService"
|
|
@update:service="handleServiceUpdate"
|
|
/>
|
|
|
|
<ProviderAuthPanel
|
|
v-else-if="!isManualMode && selectedProvider && selectedService"
|
|
:provider="selectedProvider"
|
|
:service="selectedService"
|
|
@update:service="handleServiceUpdate"
|
|
/>
|
|
</v-card>
|
|
</template>
|
|
|
|
<!-- Step 4: Auth (manual) OR Test (discovery) -->
|
|
<template #item.4>
|
|
<v-card flat class="pa-6">
|
|
<ProviderAuthPanel
|
|
v-if="isManualMode && selectedProvider && selectedService"
|
|
:provider="selectedProvider"
|
|
:service="selectedService"
|
|
@update:service="handleServiceUpdate"
|
|
/>
|
|
|
|
<!-- Discovery path: Test & Save -->
|
|
<TestAndSavePanel
|
|
v-else-if="!isManualMode && selectedProvider && selectedService"
|
|
:provider="selectedProvider"
|
|
:service="selectedService"
|
|
:on-test="testConnection"
|
|
@update:service="handleServiceUpdate"
|
|
@tested="handleServiceTested"
|
|
/>
|
|
</v-card>
|
|
</template>
|
|
|
|
<!-- Step 5: Test & Save (manual only) -->
|
|
<template #item.5>
|
|
<v-card flat class="pa-6">
|
|
<TestAndSavePanel
|
|
v-if="selectedProvider && selectedService"
|
|
:provider="selectedProvider"
|
|
:service="selectedService"
|
|
:on-test="testConnection"
|
|
@update:service="handleServiceUpdate"
|
|
@tested="handleServiceTested"
|
|
/>
|
|
</v-card>
|
|
</template>
|
|
</v-stepper>
|
|
</v-card-text>
|
|
|
|
<v-divider />
|
|
|
|
<v-card-actions class="pa-6">
|
|
<!-- Previous Button -->
|
|
<v-btn
|
|
v-if="currentStep > 1"
|
|
variant="text"
|
|
prepend-icon="mdi-arrow-left"
|
|
@click="handlePreviousStep"
|
|
>
|
|
Previous
|
|
</v-btn>
|
|
|
|
<v-spacer />
|
|
|
|
<v-btn
|
|
variant="text"
|
|
@click="close"
|
|
>
|
|
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"
|
|
color="primary"
|
|
:loading="saving"
|
|
:disabled="!canSave"
|
|
@click="saveAccount"
|
|
>
|
|
<v-icon start>mdi-content-save</v-icon>
|
|
Save Account
|
|
</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
</template> |