Files
documents_manager/src/components/DocumentSelectorDialog.vue
T
Sebastian f80e3679b6 feat: add documents selector
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-08-15 11:20:37 -04:00

229 lines
6.9 KiB
Vue

<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { useDocumentManager, useDocumentSelection } from '@/composables'
import { CollectionObject } from '@/models/collection'
import { EntityObject } from '@/models/entity'
import { formatSize, getFileIcon } from '@/utils/fileHelpers'
export interface DocumentSelection {
kind: 'entity' | 'collection'
identifier: string
name: string
mimeType?: string
size?: number
}
const PROVIDER_ID = 'default'
const SERVICE_ID = 'personal'
const LARGE_FILE_WARNING_BYTES = 25 * 1024 * 1024
const props = withDefaults(defineProps<{
modelValue: boolean
selectable?: ('entity' | 'collection')[]
}>(), {
selectable: () => ['entity'],
})
const emit = defineEmits<{
'update:modelValue': [value: boolean]
select: [items: DocumentSelection[]]
}>()
const allowEntities = computed(() => props.selectable.includes('entity'))
const allowCollections = computed(() => props.selectable.includes('collection'))
const documentManager = useDocumentManager({
providerId: PROVIDER_ID,
serviceId: SERVICE_ID,
})
const selection = useDocumentSelection({
multiple: true,
allowFolders: allowCollections.value,
allowFiles: allowEntities.value,
})
const initialized = ref(false)
watch(() => props.modelValue, async (isOpen) => {
if (!isOpen) {
return
}
selection.clear()
if (!initialized.value) {
await documentManager.initialize()
initialized.value = true
}
await documentManager.navigateToRoot()
})
function nodeId(node: CollectionObject | EntityObject): string {
return String(node.identifier ?? '')
}
function nodeLabel(node: CollectionObject | EntityObject): string {
return node.properties.label || nodeId(node)
}
const breadcrumbs = computed(() => [
{ id: documentManager.ROOT_ID, label: 'Home' },
...documentManager.breadcrumbs.value.map(node => ({ id: nodeId(node), label: nodeLabel(node) })),
])
async function navigateToBreadcrumb(id: string) {
selection.clear()
await documentManager.navigateTo(id === documentManager.ROOT_ID ? null : id)
}
async function openFolder(folder: CollectionObject) {
selection.clear()
await documentManager.navigateTo(nodeId(folder))
}
function toggleFolderSelection(folder: CollectionObject, event: Event) {
event.stopPropagation()
selection.toggle(folder)
}
function toggleEntity(entity: EntityObject) {
selection.toggle(entity)
}
const hasLargeSelection = computed(() =>
selection.selectedEntities.value.some(entity => entity.properties.size > LARGE_FILE_WARNING_BYTES),
)
function entityIdentifier(entity: EntityObject): string {
return `${entity.provider}:${entity.service}:${entity.collection}:${entity.identifier}`
}
function collectionIdentifier(collection: CollectionObject): string {
return `${collection.provider}:${collection.service}:${collection.identifier}`
}
function toSelection(node: CollectionObject | EntityObject): DocumentSelection {
if (node instanceof EntityObject) {
return {
kind: 'entity',
identifier: entityIdentifier(node),
name: nodeLabel(node),
mimeType: node.properties.mime || 'application/octet-stream',
size: node.properties.size,
}
}
return {
kind: 'collection',
identifier: collectionIdentifier(node),
name: nodeLabel(node),
}
}
function handleClose() {
emit('update:modelValue', false)
}
function handleConfirm() {
const items = selection.selectedNodeArray.value.map(toSelection)
if (items.length === 0) {
return
}
emit('select', items)
selection.clear()
emit('update:modelValue', false)
}
</script>
<template>
<v-dialog :model-value="modelValue" @update:model-value="emit('update:modelValue', $event)" max-width="560">
<v-card>
<v-card-title class="d-flex align-center">
<v-icon class="mr-2">mdi-folder-search-outline</v-icon>
Select Documents
</v-card-title>
<v-card-text>
<v-breadcrumbs :items="breadcrumbs" density="compact" class="px-0 mb-2">
<template #item="{ item }">
<v-breadcrumbs-item :disabled="item.id === breadcrumbs[breadcrumbs.length - 1]?.id" @click="navigateToBreadcrumb(item.id)">
{{ item.label }}
</v-breadcrumbs-item>
</template>
</v-breadcrumbs>
<v-progress-linear v-if="documentManager.isLoading.value" indeterminate color="primary" class="mb-2" />
<div class="selector-list">
<v-list v-if="documentManager.currentCollections.value.length > 0 || documentManager.currentEntities.value.length > 0" density="compact">
<v-list-item
v-for="folder in documentManager.currentCollections.value"
:key="nodeId(folder)"
:title="nodeLabel(folder)"
@click="openFolder(folder)"
>
<template #prepend>
<v-checkbox-btn
v-if="allowCollections"
:model-value="selection.isSelected(nodeId(folder))"
class="mr-2"
@click="toggleFolderSelection(folder, $event)"
/>
<v-icon>mdi-folder</v-icon>
</template>
<template #append>
<v-icon size="small">mdi-chevron-right</v-icon>
</template>
</v-list-item>
<v-list-item
v-for="entity in documentManager.currentEntities.value"
:key="nodeId(entity)"
:title="nodeLabel(entity)"
:subtitle="formatSize(entity.properties.size)"
:disabled="!allowEntities"
@click="allowEntities && toggleEntity(entity)"
>
<template #prepend>
<v-checkbox-btn v-if="allowEntities" :model-value="selection.isSelected(nodeId(entity))" class="mr-2" />
<v-icon>{{ getFileIcon(entity) }}</v-icon>
</template>
</v-list-item>
</v-list>
<div v-else-if="!documentManager.isLoading.value" class="text-center py-8 text-medium-emphasis">
<v-icon size="40" class="mb-2">mdi-folder-open-outline</v-icon>
<div>This folder is empty</div>
</div>
</div>
<v-alert v-if="hasLargeSelection" type="warning" variant="tonal" density="compact" class="mt-3">
One or more selected files are larger than 25 MB and may take a moment to process.
</v-alert>
</v-card-text>
<v-card-actions>
<span class="text-caption text-medium-emphasis ml-2">
{{ selection.count.value }} selected
</span>
<v-spacer />
<v-btn variant="text" @click="handleClose">Cancel</v-btn>
<v-btn color="primary" variant="elevated" :disabled="!selection.hasSelection.value" @click="handleConfirm">
Select
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<style scoped>
.selector-list {
max-height: 360px;
overflow-y: auto;
border: 1px solid rgb(var(--v-border-color));
border-radius: 8px;
}
</style>