Initial commit
This commit is contained in:
@@ -0,0 +1,568 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useServicesStore } from '@MailManager/stores/servicesStore'
|
||||
import { useProvidersStore } from '@MailManager/stores/providersStore'
|
||||
import type { ProviderDiscoveryStatus, ServiceLocation, ServiceIdentity } from '@MailManager/types'
|
||||
import type { ServiceObject } from '@MailManager/models/service'
|
||||
import DiscoveryStatusStep from '@MailManager/components/steps/DiscoveryStatusStep.vue'
|
||||
import ProviderSelectionStep from '@MailManager/components/steps/ProviderSelectionStep.vue'
|
||||
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 DiscoveryEntryStep from '@MailManager/components/steps/DiscoveryEntryStep.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 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 selectedProviderId = ref<string | undefined>(undefined)
|
||||
const selectedProviderLabel = ref<string>('')
|
||||
|
||||
// Step 3: Config (manual only) OR Auth (both paths)
|
||||
const configuredLocation = ref<ServiceLocation | null>(null)
|
||||
|
||||
// Step 4: Auth (both paths)
|
||||
const configuredIdentity = ref<ServiceIdentity | null>(null)
|
||||
const authValid = ref(false)
|
||||
|
||||
// Step 5: Test & Save
|
||||
const accountLabel = ref<string>('')
|
||||
const accountEnabled = ref(true)
|
||||
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 !!configuredLocation.value
|
||||
}
|
||||
if (currentStep.value === MANUAL_STEPS.AUTH) {
|
||||
return authValid.value
|
||||
}
|
||||
} else {
|
||||
if (currentStep.value === DISCOVERY_STEPS.AUTH) {
|
||||
return authValid.value
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
// 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 {
|
||||
const services = await servicesStore.discover(
|
||||
discoverAddress.value,
|
||||
discoverSecret.value || undefined,
|
||||
discoverHostname.value || undefined,
|
||||
identifier
|
||||
)
|
||||
|
||||
// Success - check if we got results for this provider
|
||||
const service = services.find(s => s.provider === identifier)
|
||||
if (service && service.location) {
|
||||
discoveryStatus.value[identifier] = {
|
||||
provider: identifier,
|
||||
status: 'success',
|
||||
location: service.location,
|
||||
metadata: extractLocationMetadata(service.location)
|
||||
}
|
||||
discoveredServices.value.push(service)
|
||||
} 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 service = discoveredServices.value.find(s => s.provider === identifier)
|
||||
if (!service || !service.location) return
|
||||
|
||||
selectedProviderId.value = identifier
|
||||
selectedProviderLabel.value = providersStore.provider(identifier)?.label || identifier
|
||||
configuredLocation.value = service.location
|
||||
|
||||
// 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
|
||||
selectedProviderId.value = identifier
|
||||
selectedProviderLabel.value = providersStore.provider(identifier)?.label || identifier
|
||||
const service = discoveredServices.value.find(s => s.provider === identifier)
|
||||
|
||||
configuredLocation.value = service?.location || null
|
||||
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
|
||||
selectedProviderId.value = identifier
|
||||
selectedProviderLabel.value = providersStore.provider(identifier)?.label || identifier
|
||||
currentStep.value = MANUAL_STEPS.CONFIG // Go to manual config
|
||||
}
|
||||
|
||||
function goBackToIdentity() {
|
||||
currentStep.value = DISCOVERY_STEPS.ENTRY
|
||||
isManualMode.value = false
|
||||
discoveredServices.value = []
|
||||
discoveryStatus.value = {}
|
||||
}
|
||||
|
||||
async function testConnection() {
|
||||
if (!selectedProviderId.value || !configuredLocation.value || !configuredIdentity.value) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Missing configuration'
|
||||
}
|
||||
}
|
||||
|
||||
const testResult = await servicesStore.test(
|
||||
selectedProviderId.value,
|
||||
null,
|
||||
configuredLocation.value,
|
||||
configuredIdentity.value
|
||||
)
|
||||
|
||||
return testResult
|
||||
}
|
||||
|
||||
async function saveAccount() {
|
||||
if (!selectedProviderId.value || !configuredLocation.value || !configuredIdentity.value) return
|
||||
|
||||
saving.value = true
|
||||
|
||||
try {
|
||||
const accountData = {
|
||||
label: accountLabel.value || discoverAddress.value,
|
||||
email: discoverAddress.value,
|
||||
enabled: accountEnabled.value,
|
||||
location: configuredLocation.value,
|
||||
identity: configuredIdentity.value
|
||||
}
|
||||
|
||||
await servicesStore.create(
|
||||
selectedProviderId.value,
|
||||
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
|
||||
selectedProviderId.value = undefined
|
||||
selectedProviderLabel.value = ''
|
||||
configuredLocation.value = null
|
||||
configuredIdentity.value = null
|
||||
authValid.value = false
|
||||
accountLabel.value = ''
|
||||
accountEnabled.value = true
|
||||
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">
|
||||
<DiscoveryEntryStep
|
||||
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 -->
|
||||
<DiscoveryStatusStep
|
||||
v-if="!isManualMode"
|
||||
:address="discoverAddress"
|
||||
:status="discoveryStatus"
|
||||
@select="handleProviderSelect"
|
||||
@advanced="handleProviderAdvanced"
|
||||
@manual="handleManualMode"
|
||||
@back="goBackToIdentity"
|
||||
/>
|
||||
|
||||
<!-- Manual path - provider picker -->
|
||||
<ProviderSelectionStep
|
||||
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 -->
|
||||
<ProviderConfigStep
|
||||
v-if="isManualMode && selectedProviderId"
|
||||
:provider-id="selectedProviderId"
|
||||
:discovered-location="configuredLocation || undefined"
|
||||
v-model="configuredLocation"
|
||||
@valid="() => { /* Can proceed to next step */ }"
|
||||
/>
|
||||
|
||||
<ProviderAuthStep
|
||||
v-else-if="!isManualMode && selectedProviderId"
|
||||
:provider-id="selectedProviderId"
|
||||
:provider-label="selectedProviderLabel"
|
||||
:email-address="discoverAddress"
|
||||
:discovered-location="configuredLocation || undefined"
|
||||
:prefilled-identity="discoverAddress"
|
||||
:prefilled-secret="discoverSecret || undefined"
|
||||
v-model="configuredIdentity"
|
||||
@valid="(valid) => authValid = valid"
|
||||
/>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<!-- Step 4: Auth (manual) OR Test (discovery) -->
|
||||
<template #item.4>
|
||||
<v-card flat class="pa-6">
|
||||
<ProviderAuthStep
|
||||
v-if="isManualMode && selectedProviderId"
|
||||
:provider-id="selectedProviderId"
|
||||
:provider-label="selectedProviderLabel"
|
||||
:email-address="discoverAddress"
|
||||
:discovered-location="configuredLocation || undefined"
|
||||
:prefilled-identity="discoverAddress"
|
||||
:prefilled-secret="discoverSecret || undefined"
|
||||
v-model="configuredIdentity"
|
||||
@valid="(valid) => authValid = valid"
|
||||
/>
|
||||
|
||||
<!-- Discovery path: Test & Save -->
|
||||
<TestAndSaveStep
|
||||
v-else-if="!isManualMode && selectedProviderId"
|
||||
:provider-id="selectedProviderId"
|
||||
:provider-label="selectedProviderLabel"
|
||||
:email-address="discoverAddress"
|
||||
:location="configuredLocation"
|
||||
:identity="configuredIdentity"
|
||||
:prefilled-label="discoverAddress"
|
||||
:on-test="testConnection"
|
||||
@update:label="(val) => accountLabel = val"
|
||||
@update:enabled="(val) => accountEnabled = val"
|
||||
@valid="(valid) => testAndSaveValid = valid"
|
||||
/>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<!-- Step 5: Test & Save (manual only) -->
|
||||
<template #item.5>
|
||||
<v-card flat class="pa-6">
|
||||
<TestAndSaveStep
|
||||
v-if="selectedProviderId"
|
||||
:provider-id="selectedProviderId"
|
||||
:provider-label="selectedProviderLabel"
|
||||
:email-address="discoverAddress"
|
||||
:location="configuredLocation"
|
||||
:identity="configuredIdentity"
|
||||
:prefilled-label="discoverAddress"
|
||||
:on-test="testConnection"
|
||||
@update:label="(val) => accountLabel = val"
|
||||
@update:enabled="(val) => accountEnabled = val"
|
||||
@valid="(valid) => testAndSaveValid = valid"
|
||||
/>
|
||||
</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>
|
||||
@@ -0,0 +1,355 @@
|
||||
<script setup lang="ts">
|
||||
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'
|
||||
|
||||
const EDIT_STEPS = {
|
||||
CONFIG: 1,
|
||||
AUTH: 2,
|
||||
TEST: 3
|
||||
} as const
|
||||
|
||||
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 currentStep = ref<number>(EDIT_STEPS.CONFIG)
|
||||
const saving = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
// 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)
|
||||
|
||||
// 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()
|
||||
}
|
||||
})
|
||||
|
||||
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 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'
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
await servicesStore.update(
|
||||
service.value.provider,
|
||||
service.value.identifier as string | number,
|
||||
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 = EDIT_STEPS.CONFIG
|
||||
service.value = null
|
||||
accountLabel.value = ''
|
||||
accountEnabled.value = true
|
||||
configuredLocation.value = null
|
||||
configuredIdentity.value = null
|
||||
configValid.value = false
|
||||
authValid.value = false
|
||||
testAndSaveValid.value = false
|
||||
}
|
||||
|
||||
// Watch for location changes
|
||||
watch(configuredLocation, (newLocation) => {
|
||||
configValid.value = !!newLocation
|
||||
})
|
||||
</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" 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 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-divider />
|
||||
|
||||
<v-stepper
|
||||
v-model="currentStep"
|
||||
:items="stepperItems"
|
||||
alt-labels
|
||||
flat
|
||||
hide-actions
|
||||
>
|
||||
<!-- Step 1: Protocol Configuration -->
|
||||
<template #item.1>
|
||||
<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"
|
||||
:on-test="testConnection"
|
||||
@update:label="(val) => accountLabel = val"
|
||||
@update:enabled="(val) => accountEnabled = val"
|
||||
@valid="(valid) => testAndSaveValid = valid"
|
||||
/>
|
||||
</v-card>
|
||||
</template>
|
||||
</v-stepper>
|
||||
</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
|
||||
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 Changes
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
@@ -0,0 +1,158 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
address: string
|
||||
secret?: string | null
|
||||
hostname?: string | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:address': [value: string]
|
||||
'update:secret': [value: string | null]
|
||||
'update:hostname': [value: string | null]
|
||||
'discover': []
|
||||
'manual': []
|
||||
}>()
|
||||
|
||||
const localAddress = computed({
|
||||
get: () => props.address,
|
||||
set: (value) => emit('update:address', value?.trim())
|
||||
})
|
||||
|
||||
const localSecret = computed({
|
||||
get: () => props.secret,
|
||||
set: (value) => emit('update:secret', value?.trim() || null)
|
||||
})
|
||||
|
||||
const localHostname = computed({
|
||||
get: () => props.hostname || '',
|
||||
set: (value) => emit('update:hostname', value?.trim() || null)
|
||||
})
|
||||
|
||||
const showSecret = ref(false)
|
||||
|
||||
const rules = {
|
||||
required: (v: string) => !!v || 'Required',
|
||||
email: (v: string) => /.+@.+\..+/.test(v) || 'Invalid email address'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="identity-entry-step">
|
||||
<h3 class="text-h6 mb-2">Email Account Setup</h3>
|
||||
<p class="text-body-2 text-medium-emphasis mb-6">
|
||||
Enter your email address to automatically discover your mail server settings.
|
||||
</p>
|
||||
|
||||
<v-text-field
|
||||
v-model="localAddress"
|
||||
label="Email Address"
|
||||
type="email"
|
||||
prepend-inner-icon="mdi-email"
|
||||
variant="outlined"
|
||||
required
|
||||
autocomplete="off"
|
||||
:rules="[rules.required, rules.email]"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<!-- Advanced Options -->
|
||||
<v-expansion-panels variant="accordion" class="mb-6">
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-title>
|
||||
<v-icon start>mdi-cog</v-icon>
|
||||
Advanced Options (Optional)
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
|
||||
<v-text-field
|
||||
v-model="localSecret"
|
||||
:type="showSecret ? 'text' : 'password'"
|
||||
label="Password (Optional)"
|
||||
hint="Provide password to validate credentials during discovery"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-lock"
|
||||
variant="outlined"
|
||||
autocomplete="new-password"
|
||||
class="mb-4"
|
||||
>
|
||||
<template #append-inner>
|
||||
<v-btn
|
||||
:icon="showSecret ? 'mdi-eye-off' : 'mdi-eye'"
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="showSecret = !showSecret"
|
||||
/>
|
||||
</template>
|
||||
</v-text-field>
|
||||
|
||||
<!-- Info -->
|
||||
<v-alert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
class="mt-6"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-information</v-icon>
|
||||
</template>
|
||||
<div class="text-caption">
|
||||
Your credentials are used only to discover and test server settings.
|
||||
They are transmitted securely and not stored during discovery.
|
||||
</div>
|
||||
</v-alert>
|
||||
|
||||
<v-text-field
|
||||
v-model="localHostname"
|
||||
label="Server Hostname"
|
||||
hint="If you know your mail server hostname, enter it here to skip DNS lookup (e.g., mail.example.com)"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-server"
|
||||
variant="outlined"
|
||||
placeholder="mail.example.com"
|
||||
clearable
|
||||
/>
|
||||
|
||||
<v-alert type="info" variant="tonal" density="compact" class="mt-3">
|
||||
<template #prepend>
|
||||
<v-icon size="small">mdi-information</v-icon>
|
||||
</template>
|
||||
<div class="text-caption">
|
||||
Providing a hostname will skip DNS SRV lookups and test the server directly.
|
||||
Leave blank for automatic discovery.
|
||||
</div>
|
||||
</v-alert>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="d-flex flex-column gap-3">
|
||||
<v-btn
|
||||
color="primary"
|
||||
size="large"
|
||||
block
|
||||
:disabled="!localAddress || !rules.email(localAddress)"
|
||||
@click="$emit('discover')"
|
||||
>
|
||||
<v-icon start>mdi-magnify</v-icon>
|
||||
Discover Settings
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
variant="text"
|
||||
block
|
||||
@click="$emit('manual')"
|
||||
>
|
||||
Manual Configuration
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.gap-3 {
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,220 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { ProviderDiscoveryStatus } from '@MailManager/types/service'
|
||||
|
||||
const props = defineProps<{
|
||||
address: string
|
||||
status: Record<string, ProviderDiscoveryStatus>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'select': [providerId: string]
|
||||
'advanced': [providerId: string]
|
||||
'manual': []
|
||||
'back': []
|
||||
}>()
|
||||
|
||||
const sortedStatus = computed(() => {
|
||||
const statusArray = Object.values(props.status)
|
||||
const order = { success: 0, discovering: 1, pending: 2, failed: 3 }
|
||||
return statusArray.sort((a, b) => order[a.status] - order[b.status])
|
||||
})
|
||||
|
||||
const progressPercent = computed(() => {
|
||||
const total = Object.keys(props.status).length
|
||||
if (total === 0) return 0
|
||||
|
||||
const completed = Object.values(props.status).filter(
|
||||
s => s.status === 'success' || s.status === 'failed'
|
||||
).length
|
||||
|
||||
return (completed / total) * 100
|
||||
})
|
||||
|
||||
const isDiscovering = computed(() => {
|
||||
return Object.values(props.status).some(
|
||||
s => s.status === 'discovering' || s.status === 'pending'
|
||||
)
|
||||
})
|
||||
|
||||
const successCount = computed(() => {
|
||||
return Object.values(props.status).filter(s => s.status === 'success').length
|
||||
})
|
||||
|
||||
function getStatusColor(status: string): string {
|
||||
const colors: Record<string, string> = {
|
||||
success: 'success',
|
||||
failed: 'error',
|
||||
discovering: 'primary',
|
||||
pending: 'grey'
|
||||
}
|
||||
return colors[status] || 'grey'
|
||||
}
|
||||
|
||||
function getProviderLabel(providerId: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
jmap: 'JMAP',
|
||||
smtp: 'SMTP/IMAP',
|
||||
imap: 'IMAP',
|
||||
exchange: 'Microsoft Exchange'
|
||||
}
|
||||
return labels[providerId] || providerId.toUpperCase()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="discovery-status-step">
|
||||
<h3 class="text-h6 mb-2">Discovering Mail Servers</h3>
|
||||
<p class="text-body-2 text-medium-emphasis mb-6">
|
||||
Testing {{ address }} across multiple providers...
|
||||
</p>
|
||||
|
||||
<!-- Overall Progress -->
|
||||
<v-progress-linear
|
||||
:model-value="progressPercent"
|
||||
color="primary"
|
||||
height="8"
|
||||
rounded
|
||||
class="mb-6"
|
||||
/>
|
||||
|
||||
<!-- Provider Status List -->
|
||||
<v-list class="provider-status-list">
|
||||
<v-list-item
|
||||
v-for="status in sortedStatus"
|
||||
:key="status.provider"
|
||||
class="provider-status-item mb-2"
|
||||
:class="`status-${status.status}`"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-avatar :color="getStatusColor(status.status)" size="40">
|
||||
<v-icon v-if="status.status === 'success'" color="white">
|
||||
mdi-check
|
||||
</v-icon>
|
||||
<v-icon v-else-if="status.status === 'failed'" color="white">
|
||||
mdi-close
|
||||
</v-icon>
|
||||
<v-progress-circular
|
||||
v-else-if="status.status === 'discovering'"
|
||||
indeterminate
|
||||
size="24"
|
||||
width="3"
|
||||
color="white"
|
||||
/>
|
||||
<v-icon v-else color="white">
|
||||
mdi-clock-outline
|
||||
</v-icon>
|
||||
</v-avatar>
|
||||
</template>
|
||||
|
||||
<v-list-item-title class="d-flex align-center gap-2">
|
||||
<span class="font-weight-medium">{{ getProviderLabel(status.provider) }}</span>
|
||||
<v-chip
|
||||
v-if="status.status === 'success'"
|
||||
size="x-small"
|
||||
color="success"
|
||||
variant="flat"
|
||||
>
|
||||
Found
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-else-if="status.status === 'failed'"
|
||||
size="x-small"
|
||||
color="error"
|
||||
variant="flat"
|
||||
>
|
||||
{{ status.error || 'Failed' }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-else-if="status.status === 'discovering'"
|
||||
size="x-small"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
>
|
||||
Testing...
|
||||
</v-chip>
|
||||
</v-list-item-title>
|
||||
|
||||
<v-list-item-subtitle v-if="status.status === 'success' && status.metadata">
|
||||
<div class="text-caption">
|
||||
<v-icon size="14" class="mr-1">mdi-server</v-icon>
|
||||
{{ status.metadata.host }}{{ status.metadata.port ? ':' + status.metadata.port : '' }}
|
||||
<span v-if="status.metadata.protocol" class="ml-2">
|
||||
<v-icon size="14" class="mr-1">mdi-shield-lock</v-icon>
|
||||
{{ status.metadata.protocol.toUpperCase() }}
|
||||
</span>
|
||||
</div>
|
||||
</v-list-item-subtitle>
|
||||
|
||||
<template #append>
|
||||
<div v-if="status.status === 'success'" class="d-flex gap-2">
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
prepend-icon="mdi-check"
|
||||
@click="$emit('select', status.provider)"
|
||||
>
|
||||
Select
|
||||
</v-btn>
|
||||
<v-tooltip text="Advanced configuration">
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
icon="mdi-tune"
|
||||
@click="$emit('advanced', status.provider)"
|
||||
/>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<!-- No Results Message -->
|
||||
<v-alert
|
||||
v-if="!isDiscovering && successCount === 0"
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
class="mt-6"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-alert</v-icon>
|
||||
</template>
|
||||
<div>
|
||||
<div class="font-weight-medium mb-1">No configurations found</div>
|
||||
<div class="text-caption">
|
||||
We couldn't automatically discover server settings for {{ address }}.
|
||||
You can try manual configuration instead.
|
||||
</div>
|
||||
</div>
|
||||
</v-alert>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.provider-status-item {
|
||||
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.provider-status-item.status-success {
|
||||
background-color: rgba(var(--v-theme-success), 0.05);
|
||||
}
|
||||
|
||||
.provider-status-item.status-discovering {
|
||||
background-color: rgba(var(--v-theme-primary), 0.05);
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.gap-3 {
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<div class="provider-auth-step">
|
||||
<h3 class="text-h6 mb-2">Authentication</h3>
|
||||
<p class="text-body-2 text-medium-emphasis mb-6">
|
||||
Configure authentication for {{ providerLabel }}
|
||||
</p>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="loadingPanel" class="text-center py-8">
|
||||
<v-progress-circular indeterminate color="primary" />
|
||||
<p class="text-caption text-medium-emphasis mt-2">
|
||||
Loading authentication panel...
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Dynamic Provider Auth Panel -->
|
||||
<component
|
||||
v-else-if="currentAuthPanel"
|
||||
:is="currentAuthPanel"
|
||||
:email-address="emailAddress"
|
||||
:discovered-location="discoveredLocation"
|
||||
:prefilled-identity="prefilledIdentity"
|
||||
:prefilled-secret="prefilledSecret"
|
||||
v-model="localIdentity"
|
||||
@update:model-value="handleIdentityUpdate"
|
||||
@valid="handleValidChange"
|
||||
@error="handleAuthError"
|
||||
/>
|
||||
|
||||
<!-- No Panel Available -->
|
||||
<v-alert v-else type="error" variant="tonal">
|
||||
<v-icon start>mdi-alert-circle</v-icon>
|
||||
No authentication method available for this provider.
|
||||
Please contact support.
|
||||
</v-alert>
|
||||
|
||||
<!-- Error Display -->
|
||||
<v-alert
|
||||
v-if="authError"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
class="mt-4"
|
||||
closable
|
||||
@click:close="authError = ''"
|
||||
>
|
||||
<v-icon start>mdi-alert</v-icon>
|
||||
{{ authError }}
|
||||
</v-alert>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useIntegrationStore } from '@KTXC/stores/integrationStore'
|
||||
import type { ServiceIdentity, ServiceLocation } from '@MailManager/types/service'
|
||||
|
||||
const props = defineProps<{
|
||||
providerId: string
|
||||
providerLabel: string
|
||||
emailAddress: string
|
||||
discoveredLocation?: ServiceLocation
|
||||
prefilledIdentity?: string
|
||||
prefilledSecret?: string
|
||||
modelValue?: ServiceIdentity | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: ServiceIdentity]
|
||||
'valid': [value: boolean]
|
||||
}>()
|
||||
|
||||
const integrationStore = useIntegrationStore()
|
||||
|
||||
const loadedPanels = new Map<string, any>()
|
||||
const currentAuthPanel = ref<any>(null)
|
||||
const loadingPanel = ref(false)
|
||||
const localIdentity = ref<ServiceIdentity | undefined>(props.modelValue)
|
||||
const authError = ref('')
|
||||
|
||||
// The full integration ID (e.g., "jmap")
|
||||
const effectiveIntegrationId = computed(() => {
|
||||
return props.providerId
|
||||
})
|
||||
|
||||
// Load provider auth panel dynamically
|
||||
async function loadAuthPanel(integrationId: string) {
|
||||
if (loadedPanels.has(integrationId)) {
|
||||
currentAuthPanel.value = loadedPanels.get(integrationId)
|
||||
return
|
||||
}
|
||||
|
||||
loadingPanel.value = true
|
||||
|
||||
// Try to find panel - integration IDs are prefixed with module handle
|
||||
// so we need to search for panels that match the provider ID
|
||||
const panels = integrationStore.getItems('mail_account_auth_panels')
|
||||
const panelConfig = panels.find((panel: any) => {
|
||||
// Check if the ID ends with the provider ID (e.g., "provider_jmapc.jmap" contains "jmap")
|
||||
return panel.id === integrationId || panel.id.endsWith(`.${integrationId}`)
|
||||
})
|
||||
|
||||
if (!panelConfig?.component) {
|
||||
console.error(`No auth panel found for provider ID: ${integrationId}`)
|
||||
console.error(`Available panels:`, panels.map((p: any) => p.id))
|
||||
currentAuthPanel.value = null
|
||||
loadingPanel.value = false
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const module = await panelConfig.component()
|
||||
const component = module.default || module
|
||||
loadedPanels.set(integrationId, component)
|
||||
currentAuthPanel.value = component
|
||||
} catch (error) {
|
||||
console.error(`Failed to load auth panel for ${integrationId}:`, error)
|
||||
currentAuthPanel.value = null
|
||||
authError.value = `Failed to load authentication panel: ${error}`
|
||||
} finally {
|
||||
loadingPanel.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Load panel when provider changes
|
||||
watch(
|
||||
effectiveIntegrationId,
|
||||
(newIntegrationId, oldIntegrationId) => {
|
||||
if (newIntegrationId && newIntegrationId !== oldIntegrationId) {
|
||||
loadAuthPanel(newIntegrationId)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function handleIdentityUpdate(identity: ServiceIdentity) {
|
||||
localIdentity.value = identity
|
||||
emit('update:modelValue', identity)
|
||||
}
|
||||
|
||||
function handleValidChange(valid: boolean) {
|
||||
emit('valid', valid)
|
||||
}
|
||||
|
||||
function handleAuthError(error: string) {
|
||||
authError.value = error
|
||||
}
|
||||
|
||||
// Watch for prop changes
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
localIdentity.value = newValue
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.provider-auth-step {
|
||||
max-width: 800px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,124 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useIntegrationStore } from '@KTXC/stores/integrationStore'
|
||||
import type { ServiceLocation } from '@MailManager/types/service'
|
||||
|
||||
const props = defineProps<{
|
||||
providerId: string
|
||||
discoveredLocation?: ServiceLocation
|
||||
modelValue?: ServiceLocation | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: ServiceLocation]
|
||||
'valid': [value: boolean]
|
||||
}>()
|
||||
|
||||
const integrationStore = useIntegrationStore()
|
||||
|
||||
const loadedPanels = new Map<string, any>()
|
||||
const currentProviderPanel = ref<any>(null)
|
||||
const loadingPanel = ref(false)
|
||||
const localLocation = ref<ServiceLocation | undefined>(props.modelValue || props.discoveredLocation)
|
||||
|
||||
// The full integration ID (e.g., "provider_jmapc.jmap")
|
||||
const effectiveIntegrationId = computed(() => {
|
||||
return props.providerId
|
||||
})
|
||||
|
||||
// Load provider panel dynamically using the integration ID
|
||||
async function loadProviderPanel(integrationId: string) {
|
||||
if (loadedPanels.has(integrationId)) {
|
||||
currentProviderPanel.value = loadedPanels.get(integrationId)
|
||||
return
|
||||
}
|
||||
|
||||
loadingPanel.value = true
|
||||
|
||||
// Try to find panel - integration IDs are prefixed with module handle
|
||||
// so we need to search for panels that match the provider ID
|
||||
const panels = integrationStore.getItems('mail_account_config_panels')
|
||||
const panelConfig = panels.find((panel: any) => {
|
||||
// Check if the ID ends with the provider ID (e.g., "provider_jmapc.jmap" contains "jmap")
|
||||
return panel.id === integrationId || panel.id.endsWith(`.${integrationId}`)
|
||||
})
|
||||
|
||||
if (!panelConfig?.component) {
|
||||
console.warn(`No config panel found for provider ID: ${integrationId}`)
|
||||
console.warn(`Available panels:`, panels.map((p: any) => p.id))
|
||||
currentProviderPanel.value = null
|
||||
loadingPanel.value = false
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const module = await panelConfig.component()
|
||||
const component = module.default || module
|
||||
loadedPanels.set(integrationId, component)
|
||||
currentProviderPanel.value = component
|
||||
} catch (error) {
|
||||
console.error(`Failed to load panel for ${integrationId}:`, error)
|
||||
currentProviderPanel.value = null
|
||||
} finally {
|
||||
loadingPanel.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(effectiveIntegrationId, (newIntegrationId, oldIntegrationId) => {
|
||||
if (newIntegrationId && newIntegrationId !== oldIntegrationId) {
|
||||
loadProviderPanel(newIntegrationId)
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
function handleLocationUpdate(location: ServiceLocation) {
|
||||
localLocation.value = location
|
||||
emit('update:modelValue', location)
|
||||
// Emit valid when location is provided
|
||||
emit('valid', !!location)
|
||||
}
|
||||
|
||||
// Watch for prop changes
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
if (newValue) {
|
||||
localLocation.value = newValue
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.discoveredLocation, (newValue) => {
|
||||
if (newValue && !props.modelValue) {
|
||||
localLocation.value = newValue
|
||||
emit('update:modelValue', newValue)
|
||||
emit('valid', true)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="provider-config-step">
|
||||
<h3 class="text-h6 mb-2">Protocol Configuration</h3>
|
||||
<p class="text-body-2 text-medium-emphasis mb-6">
|
||||
Configure the connection settings for your mail service
|
||||
</p>
|
||||
|
||||
<!-- Dynamic Provider Panel -->
|
||||
<component
|
||||
v-if="currentProviderPanel"
|
||||
:is="currentProviderPanel"
|
||||
v-model="localLocation"
|
||||
:discovered-location="discoveredLocation"
|
||||
@update:model-value="handleLocationUpdate"
|
||||
/>
|
||||
|
||||
<!-- Loading state for panel -->
|
||||
<div v-else-if="loadingPanel" class="text-center py-8">
|
||||
<v-progress-circular indeterminate color="primary" />
|
||||
<p class="text-caption text-medium-emphasis mt-2">Loading configuration panel...</p>
|
||||
</div>
|
||||
|
||||
<!-- No panel available -->
|
||||
<v-alert v-else type="info" variant="tonal">
|
||||
<v-icon start>mdi-information</v-icon>
|
||||
No configuration panel available for this provider
|
||||
</v-alert>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,125 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useProvidersStore } from '@MailManager/stores/providersStore'
|
||||
import { useIntegrationStore } from '@KTXC/stores/integrationStore'
|
||||
|
||||
const emit = defineEmits<{
|
||||
'select': [providerId: string]
|
||||
'back': []
|
||||
}>()
|
||||
|
||||
const providersStore = useProvidersStore()
|
||||
const integrationStore = useIntegrationStore()
|
||||
const selected = ref<string | null>(null)
|
||||
|
||||
// Get provider metadata from integrations
|
||||
const providerMetadata = computed(() => {
|
||||
const metadata = integrationStore.getItems('mail_provider_metadata')
|
||||
return metadata.reduce((acc: any, meta: any) => {
|
||||
acc[meta.id] = meta
|
||||
return acc
|
||||
}, {})
|
||||
})
|
||||
|
||||
// Color palette for providers without specific colors
|
||||
const defaultColors = ['blue', 'green', 'orange', 'purple', 'teal', 'indigo', 'pink', 'cyan']
|
||||
|
||||
// Combine provider data with metadata
|
||||
const availableProviders = computed(() => {
|
||||
return providersStore.providers.map((provider, index) => {
|
||||
const metadata = providerMetadata.value[provider.identifier] || {}
|
||||
|
||||
return {
|
||||
id: provider.identifier,
|
||||
name: provider.label || metadata.label || provider.identifier,
|
||||
description: metadata.description || `${provider.label} mail provider`,
|
||||
icon: metadata.icon || 'mdi-email',
|
||||
color: metadata.color || defaultColors[index % defaultColors.length]
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function selectProvider(providerId: string) {
|
||||
selected.value = providerId
|
||||
emit('select', providerId)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="provider-selection-step">
|
||||
<h3 class="text-h6 mb-2">Select Provider</h3>
|
||||
<p class="text-body-2 text-medium-emphasis mb-6">
|
||||
Choose the mail provider you want to configure manually.
|
||||
</p>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="providersStore.transceiving" class="text-center py-8">
|
||||
<v-progress-circular indeterminate color="primary" />
|
||||
<p class="text-caption text-medium-emphasis mt-2">
|
||||
Loading providers...
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- No Providers Available -->
|
||||
<v-alert
|
||||
v-else-if="availableProviders.length === 0"
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
>
|
||||
<v-icon start>mdi-alert</v-icon>
|
||||
No mail providers are currently available. Please contact your administrator.
|
||||
</v-alert>
|
||||
|
||||
<!-- Provider Grid -->
|
||||
<v-row v-else>
|
||||
<v-col
|
||||
v-for="provider in availableProviders"
|
||||
:key="provider.id"
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="4"
|
||||
>
|
||||
<v-card
|
||||
variant="outlined"
|
||||
hover
|
||||
class="provider-card"
|
||||
:class="{ 'provider-card--selected': selected === provider.id }"
|
||||
@click="selectProvider(provider.id)"
|
||||
>
|
||||
<v-card-text class="text-center pa-6">
|
||||
<v-avatar :color="provider.color" size="64" class="mb-4">
|
||||
<v-icon :icon="provider.icon" size="32" color="white" />
|
||||
</v-avatar>
|
||||
|
||||
<h4 class="text-h6 mb-2">{{ provider.name }}</h4>
|
||||
<p class="text-caption text-medium-emphasis">
|
||||
{{ provider.description }}
|
||||
</p>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.provider-card {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.provider-card:hover {
|
||||
border-color: rgb(var(--v-theme-primary));
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.provider-card--selected {
|
||||
border-color: rgb(var(--v-theme-primary));
|
||||
background-color: rgba(var(--v-theme-primary), 0.05);
|
||||
}
|
||||
|
||||
.gap-3 {
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,295 @@
|
||||
<template>
|
||||
<div class="test-and-save-step">
|
||||
<h3 class="text-h6 mb-2">Test & Save</h3>
|
||||
<p class="text-body-2 text-medium-emphasis mb-6">
|
||||
Test your connection and save the account configuration
|
||||
</p>
|
||||
|
||||
<!-- Configuration Summary -->
|
||||
<v-card variant="outlined" class="mb-4">
|
||||
<v-card-subtitle>Configuration Summary</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-list density="compact" class="bg-transparent">
|
||||
<!-- Account Label -->
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-label</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>Account Name</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ accountLabel }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<!-- Email Address -->
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-email</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>Email Address</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ emailAddress }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<!-- Provider -->
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-cloud</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>Provider</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ providerLabel }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<!-- Location Details -->
|
||||
<template v-if="location">
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<v-list-item v-if="location.type === 'URI'">
|
||||
<template #prepend>
|
||||
<v-icon>mdi-web</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>Service URL</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ location.scheme }}://{{ location.host }}:{{ location.port }}{{ location.path || '' }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<template v-if="location.type === 'SOCKET_SOLE'">
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-server</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>Server</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ location.host }}:{{ location.port }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-shield-lock</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>Security</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ location.encryption.toUpperCase() }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<template v-if="location.type === 'SOCKET_SPLIT'">
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-inbox-arrow-down</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>Incoming Mail</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ location.inbound.protocol.toUpperCase() }} - {{ location.inbound.host }}:{{ location.inbound.port }} ({{ location.inbound.encryption.toUpperCase() }})
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-send</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>Outgoing Mail</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ location.outbound.protocol.toUpperCase() }} - {{ location.outbound.host }}:{{ location.outbound.port }} ({{ location.outbound.encryption.toUpperCase() }})
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- Authentication Method -->
|
||||
<v-divider class="my-2" />
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon>{{ getAuthIcon(identity?.type) }}</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>Authentication</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ getAuthLabel(identity?.type) }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- Account Label Input -->
|
||||
<v-text-field
|
||||
v-model="localAccountLabel"
|
||||
label="Account Name"
|
||||
variant="outlined"
|
||||
hint="A friendly name for this account (e.g., Work Email)"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-label"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<!-- Enable Account Toggle -->
|
||||
<v-switch
|
||||
v-model="accountEnabled"
|
||||
label="Enable this account"
|
||||
color="primary"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<!-- Test Connection -->
|
||||
<div class="mb-4">
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
size="large"
|
||||
block
|
||||
:loading="testing"
|
||||
:disabled="testSuccess"
|
||||
prepend-icon="mdi-connection"
|
||||
@click="handleTest"
|
||||
>
|
||||
{{ testSuccess ? 'Connection Tested Successfully' : 'Test Connection' }}
|
||||
</v-btn>
|
||||
|
||||
<!-- Test Result -->
|
||||
<v-alert
|
||||
v-if="testResult"
|
||||
:type="testResult.success ? 'success' : 'error'"
|
||||
variant="tonal"
|
||||
class="mt-4"
|
||||
>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon
|
||||
:icon="testResult.success ? 'mdi-check-circle' : 'mdi-alert-circle'"
|
||||
class="mr-2"
|
||||
/>
|
||||
<div class="flex-grow-1">
|
||||
<div class="font-weight-bold">{{ testResult.message }}</div>
|
||||
<div v-if="testResult.details?.latency" class="text-caption">
|
||||
Response time: {{ testResult.details.latency }}ms
|
||||
</div>
|
||||
<div v-if="testResult.details?.protocols" class="text-caption">
|
||||
Protocols: {{ Object.keys(testResult.details.protocols).join(', ') }}
|
||||
</div>
|
||||
<div v-if="testResult.details?.capabilities" class="text-caption mt-1">
|
||||
Capabilities: {{ formatCapabilities(testResult.details.capabilities) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-alert>
|
||||
</div>
|
||||
|
||||
<!-- Save Warning -->
|
||||
<v-alert
|
||||
v-if="!testSuccess"
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
>
|
||||
<v-icon start>mdi-alert</v-icon>
|
||||
Please test the connection before saving
|
||||
</v-alert>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import type { ServiceIdentity, ServiceLocation } from '@MailManager/types/service'
|
||||
|
||||
const props = defineProps<{
|
||||
providerId: string
|
||||
providerLabel: string
|
||||
emailAddress: string
|
||||
location: ServiceLocation | null
|
||||
identity: ServiceIdentity | null
|
||||
prefilledLabel?: string
|
||||
onTest: () => Promise<any>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:label': [value: string]
|
||||
'update:enabled': [value: boolean]
|
||||
'tested': [success: boolean]
|
||||
'valid': [value: boolean]
|
||||
}>()
|
||||
|
||||
// Local state
|
||||
const localAccountLabel = ref(props.prefilledLabel || props.emailAddress || '')
|
||||
const accountEnabled = ref(true)
|
||||
const testing = ref(false)
|
||||
const testResult = ref<any>(null)
|
||||
|
||||
// Computed
|
||||
const accountLabel = computed(() => localAccountLabel.value || props.emailAddress || 'New Account')
|
||||
const testSuccess = computed(() => testResult.value?.success === true)
|
||||
|
||||
const isValid = computed(() => {
|
||||
return testSuccess.value && !!localAccountLabel.value
|
||||
})
|
||||
|
||||
// Helper functions
|
||||
function getAuthIcon(type?: string): string {
|
||||
switch (type) {
|
||||
case 'NA': return 'mdi-lock-open-variant'
|
||||
case 'BA': return 'mdi-account-key'
|
||||
case 'TA': return 'mdi-key'
|
||||
case 'OA': return 'mdi-shield-account'
|
||||
case 'CC': return 'mdi-certificate'
|
||||
default: return 'mdi-help-circle'
|
||||
}
|
||||
}
|
||||
|
||||
function getAuthLabel(type?: string): string {
|
||||
switch (type) {
|
||||
case 'NA': return 'No Authentication'
|
||||
case 'BA': return 'Username & Password'
|
||||
case 'TA': return 'API Token'
|
||||
case 'OA': return 'OAuth 2.0'
|
||||
case 'CC': return 'Client Certificate'
|
||||
default: return 'Unknown'
|
||||
}
|
||||
}
|
||||
|
||||
function formatCapabilities(capabilities: any): string {
|
||||
if (!capabilities || typeof capabilities !== 'object') return 'N/A'
|
||||
|
||||
const caps = Object.entries(capabilities)
|
||||
.filter(([_, value]) => value === true)
|
||||
.map(([key]) => key)
|
||||
.slice(0, 5)
|
||||
|
||||
const total = caps.length
|
||||
const display = caps.slice(0, 3).join(', ')
|
||||
|
||||
if (total > 3) {
|
||||
return `${display}, +${total - 3} more`
|
||||
}
|
||||
return display
|
||||
}
|
||||
|
||||
async function handleTest() {
|
||||
if (!props.location || !props.identity) {
|
||||
testResult.value = {
|
||||
success: false,
|
||||
message: 'Missing configuration'
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
testing.value = true
|
||||
testResult.value = null
|
||||
|
||||
try {
|
||||
const result = await props.onTest()
|
||||
testResult.value = result
|
||||
emit('tested', result.success)
|
||||
} catch (error: any) {
|
||||
testResult.value = {
|
||||
success: false,
|
||||
message: error.message || 'Connection test failed'
|
||||
}
|
||||
emit('tested', false)
|
||||
} finally {
|
||||
testing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for changes and emit
|
||||
watch(localAccountLabel, (value) => {
|
||||
emit('update:label', value)
|
||||
})
|
||||
|
||||
watch(accountEnabled, (value) => {
|
||||
emit('update:enabled', value)
|
||||
})
|
||||
|
||||
watch(isValid, (value) => {
|
||||
emit('valid', value)
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
Reference in New Issue
Block a user