feat: add documents selector
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user