| 1 |
<template> |
| 2 |
<Modal @close="$emit('close')"> |
| 3 |
<template #title>Create New Folder</template> |
| 4 |
|
| 5 |
<form @submit.prevent="handleCreate" class="space-y-4"> |
| 6 |
<!-- Folder name input --> |
| 7 |
<div> |
| 8 |
<label for="folderName" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"> |
| 9 |
Folder Name |
| 10 |
</label> |
| 11 |
<input |
| 12 |
id="folderName" |
| 13 |
ref="nameInput" |
| 14 |
v-model="form.name" |
| 15 |
type="text" |
| 16 |
required |
| 17 |
class="input" |
| 18 |
:class="{ 'border-red-500': errors.name }" |
| 19 |
placeholder="Enter folder name" |
| 20 |
@input="validateName" |
| 21 |
/> |
| 22 |
<p v-if="errors.name" class="mt-1 text-sm text-red-600 dark:text-red-400"> |
| 23 |
{{ errors.name }} |
| 24 |
</p> |
| 25 |
</div> |
| 26 |
|
| 27 |
<!-- Current path display --> |
| 28 |
<div class="p-3 bg-gray-50 dark:bg-gray-700 rounded-md"> |
| 29 |
<p class="text-sm text-gray-600 dark:text-gray-400"> |
| 30 |
Create in: <span class="font-mono">{{ displayPath }}</span> |
| 31 |
</p> |
| 32 |
</div> |
| 33 |
|
| 34 |
<!-- Options --> |
| 35 |
<div class="space-y-3"> |
| 36 |
<label class="flex items-center"> |
| 37 |
<input |
| 38 |
v-model="form.encrypted" |
| 39 |
type="checkbox" |
| 40 |
class="mr-2 rounded border-gray-300 focus:ring-primary-500" |
| 41 |
/> |
| 42 |
<span class="text-sm text-gray-700 dark:text-gray-300"> |
| 43 |
Create encrypted folder |
| 44 |
</span> |
| 45 |
</label> |
| 46 |
|
| 47 |
<div v-if="form.encrypted" class="ml-6 text-xs text-gray-500 dark:text-gray-400"> |
| 48 |
All files uploaded to this folder will be automatically encrypted |
| 49 |
</div> |
| 50 |
</div> |
| 51 |
|
| 52 |
<!-- Error display --> |
| 53 |
<div v-if="error" class="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md"> |
| 54 |
<p class="text-sm text-red-600 dark:text-red-400">{{ error }}</p> |
| 55 |
</div> |
| 56 |
</form> |
| 57 |
|
| 58 |
<template #footer> |
| 59 |
<button |
| 60 |
type="button" |
| 61 |
@click="$emit('close')" |
| 62 |
class="btn btn-outline mr-3" |
| 63 |
> |
| 64 |
Cancel |
| 65 |
</button> |
| 66 |
<button |
| 67 |
@click="handleCreate" |
| 68 |
:disabled="loading || !isValid" |
| 69 |
class="btn btn-primary" |
| 70 |
:class="{ 'opacity-50 cursor-not-allowed': loading || !isValid }" |
| 71 |
> |
| 72 |
<div v-if="loading" class="spinner w-4 h-4 mr-2"></div> |
| 73 |
{{ loading ? 'Creating...' : 'Create Folder' }} |
| 74 |
</button> |
| 75 |
</template> |
| 76 |
</Modal> |
| 77 |
</template> |
| 78 |
|
| 79 |
<script setup lang="ts"> |
| 80 |
import { ref, computed, onMounted, nextTick } from 'vue' |
| 81 |
import { apiClient } from '@/services/api' |
| 82 |
import Modal from './Modal.vue' |
| 83 |
|
| 84 |
interface Props { |
| 85 |
currentPath?: string |
| 86 |
} |
| 87 |
|
| 88 |
const props = withDefaults(defineProps<Props>(), { |
| 89 |
currentPath: '/', |
| 90 |
}) |
| 91 |
|
| 92 |
const emit = defineEmits<{ |
| 93 |
close: [] |
| 94 |
created: [path: string] |
| 95 |
}>() |
| 96 |
|
| 97 |
// Form state |
| 98 |
const form = ref({ |
| 99 |
name: '', |
| 100 |
encrypted: false, |
| 101 |
}) |
| 102 |
|
| 103 |
const nameInput = ref<HTMLInputElement>() |
| 104 |
const loading = ref(false) |
| 105 |
const error = ref('') |
| 106 |
const errors = ref<Record<string, string>>({}) |
| 107 |
|
| 108 |
// Computed |
| 109 |
const displayPath = computed(() => { |
| 110 |
return props.currentPath === '/' ? '/' : props.currentPath |
| 111 |
}) |
| 112 |
|
| 113 |
const isValid = computed(() => { |
| 114 |
return form.value.name.trim().length > 0 && !errors.value.name |
| 115 |
}) |
| 116 |
|
| 117 |
// Methods |
| 118 |
function validateName() { |
| 119 |
errors.value.name = '' |
| 120 |
|
| 121 |
const name = form.value.name.trim() |
| 122 |
|
| 123 |
if (!name) { |
| 124 |
errors.value.name = 'Folder name is required' |
| 125 |
return |
| 126 |
} |
| 127 |
|
| 128 |
if (name.length > 255) { |
| 129 |
errors.value.name = 'Folder name must be less than 255 characters' |
| 130 |
return |
| 131 |
} |
| 132 |
|
| 133 |
// Check for invalid characters |
| 134 |
const invalidChars = /[<>:"/\\|?*\x00-\x1f]/ |
| 135 |
if (invalidChars.test(name)) { |
| 136 |
errors.value.name = 'Folder name contains invalid characters' |
| 137 |
return |
| 138 |
} |
| 139 |
|
| 140 |
// Check for reserved names |
| 141 |
const reservedNames = ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'] |
| 142 |
if (reservedNames.includes(name.toUpperCase())) { |
| 143 |
errors.value.name = 'This folder name is reserved' |
| 144 |
return |
| 145 |
} |
| 146 |
|
| 147 |
// Check for names starting/ending with spaces or dots |
| 148 |
if (name.startsWith(' ') || name.endsWith(' ') || name.startsWith('.') || name.endsWith('.')) { |
| 149 |
errors.value.name = 'Folder name cannot start or end with spaces or dots' |
| 150 |
return |
| 151 |
} |
| 152 |
} |
| 153 |
|
| 154 |
async function handleCreate() { |
| 155 |
if (!isValid.value || loading.value) return |
| 156 |
|
| 157 |
loading.value = true |
| 158 |
error.value = '' |
| 159 |
|
| 160 |
try { |
| 161 |
const folderName = form.value.name.trim() |
| 162 |
|
| 163 |
// Create folder via API |
| 164 |
await apiClient.request({ |
| 165 |
method: 'POST', |
| 166 |
url: '/files/folder', |
| 167 |
data: { |
| 168 |
name: folderName, |
| 169 |
path: props.currentPath, |
| 170 |
encrypted: form.value.encrypted, |
| 171 |
}, |
| 172 |
}) |
| 173 |
|
| 174 |
const newPath = props.currentPath === '/' ? `/${folderName}` : `${props.currentPath}/${folderName}` |
| 175 |
|
| 176 |
emit('created', newPath) |
| 177 |
emit('close') |
| 178 |
|
| 179 |
// Show success notification |
| 180 |
if (window.$notify) { |
| 181 |
window.$notify.success(`Folder "${folderName}" created successfully`) |
| 182 |
} |
| 183 |
} catch (err: any) { |
| 184 |
error.value = err.response?.data?.message || 'Failed to create folder' |
| 185 |
console.error('Create folder failed:', err) |
| 186 |
} finally { |
| 187 |
loading.value = false |
| 188 |
} |
| 189 |
} |
| 190 |
|
| 191 |
// Focus input on mount |
| 192 |
onMounted(async () => { |
| 193 |
await nextTick() |
| 194 |
nameInput.value?.focus() |
| 195 |
}) |
| 196 |
</script> |