feat: collection rename
Signed-off-by: Sebastian <krupinski01@gmail.com>
This commit is contained in:
247
src/components/RenameFolderDialog.vue
Normal file
247
src/components/RenameFolderDialog.vue
Normal file
@@ -0,0 +1,247 @@
|
||||
<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'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
service: ServiceInterface
|
||||
folder: CollectionObject
|
||||
allFolders?: CollectionObject[]
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
allFolders: () => []
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
updated: [folder: CollectionObject]
|
||||
}>()
|
||||
|
||||
const collectionsStore = useCollectionsStore()
|
||||
|
||||
const folderName = ref('')
|
||||
const loading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const validationErrors = ref<string[]>([])
|
||||
|
||||
const dialogValue = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: boolean) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const isValid = computed(() => {
|
||||
return folderName.value.trim().length > 0 && validationErrors.value.length === 0
|
||||
})
|
||||
|
||||
const parentFolderLabel = computed(() => {
|
||||
const parentId = props.folder.collection
|
||||
if (parentId === null || parentId === undefined) return 'Root'
|
||||
|
||||
const parent = props.allFolders.find(
|
||||
f =>
|
||||
String(f.identifier) === String(parentId) &&
|
||||
f.provider === props.folder.provider &&
|
||||
String(f.service) === String(props.folder.service)
|
||||
)
|
||||
|
||||
return parent?.properties.label || 'Root'
|
||||
})
|
||||
|
||||
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)')
|
||||
}
|
||||
|
||||
if (/[<>:"|?*\x00-\x1F]/.test(name)) {
|
||||
errors.push('Folder name contains invalid characters')
|
||||
}
|
||||
|
||||
if (props.service.provider === 'imap' && /[\/\\]/.test(name)) {
|
||||
errors.push('IMAP folder names cannot contain / or \\')
|
||||
}
|
||||
|
||||
if (name !== name.trim()) {
|
||||
errors.push('Folder name cannot have leading or trailing spaces')
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
const checkDuplicateName = (name: string): boolean => {
|
||||
const parentId = props.folder.collection ?? null
|
||||
return props.allFolders.some(f => {
|
||||
if (String(f.identifier) === String(props.folder.identifier)) return false
|
||||
return (
|
||||
f.properties.label === name &&
|
||||
String(f.collection) === String(parentId) &&
|
||||
f.provider === props.folder.provider &&
|
||||
String(f.service) === String(props.folder.service)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
watch(folderName, (newName) => {
|
||||
errorMessage.value = ''
|
||||
validationErrors.value = validateFolderName(newName)
|
||||
|
||||
if (validationErrors.value.length === 0 && newName.trim().length > 0 && checkDuplicateName(newName)) {
|
||||
validationErrors.value.push('A folder with this name already exists in this location')
|
||||
}
|
||||
})
|
||||
|
||||
function resetForm() {
|
||||
folderName.value = props.folder.properties.label || ''
|
||||
errorMessage.value = ''
|
||||
validationErrors.value = []
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
watch(dialogValue, (isOpen) => {
|
||||
if (isOpen) {
|
||||
resetForm()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
const handleRename = async () => {
|
||||
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
|
||||
}
|
||||
|
||||
const newName = folderName.value.trim()
|
||||
if (newName === props.folder.properties.label) {
|
||||
dialogValue.value = false
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const properties = new CollectionPropertiesObject()
|
||||
properties.label = newName
|
||||
properties.rank = props.folder.properties.rank ?? 0
|
||||
properties.subscribed = props.folder.properties.subscribed ?? true
|
||||
|
||||
const updatedFolder = await collectionsStore.update(
|
||||
props.folder.provider,
|
||||
props.folder.service,
|
||||
props.folder.identifier,
|
||||
properties
|
||||
)
|
||||
|
||||
emit('updated', updatedFolder)
|
||||
dialogValue.value = false
|
||||
resetForm()
|
||||
} catch (error: any) {
|
||||
console.error('[RenameFolderDialog] Failed to rename folder:', error)
|
||||
errorMessage.value = error.message || 'Failed to rename 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">
|
||||
Rename Folder
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<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>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="text-caption text-medium-emphasis">Location</div>
|
||||
<div class="text-body-2">
|
||||
{{ parentFolderLabel }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-text-field
|
||||
v-model="folderName"
|
||||
label="Folder Name"
|
||||
placeholder="Enter folder name"
|
||||
variant="outlined"
|
||||
autofocus
|
||||
:error-messages="validationErrors"
|
||||
:disabled="loading"
|
||||
@keyup.enter="isValid && handleRename()"
|
||||
/>
|
||||
|
||||
<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="handleRename"
|
||||
>
|
||||
Save
|
||||
</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>
|
||||
Reference in New Issue
Block a user