feat: add documents selector
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
Generated
+139
-2
@@ -9,17 +9,20 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pinia": "^4.0.0",
|
"pinia": "^4.0.0",
|
||||||
"vue": "^3.5.13"
|
"vue": "^3.5.13",
|
||||||
|
"vuetify": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/node24": "^24.0.0",
|
"@tsconfig/node24": "^24.0.0",
|
||||||
"@types/node": "^24.0.0",
|
"@types/node": "^24.0.0",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@vitest/coverage-v8": "^4.1.6",
|
"@vitest/coverage-v8": "^4.1.6",
|
||||||
"@vue/tsconfig": "^0.9.0",
|
"@vue/tsconfig": "^0.9.0",
|
||||||
"jsdom": "^29.1.1",
|
"jsdom": "^29.1.1",
|
||||||
"typescript": "~6.0.0",
|
"typescript": "~6.0.0",
|
||||||
"vite": "^8.0.0",
|
"vite": "^8.0.0",
|
||||||
"vitest": "^4.1.6"
|
"vitest": "^4.1.6",
|
||||||
|
"vue-tsc": "^3.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@asamuzakjp/css-color": {
|
"node_modules/@asamuzakjp/css-color": {
|
||||||
@@ -714,6 +717,23 @@
|
|||||||
"undici-types": "~7.18.0"
|
"undici-types": "~7.18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vitejs/plugin-vue": {
|
||||||
|
"version": "6.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz",
|
||||||
|
"integrity": "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@rolldown/pluginutils": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
|
||||||
|
"vue": "^3.2.25"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vitest/coverage-v8": {
|
"node_modules/@vitest/coverage-v8": {
|
||||||
"version": "4.1.10",
|
"version": "4.1.10",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz",
|
||||||
@@ -868,6 +888,35 @@
|
|||||||
"url": "https://opencollective.com/vitest"
|
"url": "https://opencollective.com/vitest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@volar/language-core": {
|
||||||
|
"version": "2.4.28",
|
||||||
|
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz",
|
||||||
|
"integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@volar/source-map": "2.4.28"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@volar/source-map": {
|
||||||
|
"version": "2.4.28",
|
||||||
|
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz",
|
||||||
|
"integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@volar/typescript": {
|
||||||
|
"version": "2.4.28",
|
||||||
|
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz",
|
||||||
|
"integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@volar/language-core": "2.4.28",
|
||||||
|
"path-browserify": "^1.0.1",
|
||||||
|
"vscode-uri": "^3.0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vue/compiler-core": {
|
"node_modules/@vue/compiler-core": {
|
||||||
"version": "3.5.40",
|
"version": "3.5.40",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.40.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.40.tgz",
|
||||||
@@ -948,6 +997,22 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@vue/language-core": {
|
||||||
|
"version": "3.3.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.9.tgz",
|
||||||
|
"integrity": "sha512-in/68oAa4BCtVY6n/nkuhLIkV8DHYd2UivedJ6cMZ6UYtlq9jaoaSNUBHYCVO44z3nKg7MdE5OBoHKt5SxeBKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@volar/language-core": "2.4.28",
|
||||||
|
"@vue/compiler-dom": "^3.5.0",
|
||||||
|
"@vue/shared": "^3.5.0",
|
||||||
|
"alien-signals": "^3.2.1",
|
||||||
|
"muggle-string": "^0.4.1",
|
||||||
|
"path-browserify": "^1.0.1",
|
||||||
|
"picomatch": "^4.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vue/reactivity": {
|
"node_modules/@vue/reactivity": {
|
||||||
"version": "3.5.40",
|
"version": "3.5.40",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.40.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.40.tgz",
|
||||||
@@ -1015,6 +1080,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/alien-signals": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/assertion-error": {
|
"node_modules/assertion-error": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||||
@@ -1649,6 +1721,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "CC0-1.0"
|
"license": "CC0-1.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/muggle-string": {
|
||||||
|
"version": "0.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
|
||||||
|
"integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.12",
|
"version": "3.3.12",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||||
@@ -1713,6 +1792,13 @@
|
|||||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/path-browserify": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/pathe": {
|
"node_modules/pathe": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||||
@@ -2226,6 +2312,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vscode-uri": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/vue": {
|
"node_modules/vue": {
|
||||||
"version": "3.5.40",
|
"version": "3.5.40",
|
||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.40.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.40.tgz",
|
||||||
@@ -2247,6 +2340,50 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-tsc": {
|
||||||
|
"version": "3.3.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.3.9.tgz",
|
||||||
|
"integrity": "sha512-TS3Y1ux/IRoE8OCP2PpACAeOseuIs0UvWrcr7u+w3PmfY+SlCfEf8zjrBgnQksHUgLpthi5vHlffcQTQTdPBZA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@volar/typescript": "2.4.28",
|
||||||
|
"@vue/language-core": "3.3.9"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vue-tsc": "bin/vue-tsc.js"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vuetify": {
|
||||||
|
"version": "4.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-4.1.9.tgz",
|
||||||
|
"integrity": "sha512-2NlpUiSwkUAGxKmkKwoRCst3IJF6EPP7msNJd+VxFjcAN9xCG+RFODzsfXfO2ypl9aSj3T/PJ65BBjQeTGgFKA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/johnleider"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=4.7",
|
||||||
|
"vite-plugin-vuetify": ">=2.1.0",
|
||||||
|
"vue": "^3.5.0 || ^3.6.0-0",
|
||||||
|
"webpack-plugin-vuetify": ">=3.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite-plugin-vuetify": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"webpack-plugin-vuetify": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/w3c-xmlserializer": {
|
"node_modules/w3c-xmlserializer": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||||
|
|||||||
+4
-1
@@ -15,14 +15,17 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pinia": "^4.0.0",
|
"pinia": "^4.0.0",
|
||||||
"vue": "^3.5.13"
|
"vue": "^3.5.13",
|
||||||
|
"vuetify": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/node24": "^24.0.0",
|
"@tsconfig/node24": "^24.0.0",
|
||||||
"@types/node": "^24.0.0",
|
"@types/node": "^24.0.0",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@vue/tsconfig": "^0.9.0",
|
"@vue/tsconfig": "^0.9.0",
|
||||||
"typescript": "~6.0.0",
|
"typescript": "~6.0.0",
|
||||||
"vite": "^8.0.0",
|
"vite": "^8.0.0",
|
||||||
|
"vue-tsc": "^3.0.5",
|
||||||
"@vitest/coverage-v8": "^4.1.6",
|
"@vitest/coverage-v8": "^4.1.6",
|
||||||
"jsdom": "^29.1.1",
|
"jsdom": "^29.1.1",
|
||||||
"vitest": "^4.1.6"
|
"vitest": "^4.1.6"
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Central export point for Documents Manager composables
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { useDocumentManager } from './useDocumentManager'
|
||||||
|
export { useDocumentSelection } from './useDocumentSelection'
|
||||||
|
|
||||||
|
export type { UseDocumentManagerOptions } from './useDocumentManager'
|
||||||
|
export type { UseDocumentSelectionOptions } from './useDocumentSelection'
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
/**
|
||||||
|
* File Manager composable for convenient file/folder operations
|
||||||
|
* Provides reactive access to file manager state and actions
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { computed, ref, unref } from 'vue'
|
||||||
|
import type { Ref, ComputedRef } from 'vue'
|
||||||
|
import { useProvidersStore } from '@/stores/providersStore'
|
||||||
|
import { useServicesStore } from '@/stores/servicesStore'
|
||||||
|
import { useNodesStore, ROOT_ID } from '@/stores/nodesStore'
|
||||||
|
import type { ListFilter, ListSort, ListRange } from '@/types/common'
|
||||||
|
import type { DocumentInterface } from '@/types/document'
|
||||||
|
import { CollectionObject } from '@/models/collection'
|
||||||
|
import { EntityObject } from '@/models/entity'
|
||||||
|
|
||||||
|
// Base URL for file manager transfer endpoints
|
||||||
|
const TRANSFER_BASE_URL = '/m/documents_manager'
|
||||||
|
|
||||||
|
export interface UseDocumentManagerOptions {
|
||||||
|
providerId: string | Ref<string> | ComputedRef<string>
|
||||||
|
serviceId: string | Ref<string> | ComputedRef<string>
|
||||||
|
autoFetch?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDocumentManager(options: UseDocumentManagerOptions) {
|
||||||
|
const providersStore = useProvidersStore()
|
||||||
|
const servicesStore = useServicesStore()
|
||||||
|
const nodesStore = useNodesStore()
|
||||||
|
|
||||||
|
const { providerId, serviceId, autoFetch = false } = options
|
||||||
|
|
||||||
|
const currentProviderId = () => unref(providerId)
|
||||||
|
const currentServiceId = () => unref(serviceId)
|
||||||
|
|
||||||
|
// Current location (folder being viewed)
|
||||||
|
const currentLocation: Ref<string> = ref(ROOT_ID)
|
||||||
|
|
||||||
|
// Loading/error state
|
||||||
|
const isLoading = computed(() => nodesStore.transceiving)
|
||||||
|
const error = computed(() => nodesStore.error)
|
||||||
|
|
||||||
|
// Provider and service
|
||||||
|
const provider = computed(() => providersStore.provider(currentProviderId()))
|
||||||
|
const service = computed(() => servicesStore.service(currentProviderId(), currentServiceId()))
|
||||||
|
const rootId = computed(() => ROOT_ID)
|
||||||
|
|
||||||
|
// Current children
|
||||||
|
const currentChildren = computed(() =>
|
||||||
|
nodesStore.getChildren(currentProviderId(), currentServiceId(), currentLocation.value)
|
||||||
|
)
|
||||||
|
|
||||||
|
const currentCollections: ComputedRef<CollectionObject[]> = computed(() =>
|
||||||
|
nodesStore.getChildCollections(currentProviderId(), currentServiceId(), currentLocation.value)
|
||||||
|
)
|
||||||
|
|
||||||
|
const currentEntities: ComputedRef<EntityObject[]> = computed(() =>
|
||||||
|
nodesStore.getChildEntities(currentProviderId(), currentServiceId(), currentLocation.value)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Breadcrumb path
|
||||||
|
const breadcrumbs = computed(() => {
|
||||||
|
if (currentLocation.value === ROOT_ID) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return nodesStore.getPath(currentProviderId(), currentServiceId(), currentLocation.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Is at root?
|
||||||
|
const isAtRoot = computed(() => currentLocation.value === ROOT_ID)
|
||||||
|
|
||||||
|
// Navigate to a folder
|
||||||
|
const navigateTo = async (collectionId: string | null) => {
|
||||||
|
currentLocation.value = collectionId || ROOT_ID
|
||||||
|
await refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate up one level
|
||||||
|
const navigateUp = async () => {
|
||||||
|
if (currentLocation.value === ROOT_ID) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const currentNode = nodesStore.getNode(currentProviderId(), currentServiceId(), currentLocation.value)
|
||||||
|
if (currentNode) {
|
||||||
|
await navigateTo(currentNode.collection ? String(currentNode.collection) : ROOT_ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to root
|
||||||
|
const navigateToRoot = async () => {
|
||||||
|
await navigateTo(ROOT_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh current location
|
||||||
|
const refresh = async (
|
||||||
|
filter?: ListFilter,
|
||||||
|
sort?: ListSort,
|
||||||
|
range?: ListRange
|
||||||
|
) => {
|
||||||
|
await nodesStore.fetchNodes(
|
||||||
|
currentProviderId(),
|
||||||
|
currentServiceId(),
|
||||||
|
currentLocation.value === ROOT_ID ? ROOT_ID : currentLocation.value,
|
||||||
|
filter,
|
||||||
|
sort,
|
||||||
|
range
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new folder
|
||||||
|
const createFolder = async (label: string): Promise<CollectionObject> => {
|
||||||
|
return await nodesStore.createCollection(
|
||||||
|
currentProviderId(),
|
||||||
|
currentServiceId(),
|
||||||
|
currentLocation.value === ROOT_ID ? ROOT_ID : currentLocation.value,
|
||||||
|
{ label, owner: '' }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new file
|
||||||
|
const createFile = async (
|
||||||
|
label: string,
|
||||||
|
mime: string = 'application/octet-stream'
|
||||||
|
): Promise<EntityObject> => {
|
||||||
|
const properties: DocumentInterface = {
|
||||||
|
'@type': 'documents.properties',
|
||||||
|
urid: null,
|
||||||
|
size: 0,
|
||||||
|
label,
|
||||||
|
mime,
|
||||||
|
format: null,
|
||||||
|
encoding: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
return await nodesStore.createEntity(
|
||||||
|
currentProviderId(),
|
||||||
|
currentServiceId(),
|
||||||
|
currentLocation.value === ROOT_ID ? ROOT_ID : currentLocation.value,
|
||||||
|
properties
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename a node
|
||||||
|
const renameNode = async (nodeId: string, newLabel: string) => {
|
||||||
|
const node = nodesStore.getNode(currentProviderId(), currentServiceId(), nodeId)
|
||||||
|
if (!node) {
|
||||||
|
throw new Error('Node not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node instanceof CollectionObject) {
|
||||||
|
return await nodesStore.updateCollection(currentProviderId(), currentServiceId(), nodeId, {
|
||||||
|
label: newLabel,
|
||||||
|
owner: node.properties.owner,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const properties: DocumentInterface = {
|
||||||
|
'@type': 'documents.properties',
|
||||||
|
urid: node.properties.urid,
|
||||||
|
size: node.properties.size,
|
||||||
|
label: newLabel,
|
||||||
|
mime: node.properties.mime,
|
||||||
|
format: node.properties.format,
|
||||||
|
encoding: node.properties.encoding,
|
||||||
|
}
|
||||||
|
return await nodesStore.updateEntity(currentProviderId(), currentServiceId(), node.collection, nodeId, properties)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a node
|
||||||
|
const deleteNode = async (nodeId: string): Promise<boolean> => {
|
||||||
|
const node = nodesStore.getNode(currentProviderId(), currentServiceId(), nodeId)
|
||||||
|
if (!node) {
|
||||||
|
throw new Error('Node not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node instanceof CollectionObject) {
|
||||||
|
return await nodesStore.deleteCollection(currentProviderId(), currentServiceId(), nodeId)
|
||||||
|
} else {
|
||||||
|
return await nodesStore.deleteEntity(currentProviderId(), currentServiceId(), node.collection, nodeId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read file content
|
||||||
|
const readFile = async (entityId: string): Promise<string | null> => {
|
||||||
|
const node = nodesStore.getNode(currentProviderId(), currentServiceId(), entityId)
|
||||||
|
if (!node || !(node instanceof EntityObject)) {
|
||||||
|
throw new Error('Entity not found')
|
||||||
|
}
|
||||||
|
return await nodesStore.readEntity(currentProviderId(), currentServiceId(), node.collection || ROOT_ID, entityId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write file content
|
||||||
|
const writeFile = async (entityId: string, content: string): Promise<number> => {
|
||||||
|
const node = nodesStore.getNode(currentProviderId(), currentServiceId(), entityId)
|
||||||
|
if (!node || !(node instanceof EntityObject)) {
|
||||||
|
throw new Error('Entity not found')
|
||||||
|
}
|
||||||
|
return await nodesStore.writeEntity(currentProviderId(), currentServiceId(), node.collection, entityId, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a URL suitable for inline viewing (img src / video src)
|
||||||
|
const getEntityUrl = (entityId: string, collectionId?: string | null): string => {
|
||||||
|
const collection = collectionId ?? currentLocation.value
|
||||||
|
return `${TRANSFER_BASE_URL}/download/entity/${encodeURIComponent(currentProviderId())}/${encodeURIComponent(currentServiceId())}/${encodeURIComponent(collection)}/${encodeURIComponent(entityId)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download a single file
|
||||||
|
const downloadEntity = (entityId: string, collectionId?: string | null): void => {
|
||||||
|
const collection = collectionId ?? currentLocation.value
|
||||||
|
// Use path parameters: /download/entity/{provider}/{service}/{collection}/{identifier}
|
||||||
|
const url = `${TRANSFER_BASE_URL}/download/entity/${encodeURIComponent(currentProviderId())}/${encodeURIComponent(currentServiceId())}/${encodeURIComponent(collection)}/${encodeURIComponent(entityId)}`
|
||||||
|
|
||||||
|
// Trigger download by opening URL (browser handles it)
|
||||||
|
window.open(url, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadCollection = (collectionId: string): void => {
|
||||||
|
// Use path parameters: /download/collection/{provider}/{service}/{identifier}
|
||||||
|
const url = `${TRANSFER_BASE_URL}/download/collection/${encodeURIComponent(currentProviderId())}/${encodeURIComponent(currentServiceId())}/${encodeURIComponent(collectionId)}`
|
||||||
|
|
||||||
|
window.open(url, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadArchive = (ids: string[], name: string = 'download', collectionId?: string | null): void => {
|
||||||
|
const collection = collectionId ?? currentLocation.value
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
provider: currentProviderId(),
|
||||||
|
service: currentServiceId(),
|
||||||
|
})
|
||||||
|
ids.forEach(id => params.append('ids[]', id))
|
||||||
|
if (name) {
|
||||||
|
params.append('name', name)
|
||||||
|
}
|
||||||
|
if (collection && collection !== ROOT_ID) {
|
||||||
|
params.append('collection', collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${TRANSFER_BASE_URL}/download/archive?${params.toString()}`
|
||||||
|
|
||||||
|
window.open(url, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize - fetch providers, services, and initial nodes if autoFetch
|
||||||
|
const initialize = async () => {
|
||||||
|
await providersStore.list([currentProviderId()])
|
||||||
|
await servicesStore.list()
|
||||||
|
if (autoFetch) {
|
||||||
|
await refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
currentLocation,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
|
||||||
|
// Provider/Service
|
||||||
|
provider,
|
||||||
|
service,
|
||||||
|
rootId,
|
||||||
|
|
||||||
|
// Current view
|
||||||
|
currentChildren,
|
||||||
|
currentCollections,
|
||||||
|
currentEntities,
|
||||||
|
breadcrumbs,
|
||||||
|
isAtRoot,
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
navigateTo,
|
||||||
|
navigateUp,
|
||||||
|
navigateToRoot,
|
||||||
|
refresh,
|
||||||
|
|
||||||
|
// Operations
|
||||||
|
createFolder,
|
||||||
|
createFile,
|
||||||
|
renameNode,
|
||||||
|
deleteNode,
|
||||||
|
readFile,
|
||||||
|
writeFile,
|
||||||
|
getEntityUrl,
|
||||||
|
downloadEntity,
|
||||||
|
downloadCollection,
|
||||||
|
downloadArchive,
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
initialize,
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
ROOT_ID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useDocumentManager
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
/**
|
||||||
|
* File selection composable
|
||||||
|
* Provides reactive selection management for file manager
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import type { Ref, ComputedRef } from 'vue'
|
||||||
|
import { CollectionObject } from '@/models/collection'
|
||||||
|
import { EntityObject } from '@/models/entity'
|
||||||
|
|
||||||
|
type NodeRecord = CollectionObject | EntityObject
|
||||||
|
|
||||||
|
export interface UseDocumentSelectionOptions {
|
||||||
|
multiple?: boolean
|
||||||
|
allowFolders?: boolean
|
||||||
|
allowFiles?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDocumentSelection(options: UseDocumentSelectionOptions = {}) {
|
||||||
|
const {
|
||||||
|
multiple = true,
|
||||||
|
allowFolders = true,
|
||||||
|
allowFiles = true,
|
||||||
|
} = options
|
||||||
|
|
||||||
|
const selectedIds: Ref<Set<string>> = ref(new Set())
|
||||||
|
const selectedNodes: Ref<Map<string, NodeRecord>> = ref(new Map())
|
||||||
|
|
||||||
|
const count: ComputedRef<number> = computed(() => selectedIds.value.size)
|
||||||
|
const hasSelection: ComputedRef<boolean> = computed(() => selectedIds.value.size > 0)
|
||||||
|
|
||||||
|
const selectedIdArray: ComputedRef<string[]> = computed(() => Array.from(selectedIds.value))
|
||||||
|
const selectedNodeArray: ComputedRef<NodeRecord[]> = computed(() => Array.from(selectedNodes.value.values()))
|
||||||
|
|
||||||
|
const selectedCollections: ComputedRef<CollectionObject[]> = computed(() =>
|
||||||
|
selectedNodeArray.value.filter((node): node is CollectionObject => node instanceof CollectionObject),
|
||||||
|
)
|
||||||
|
|
||||||
|
const selectedEntities: ComputedRef<EntityObject[]> = computed(() =>
|
||||||
|
selectedNodeArray.value.filter((node): node is EntityObject => node instanceof EntityObject),
|
||||||
|
)
|
||||||
|
|
||||||
|
const isSelected = (nodeId: string): boolean => selectedIds.value.has(nodeId)
|
||||||
|
|
||||||
|
const isTypeAllowed = (node: NodeRecord): boolean => {
|
||||||
|
if (node instanceof CollectionObject && !allowFolders) return false
|
||||||
|
if (node instanceof EntityObject && !allowFiles) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const select = (node: NodeRecord) => {
|
||||||
|
if (!isTypeAllowed(node)) return
|
||||||
|
|
||||||
|
if (!multiple) {
|
||||||
|
selectedIds.value.clear()
|
||||||
|
selectedNodes.value.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeId = String(node.identifier)
|
||||||
|
selectedIds.value.add(nodeId)
|
||||||
|
selectedNodes.value.set(nodeId, node)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deselect = (nodeId: string) => {
|
||||||
|
selectedIds.value.delete(nodeId)
|
||||||
|
selectedNodes.value.delete(nodeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggle = (node: NodeRecord) => {
|
||||||
|
const nodeId = String(node.identifier)
|
||||||
|
if (isSelected(nodeId)) {
|
||||||
|
deselect(nodeId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectMultiple = (nodes: NodeRecord[]) => {
|
||||||
|
if (!multiple) {
|
||||||
|
const lastNode = nodes[nodes.length - 1]
|
||||||
|
if (lastNode && isTypeAllowed(lastNode)) {
|
||||||
|
selectedIds.value.clear()
|
||||||
|
selectedNodes.value.clear()
|
||||||
|
const nodeId = String(lastNode.identifier)
|
||||||
|
selectedIds.value.add(nodeId)
|
||||||
|
selectedNodes.value.set(nodeId, lastNode)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (isTypeAllowed(node)) {
|
||||||
|
const nodeId = String(node.identifier)
|
||||||
|
selectedIds.value.add(nodeId)
|
||||||
|
selectedNodes.value.set(nodeId, node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectAll = (nodes: NodeRecord[]) => {
|
||||||
|
if (!multiple) return
|
||||||
|
selectMultiple(nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
selectedIds.value.clear()
|
||||||
|
selectedNodes.value.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
const setSelection = (nodes: NodeRecord[]) => {
|
||||||
|
clear()
|
||||||
|
selectMultiple(nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedIds,
|
||||||
|
selectedNodes,
|
||||||
|
count,
|
||||||
|
hasSelection,
|
||||||
|
selectedIdArray,
|
||||||
|
selectedNodeArray,
|
||||||
|
selectedCollections,
|
||||||
|
selectedEntities,
|
||||||
|
isSelected,
|
||||||
|
select,
|
||||||
|
deselect,
|
||||||
|
toggle,
|
||||||
|
selectMultiple,
|
||||||
|
selectAll,
|
||||||
|
clear,
|
||||||
|
setSelection,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useDocumentSelection
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import type { ModuleIntegrations } from "@KTXC/types/moduleTypes";
|
||||||
|
|
||||||
|
const integrations: ModuleIntegrations = {
|
||||||
|
documents_selector: [
|
||||||
|
{
|
||||||
|
id: 'dialog',
|
||||||
|
label: 'Document Selector Dialog',
|
||||||
|
priority: 10,
|
||||||
|
component: () => import('./components/DocumentSelectorDialog.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default integrations;
|
||||||
+8
-2
@@ -2,14 +2,20 @@
|
|||||||
* Documents Manager Module Boot
|
* Documents Manager Module Boot
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import integrations from '@/integrations'
|
||||||
|
|
||||||
console.log('[Documents Manager] Booting module...')
|
console.log('[Documents Manager] Booting module...')
|
||||||
|
|
||||||
console.log('[Documents Manager] Module booted successfully...')
|
console.log('[Documents Manager] Module booted successfully...')
|
||||||
|
|
||||||
// CSS will be injected by build process
|
// CSS filename is injected by the vite plugin at build time
|
||||||
//export const css = ['__CSS_FILENAME_PLACEHOLDER__']
|
export const css = ['__CSS_FILENAME_PLACEHOLDER__']
|
||||||
|
|
||||||
|
export { integrations }
|
||||||
|
|
||||||
// Export services, stores and models for external use
|
// Export services, stores and models for external use
|
||||||
export * from '@/services'
|
export * from '@/services'
|
||||||
export * from '@/stores'
|
export * from '@/stores'
|
||||||
export * from '@/models'
|
export * from '@/models'
|
||||||
|
export * from '@/composables'
|
||||||
|
export * from '@/utils/fileHelpers'
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { EntityObject } from '@/models/entity'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the appropriate icon for a file based on its MIME type
|
||||||
|
*/
|
||||||
|
export function getFileIcon(entity: EntityObject): string {
|
||||||
|
const mime = entity.properties.mime || ''
|
||||||
|
if (!mime) return 'mdi-file'
|
||||||
|
if (mime.startsWith('image/')) return 'mdi-file-image'
|
||||||
|
if (mime.startsWith('video/')) return 'mdi-file-video'
|
||||||
|
if (mime.startsWith('audio/')) return 'mdi-file-music'
|
||||||
|
if (mime.startsWith('text/')) return 'mdi-file-document'
|
||||||
|
if (mime === 'application/pdf') return 'mdi-file-pdf-box'
|
||||||
|
if (mime.includes('zip') || mime.includes('tar') || mime.includes('compressed')) return 'mdi-folder-zip'
|
||||||
|
if (mime.includes('word') || mime.includes('document')) return 'mdi-file-word'
|
||||||
|
if (mime.includes('excel') || mime.includes('spreadsheet')) return 'mdi-file-excel'
|
||||||
|
if (mime.includes('powerpoint') || mime.includes('presentation')) return 'mdi-file-powerpoint'
|
||||||
|
return 'mdi-file'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format file size in human readable format
|
||||||
|
*/
|
||||||
|
export function formatSize(bytes: number): string {
|
||||||
|
const safeBytes = Number.isFinite(bytes) ? bytes : 0
|
||||||
|
if (safeBytes === 0) return '0 B'
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||||
|
const i = Math.floor(Math.log(safeBytes) / Math.log(k))
|
||||||
|
return parseFloat((safeBytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format date string to localized format
|
||||||
|
*/
|
||||||
|
export function formatDate(value: Date | string | null | undefined): string {
|
||||||
|
if (!value) return '—'
|
||||||
|
const date = value instanceof Date ? value : new Date(value)
|
||||||
|
if (Number.isNaN(date.getTime())) return '—'
|
||||||
|
return date.toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get upload status icon
|
||||||
|
*/
|
||||||
|
export function getUploadStatusIcon(status: string): string {
|
||||||
|
switch (status) {
|
||||||
|
case 'pending': return 'mdi-clock-outline'
|
||||||
|
case 'uploading': return 'mdi-loading mdi-spin'
|
||||||
|
case 'completed': return 'mdi-check-circle'
|
||||||
|
case 'error': return 'mdi-alert-circle'
|
||||||
|
default: return 'mdi-file'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get upload status color
|
||||||
|
*/
|
||||||
|
export function getUploadStatusColor(status: string): string {
|
||||||
|
switch (status) {
|
||||||
|
case 'pending': return 'grey'
|
||||||
|
case 'uploading': return 'primary'
|
||||||
|
case 'completed': return 'success'
|
||||||
|
case 'error': return 'error'
|
||||||
|
default: return 'grey'
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -15,5 +15,5 @@
|
|||||||
"@KTXC/*": ["../../core/src/*"]
|
"@KTXC/*": ["../../core/src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.tsx"]
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user