first working login and editing of file. bugg is för rendering with tiptap
This commit is contained in:
135
frontend/src/components/FolderPicker.vue
Normal file
135
frontend/src/components/FolderPicker.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { gql } from '@/lib/gql'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
label?: string
|
||||
placeholder?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
const open = ref(false)
|
||||
const currentPath = ref(props.modelValue)
|
||||
const directories = ref<{ name: string; path: string }[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
async function fetchDirectories(path: string) {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await gql<{ serverDirectories: { name: string; path: string }[] }>(
|
||||
`query Dirs($p: String) { serverDirectories(path: $p) { name path } }`,
|
||||
{ p: path || null }
|
||||
)
|
||||
directories.value = data.serverDirectories
|
||||
// Update input box to the fetched path if we passed empty and it resolved to root
|
||||
if (directories.value.length > 0 && path === '') {
|
||||
// Find parent or use current
|
||||
const parent = directories.value.find(d => d.name === '..')
|
||||
if (parent) currentPath.value = parent.path + '/.' // hacky, but fine
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpen() {
|
||||
open.value = true
|
||||
currentPath.value = props.modelValue
|
||||
fetchDirectories(currentPath.value)
|
||||
}
|
||||
|
||||
function selectPath() {
|
||||
emit('update:modelValue', currentPath.value)
|
||||
open.value = false
|
||||
}
|
||||
|
||||
function navigate(toPath: string) {
|
||||
currentPath.value = toPath
|
||||
fetchDirectories(toPath)
|
||||
}
|
||||
|
||||
// Ensure local currentPath updates when props change externally
|
||||
watch(() => props.modelValue, (val) => {
|
||||
currentPath.value = val
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative">
|
||||
<label v-if="label" class="label">{{ label }}</label>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
:value="modelValue"
|
||||
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
|
||||
class="input flex-1"
|
||||
:placeholder="placeholder"
|
||||
/>
|
||||
<button type="button" class="btn-secondary" @click="handleOpen">Browse...</button>
|
||||
</div>
|
||||
|
||||
<!-- Modal Modal -->
|
||||
<transition name="fade">
|
||||
<div v-if="open" class="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/50 backdrop-blur-sm p-4">
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-xl border border-slate-200 dark:border-slate-700 w-full max-w-lg overflow-hidden flex flex-col max-h-[80vh]">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="p-4 border-b border-slate-200 dark:border-slate-700 flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">Select Folder</h3>
|
||||
<button type="button" @click="open = false" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="p-4 bg-slate-50 dark:bg-slate-900/50 flex-1 overflow-auto flex flex-col gap-3">
|
||||
<input
|
||||
v-model="currentPath"
|
||||
@keydown.enter.prevent="fetchDirectories(currentPath)"
|
||||
class="input font-mono text-sm"
|
||||
placeholder="Path..."
|
||||
/>
|
||||
|
||||
<div class="flex-1 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg overflow-y-auto min-h-[200px]">
|
||||
<div v-if="loading" class="p-4 text-center text-slate-400 animate-pulse text-sm">
|
||||
Loading...
|
||||
</div>
|
||||
<ul v-else class="divide-y divide-slate-100 dark:divide-slate-700/50">
|
||||
<li v-if="directories.length === 0" class="p-3 text-sm text-slate-500 text-center">
|
||||
No subdirectories
|
||||
</li>
|
||||
<li v-for="dir in directories" :key="dir.path">
|
||||
<button
|
||||
type="button"
|
||||
class="w-fulltext-left flex items-center gap-2 px-3 py-2 w-full hover:bg-accent-50 dark:hover:bg-accent-500/10 focus:bg-accent-50 focus:outline-none transition group"
|
||||
@click="navigate(dir.path)"
|
||||
>
|
||||
<svg class="w-4 h-4 text-accent-500 group-hover:text-accent-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
|
||||
<span class="text-sm font-medium text-slate-700 dark:text-slate-300 truncate">{{ dir.name }}</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="p-4 border-t border-slate-200 dark:border-slate-700 flex justify-end gap-2 bg-white dark:bg-slate-800">
|
||||
<button type="button" class="btn-ghost" @click="open = false">Cancel</button>
|
||||
<button type="button" class="btn-primary" @click="selectPath">Select Reference</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fade-enter-active, .fade-leave-active { transition: opacity 0.15s; }
|
||||
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user