Initial commit

This commit is contained in:
2026-02-10 19:39:08 -05:00
commit 2a251f9b3f
32 changed files with 6135 additions and 0 deletions

View File

@@ -0,0 +1,253 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useCollectionsStore } from '@MailManager/stores/collectionsStore'
import { CollectionPropertiesObject } from '@MailManager/models/collection'
import type { CollectionObject } from '@MailManager/models/collection'
import type { ServiceInterface } from '@MailManager/types/service'
// Props
interface Props {
modelValue: boolean
service: ServiceInterface
parentFolder?: CollectionObject | null
allFolders?: CollectionObject[]
}
const props = withDefaults(defineProps<Props>(), {
parentFolder: null,
allFolders: () => []
})
// Emits
const emit = defineEmits<{
'update:modelValue': [value: boolean]
'created': [folder: CollectionObject]
}>()
// Store
const collectionsStore = useCollectionsStore()
// Form state
const folderName = ref('')
const loading = ref(false)
const errorMessage = ref('')
const validationErrors = ref<string[]>([])
// Computed
const dialogValue = computed({
get: () => props.modelValue,
set: (value: boolean) => emit('update:modelValue', value)
})
const parentFolderLabel = computed(() => {
if (!props.parentFolder) return 'Root'
return props.parentFolder.properties.label
})
const isValid = computed(() => {
return folderName.value.trim().length > 0 && validationErrors.value.length === 0
})
// Validation functions
const validateFolderName = (name: string): string[] => {
const errors: string[] = []
if (!name || name.trim().length === 0) {
errors.push('Folder name is required')
return errors
}
if (name.length > 255) {
errors.push('Folder name too long (max 255 characters)')
}
// No special characters that might cause issues
if (/[<>:"|?*\x00-\x1F]/.test(name)) {
errors.push('Folder name contains invalid characters')
}
// Provider-specific rules
if (props.service.provider === 'imap' && /[\/\\]/.test(name)) {
errors.push('IMAP folder names cannot contain / or \\')
}
// Leading/trailing spaces
if (name !== name.trim()) {
errors.push('Folder name cannot have leading or trailing spaces')
}
return errors
}
const checkDuplicateName = (name: string): boolean => {
const parentId = props.parentFolder?.identifier ?? null
return props.allFolders.some(f =>
f.properties.label === name &&
String(f.collection) === String(parentId) &&
String(f.service) === String(props.service.identifier)
)
}
// Watch folder name for validation
watch(folderName, (newName) => {
errorMessage.value = ''
validationErrors.value = validateFolderName(newName)
// Check for duplicates only if no other validation errors
if (validationErrors.value.length === 0 && newName.trim().length > 0) {
if (checkDuplicateName(newName)) {
validationErrors.value.push('A folder with this name already exists in this location')
}
}
})
// Reset form when dialog opens/closes
watch(dialogValue, (isOpen) => {
if (isOpen) {
resetForm()
}
})
const resetForm = () => {
folderName.value = ''
errorMessage.value = ''
validationErrors.value = []
loading.value = false
}
const handleCreate = async () => {
// Final validation
const errors = validateFolderName(folderName.value)
if (errors.length > 0) {
validationErrors.value = errors
return
}
if (checkDuplicateName(folderName.value)) {
validationErrors.value = ['A folder with this name already exists in this location']
return
}
loading.value = true
errorMessage.value = ''
try {
// Create properties object
const properties = new CollectionPropertiesObject()
properties.label = folderName.value.trim()
properties.rank = 0
properties.subscribed = true
// Create the collection
const newFolder = await collectionsStore.createCollection({
provider: props.service.provider,
service: props.service.identifier,
collection: props.parentFolder?.identifier ?? null,
properties: properties
})
// Success!
emit('created', newFolder)
dialogValue.value = false
resetForm()
} catch (error: any) {
console.error('[CreateFolderDialog] Failed to create folder:', error)
errorMessage.value = error.message || 'Failed to create folder. Please try again.'
} finally {
loading.value = false
}
}
const handleCancel = () => {
dialogValue.value = false
resetForm()
}
</script>
<template>
<v-dialog
v-model="dialogValue"
max-width="500"
persistent
>
<v-card>
<v-card-title class="text-h5">
Create New Folder
</v-card-title>
<v-card-text>
<!-- Service info -->
<div class="mb-4">
<div class="text-caption text-medium-emphasis">Account</div>
<div class="text-body-2">
{{ service.label || service.primaryAddress || 'Mail Account' }}
</div>
</div>
<!-- Parent folder info -->
<div class="mb-4">
<div class="text-caption text-medium-emphasis">Location</div>
<div class="text-body-2">
{{ parentFolderLabel }}
</div>
</div>
<!-- Folder name input -->
<v-text-field
v-model="folderName"
label="Folder Name"
placeholder="Enter folder name"
variant="outlined"
autofocus
:error-messages="validationErrors"
:disabled="loading"
@keyup.enter="isValid && handleCreate()"
/>
<!-- Error message -->
<v-alert
v-if="errorMessage"
type="error"
variant="tonal"
density="compact"
class="mt-2"
>
{{ errorMessage }}
</v-alert>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
variant="text"
:disabled="loading"
@click="handleCancel"
>
Cancel
</v-btn>
<v-btn
color="primary"
variant="elevated"
:loading="loading"
:disabled="!isValid"
@click="handleCreate"
>
Create Folder
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<style scoped>
.text-caption {
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.0333em;
}
</style>