Compare commits

..

21 Commits

Author SHA1 Message Date
Sebastian 72cb2b8d02 fix: full screen preview
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-09-14 18:56:38 -04:00
Sebastian a0c50bded9 Merge pull request 'chore(deps): update dependency vuetify to v4.2.1' (#58) from renovate/vuetify-4.x-lockfile into main
Reviewed-on: #58
2026-09-14 22:02:07 +00:00
Sebastian 09ada28f09 Merge pull request 'chore(deps): update dependency vite to v8.3.0' (#59) from renovate/vite-8.x-lockfile into main
Reviewed-on: #59
2026-09-14 22:02:01 +00:00
Sebastian ea28911822 chore(deps): update dependency vite to v8.3.0
Build Test / test (pull_request) Successful in 41s
JS Unit Tests / test (pull_request) Successful in 40s
2026-09-14 03:02:08 +00:00
Sebastian 0a88c7f14a chore(deps): update dependency vuetify to v4.2.1
Build Test / test (pull_request) Successful in 47s
JS Unit Tests / test (pull_request) Successful in 51s
2026-09-14 03:02:05 +00:00
Sebastian 1d7e363076 refactor: adjust integration points
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-09-12 21:40:28 -04:00
Sebastian 7f94b97a8d Merge pull request 'chore(deps): update dependency vite to v8.2.2' (#52) from renovate/vite-8.x-lockfile into main
Reviewed-on: #52
2026-09-06 01:19:20 +00:00
Sebastian fe5d4fae37 Merge pull request 'chore(deps): update dependency vuetify to v4.2.0' (#56) from renovate/vuetify-4.x-lockfile into main
Reviewed-on: #56
2026-09-06 01:19:13 +00:00
Sebastian 01610727fd Merge pull request 'chore(deps): update vitest monorepo to v5' (#57) from renovate/major-vitest-monorepo into main
Reviewed-on: #57
2026-09-06 01:19:07 +00:00
Sebastian 6942469259 feat: pagination
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-09-05 20:14:40 -04:00
Sebastian f73e53bc65 chore(deps): update vitest monorepo to v5
Build Test / test (pull_request) Successful in 43s
JS Unit Tests / test (pull_request) Successful in 48s
2026-09-05 03:01:50 +00:00
Sebastian 66635a73ff chore(deps): update dependency vuetify to v4.2.0
Build Test / test (pull_request) Successful in 1m4s
JS Unit Tests / test (pull_request) Successful in 1m5s
2026-09-05 03:01:46 +00:00
Sebastian ea4c052981 chore(deps): update dependency vite to v8.2.2
Build Test / test (pull_request) Successful in 54s
JS Unit Tests / test (pull_request) Successful in 53s
2026-09-05 03:01:36 +00:00
Sebastian 47d97a8d1a Merge pull request 'chore(deps): update dependency jsdom to v30.0.1' (#51) from renovate/jsdom-30.x-lockfile into main
Reviewed-on: #51
2026-09-04 22:38:29 +00:00
Sebastian 5b12979c39 Merge pull request 'chore(deps): update vitest monorepo to v4.1.11' (#53) from renovate/vitest-monorepo into main
Reviewed-on: #53
2026-09-04 22:38:23 +00:00
Sebastian 48f922e7ad Merge pull request 'chore(deps): update dependency @vue/test-utils to v2.5.0' (#54) from renovate/vue-test-utils-2.x-lockfile into main
Reviewed-on: #54
2026-09-04 22:38:18 +00:00
Sebastian fe5a2c876a Merge pull request 'chore(deps): update dependency vue-router to v5.3.1' (#55) from renovate/vue-router-5.x-lockfile into main
Reviewed-on: #55
2026-09-04 22:38:12 +00:00
Sebastian eef3a76916 chore(deps): update dependency vue-router to v5.3.1
Build Test / test (pull_request) Successful in 3m44s
JS Unit Tests / test (pull_request) Successful in 2m51s
2026-09-04 03:02:57 +00:00
Sebastian 41d0f5a0f9 chore(deps): update dependency @vue/test-utils to v2.5.0
JS Unit Tests / test (pull_request) Successful in 1m15s
Build Test / test (pull_request) Successful in 1m22s
2026-09-03 03:03:17 +00:00
Sebastian ce35df5f31 chore(deps): update vitest monorepo to v4.1.11
Build Test / test (pull_request) Successful in 1m22s
JS Unit Tests / test (pull_request) Successful in 1m20s
2026-09-03 03:03:13 +00:00
Sebastian 00af47f453 chore(deps): update dependency jsdom to v30.0.1
Build Test / test (pull_request) Successful in 51s
JS Unit Tests / test (pull_request) Successful in 54s
2026-09-02 03:02:48 +00:00
12 changed files with 542 additions and 1285 deletions
+336 -953
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -28,9 +28,9 @@
"typescript": "~6.0.0",
"vite": "^8.0.0",
"vue-tsc": "^3.0.5",
"@vitest/coverage-v8": "^4.1.6",
"@vitest/coverage-v8": "^5.0.0",
"@vue/test-utils": "^2.4.10",
"jsdom": "^30.0.0",
"vitest": "^4.1.6"
"vitest": "^5.0.0"
}
}
+79
View File
@@ -0,0 +1,79 @@
<script setup lang="ts">
import { ref, nextTick, onMounted, onBeforeUnmount, watch } from 'vue'
const props = defineProps<{
hasMore: boolean
loading: boolean
}>()
const emit = defineEmits<{
intersect: []
}>()
const sentinelEl = ref<HTMLElement | null>(null)
let observer: IntersectionObserver | null = null
let observerGeneration = 0
function destroyObserver() {
if (observer) {
observer.disconnect()
observer = null
}
}
async function createObserver() {
const generation = ++observerGeneration
destroyObserver()
// The sentinel is conditionally rendered from `hasMore`. Wait until Vue has
// committed that DOM update before reading the template ref.
await nextTick()
if (generation !== observerGeneration || !sentinelEl.value || !props.hasMore) {
return
}
// root: null (viewport) still resolves correctly through clipping/scrolling
// ancestors (including Vuetify's internal v-virtual-scroll container), per the
// IntersectionObserver spec — no need to locate that container explicitly.
observer = new IntersectionObserver((entries) => {
if (entries.some((entry) => entry.isIntersecting) && props.hasMore && !props.loading) {
emit('intersect')
}
}, { root: null, rootMargin: '200px' })
observer.observe(sentinelEl.value)
}
onMounted(() => {
void createObserver()
})
onBeforeUnmount(() => {
observerGeneration++
destroyObserver()
})
watch(
[() => props.hasMore, () => props.loading],
() => {
void createObserver()
},
{ flush: 'post' },
)
</script>
<template>
<div v-if="hasMore" ref="sentinelEl" class="files-load-more-sentinel">
<v-progress-circular v-if="loading" indeterminate size="20" width="2" color="primary" />
</div>
</template>
<style scoped>
.files-load-more-sentinel {
display: flex;
align-items: center;
justify-content: center;
min-height: 48px;
padding: 12px;
width: 100%;
}
</style>
+25 -7
View File
@@ -6,11 +6,11 @@ import { formatSize, formatDate } from '@/utils/fileHelpers'
import FileActionsMenu from '@/components/FileActionsMenu.vue'
import FilePreview from '@/components/FilePreview.vue'
import FilePreviewPopover from '@/components/viewer/FilePreviewPopover.vue'
import FilesLoadMoreSentinel from '@/components/FilesLoadMoreSentinel.vue'
type ItemWithType = {
item: CollectionObject | EntityObject
type: 'collection' | 'entity'
}
type ItemWithType =
| { item: CollectionObject | EntityObject; type: 'collection' | 'entity' }
| { item: null; type: 'sentinel' }
type PreviewSource = (item: { id: string; meta?: Record<string, unknown> }) => string | Blob | Promise<string | Blob>
@@ -18,6 +18,8 @@ const props = defineProps<{
collections: CollectionObject[]
entities: EntityObject[]
selectedIds: Set<string>
hasMore?: boolean
isLoadingMore?: boolean
previewSource?: PreviewSource
thumbnailSource?: PreviewSource
}>()
@@ -30,17 +32,25 @@ const emit = defineEmits<{
'delete': [item: CollectionObject | EntityObject]
'download': [item: CollectionObject | EntityObject]
'show-details': [item: CollectionObject | EntityObject]
'load-more': []
}>()
// Combine collections and entities into a single list for virtual scrolling
// Combine collections and entities into a single list for virtual scrolling; a
// trailing sentinel row (rendered only once virtual-scroll actually mounts it)
// drives the infinite-scroll "load more" trigger.
const allItems = computed<ItemWithType[]>(() => [
...props.collections.map(c => ({ item: c, type: 'collection' as const })),
...props.entities.map(e => ({ item: e, type: 'entity' as const }))
...props.entities.map(e => ({ item: e, type: 'entity' as const })),
...(props.hasMore ? [{ item: null, type: 'sentinel' as const }] : []),
])
function isCollection(wrapped: ItemWithType): wrapped is { item: CollectionObject; type: 'collection' } {
return wrapped.type === 'collection'
}
function isSentinel(wrapped: ItemWithType): wrapped is { item: null; type: 'sentinel' } {
return wrapped.type === 'sentinel'
}
</script>
<template>
@@ -60,9 +70,17 @@ function isCollection(wrapped: ItemWithType): wrapped is { item: CollectionObjec
class="files-details-body"
>
<template #default="{ item: wrapped }">
<!-- Load-more sentinel -->
<FilesLoadMoreSentinel
v-if="isSentinel(wrapped)"
:has-more="!!hasMore"
:loading="!!isLoadingMore"
@intersect="emit('load-more')"
/>
<!-- Folder row -->
<div
v-if="isCollection(wrapped)"
v-else-if="isCollection(wrapped)"
class="files-details-row"
:class="{ 'files-details-row--selected': selectedIds.has(String(wrapped.item.identifier)) }"
@click="emit('item-click', wrapped.item, $event)"
+10
View File
@@ -5,6 +5,7 @@ import { formatSize } from '@/utils/fileHelpers'
import FileActionsMenu from '@/components/FileActionsMenu.vue'
import FilePreview from '@/components/FilePreview.vue'
import FilePreviewPopover from '@/components/viewer/FilePreviewPopover.vue'
import FilesLoadMoreSentinel from '@/components/FilesLoadMoreSentinel.vue'
type PreviewSource = (item: { id: string; meta?: Record<string, unknown> }) => string | Blob | Promise<string | Blob>
@@ -12,6 +13,8 @@ const props = defineProps<{
collections: CollectionObject[]
entities: EntityObject[]
selectedIds: Set<string>
hasMore?: boolean
isLoadingMore?: boolean
previewSource?: PreviewSource
thumbnailSource?: PreviewSource
}>()
@@ -24,6 +27,7 @@ const emit = defineEmits<{
'delete': [item: CollectionObject | EntityObject]
'download': [item: CollectionObject | EntityObject]
'show-details': [item: CollectionObject | EntityObject]
'load-more': []
}>()
</script>
@@ -90,6 +94,12 @@ const emit = defineEmits<{
/>
</div>
</div>
<FilesLoadMoreSentinel
:has-more="!!hasMore"
:loading="!!isLoadingMore"
@intersect="emit('load-more')"
/>
</template>
<style scoped>
+25 -7
View File
@@ -6,11 +6,11 @@ import { formatSize } from '@/utils/fileHelpers'
import FileActionsMenu from '@/components/FileActionsMenu.vue'
import FilePreview from '@/components/FilePreview.vue'
import FilePreviewPopover from '@/components/viewer/FilePreviewPopover.vue'
import FilesLoadMoreSentinel from '@/components/FilesLoadMoreSentinel.vue'
type ItemWithType = {
item: CollectionObject | EntityObject
type: 'collection' | 'entity'
}
type ItemWithType =
| { item: CollectionObject | EntityObject; type: 'collection' | 'entity' }
| { item: null; type: 'sentinel' }
type PreviewSource = (item: { id: string; meta?: Record<string, unknown> }) => string | Blob | Promise<string | Blob>
@@ -18,6 +18,8 @@ const props = defineProps<{
collections: CollectionObject[]
entities: EntityObject[]
selectedIds: Set<string>
hasMore?: boolean
isLoadingMore?: boolean
previewSource?: PreviewSource
thumbnailSource?: PreviewSource
}>()
@@ -30,17 +32,25 @@ const emit = defineEmits<{
'delete': [item: CollectionObject | EntityObject]
'download': [item: CollectionObject | EntityObject]
'show-details': [item: CollectionObject | EntityObject]
'load-more': []
}>()
// Combine collections and entities into a single list for virtual scrolling
// Combine collections and entities into a single list for virtual scrolling; a
// trailing sentinel row (rendered only once virtual-scroll actually mounts it)
// drives the infinite-scroll "load more" trigger.
const allItems = computed<ItemWithType[]>(() => [
...props.collections.map(c => ({ item: c, type: 'collection' as const })),
...props.entities.map(e => ({ item: e, type: 'entity' as const }))
...props.entities.map(e => ({ item: e, type: 'entity' as const })),
...(props.hasMore ? [{ item: null, type: 'sentinel' as const }] : []),
])
function isCollection(wrapped: ItemWithType): wrapped is { item: CollectionObject; type: 'collection' } {
return wrapped.type === 'collection'
}
function isSentinel(wrapped: ItemWithType): wrapped is { item: null; type: 'sentinel' } {
return wrapped.type === 'sentinel'
}
</script>
<template>
@@ -50,9 +60,17 @@ function isCollection(wrapped: ItemWithType): wrapped is { item: CollectionObjec
class="files-list-virtual pa-2"
>
<template #default="{ item: wrapped }">
<!-- Load-more sentinel -->
<FilesLoadMoreSentinel
v-if="isSentinel(wrapped)"
:has-more="!!hasMore"
:loading="!!isLoadingMore"
@intersect="emit('load-more')"
/>
<!-- Folder -->
<v-list-item
v-if="isCollection(wrapped)"
v-else-if="isCollection(wrapped)"
:active="selectedIds.has(String(wrapped.item.identifier))"
@click="($event: MouseEvent | KeyboardEvent) => emit('item-click', wrapped.item, $event)"
>
+3 -3
View File
@@ -1,11 +1,11 @@
/**
* useFileEditor — resolves which registered editor can handle a given MIME type.
*
* Other modules register editors at the `documents_file_editor` integration
* Other modules register editors at the `documents_editor` integration
* point by including an entry in their `integrations.ts`:
*
* ```ts
* documents_file_editor: [{
* documents_editor: [{
* id: 'my_editor',
* meta: { mimeTypes: ['text/plain'] },
* component: () => import('./components/MyEditor.vue'),
@@ -21,7 +21,7 @@
import { useIntegrationStore } from '@KTXC'
import type { EntityObject } from '@DocumentsManager/models/entity'
const INTEGRATION_POINT = 'documents_file_editor'
const INTEGRATION_POINT = 'documents_editor'
function mimeMatchesPattern(mime: string, pattern: string): boolean {
if (pattern.endsWith('/*')) {
+3 -10
View File
@@ -293,9 +293,7 @@ export function useFileUpload(options: UseFileUploadOptions) {
try {
// Create the folder
const collection = await nodesStore.createCollection(
currentProviderId(),
currentServiceId(),
parentId,
`${currentProviderId()}:${currentServiceId()}:${parentId || ROOT_ID}`,
{ label: folderName, owner: '' }
)
folderIdMap.set(folderPath, String(collection.identifier))
@@ -342,9 +340,7 @@ export function useFileUpload(options: UseFileUploadOptions) {
// Create the entity
const entity = await nodesStore.createEntity(
currentProviderId(),
currentServiceId(),
targetCollection || ROOT_ID,
`${currentProviderId()}:${currentServiceId()}:${targetCollection || ROOT_ID}`,
{
'@type': 'documents.properties',
urid: null,
@@ -360,10 +356,7 @@ export function useFileUpload(options: UseFileUploadOptions) {
// Write the content
await nodesStore.writeEntity(
currentProviderId(),
currentServiceId(),
targetCollection || ROOT_ID,
String(entity.identifier),
`${entity.provider}:${entity.service}:${entity.collection || ROOT_ID}:${entity.identifier}`,
content
)
+58 -79
View File
@@ -1,9 +1,10 @@
<script setup lang="ts">
import { ref, computed, onMounted, watch, nextTick, defineAsyncComponent } from 'vue'
import { ref, computed, onMounted, onUnmounted, watch, nextTick, defineAsyncComponent } from 'vue'
import { useDisplay } from 'vuetify'
import { useModuleStore, useIntegrationStore, usePreviewStore } from '@KTXC'
import { useDocumentManager, useDocumentSelection, useFileUpload, useFileEditor } from '@/composables'
import type { ViewMode, SortField, SortOrder, BreadcrumbItem } from '@/types'
import type { ListFilter, ListSort } from '@DocumentsManager/types'
import { CollectionObject } from '@DocumentsManager/models/collection'
import { EntityObject } from '@DocumentsManager/models/entity'
import { ServiceObject } from '@DocumentsManager/models/service'
@@ -118,14 +119,14 @@ const mediaViewerDialog = computed(() => {
})
const viewerItems = computed<ViewerItem[]>(() =>
sortedItems.value.entities.map(entityToViewerItem),
documentManager.currentEntities.value.map(entityToViewerItem),
)
function entityToViewerItem(entity: EntityObject): ViewerItem {
return {
id: String(entity.identifier ?? ''),
title: entity.properties.label ?? String(entity.identifier ?? ''),
mime: entity.properties.mime ?? '',
mime: 'image/webp',
meta: { collection: entity.collection ? String(entity.collection) : null },
}
}
@@ -294,76 +295,45 @@ const breadcrumbs = computed<BreadcrumbItem[]>(() => {
return items
})
const sortedItems = computed(() => {
const collections = [...documentManager.currentCollections.value]
const entities = [...documentManager.currentEntities.value]
// Sort collections
collections.sort((a, b) => {
const aVal = sortField.value === 'label'
? (a.properties.label || nodeId(a))
: sortField.value === 'modifiedOn'
? (a.modified?.getTime() ?? 0)
: sortField.value === 'createdOn'
? (a.created?.getTime() ?? 0)
: ''
const bVal = sortField.value === 'label'
? (b.properties.label || nodeId(b))
: sortField.value === 'modifiedOn'
? (b.modified?.getTime() ?? 0)
: sortField.value === 'createdOn'
? (b.created?.getTime() ?? 0)
: ''
const cmp = String(aVal).localeCompare(String(bVal))
return sortOrder.value === 'asc' ? cmp : -cmp
})
// Sort entities
entities.sort((a, b) => {
const aVal = sortField.value === 'label'
? (a.properties.label || nodeId(a))
: sortField.value === 'mime'
? (a.properties.mime || '')
: sortField.value === 'size'
? a.properties.size
: sortField.value === 'modifiedOn'
? (a.modified?.getTime() ?? 0)
: sortField.value === 'createdOn'
? (a.created?.getTime() ?? 0)
: ''
const bVal = sortField.value === 'label'
? (b.properties.label || nodeId(b))
: sortField.value === 'mime'
? (b.properties.mime || '')
: sortField.value === 'size'
? b.properties.size
: sortField.value === 'modifiedOn'
? (b.modified?.getTime() ?? 0)
: sortField.value === 'createdOn'
? (b.created?.getTime() ?? 0)
: ''
const cmp = String(aVal).localeCompare(String(bVal))
return sortOrder.value === 'asc' ? cmp : -cmp
})
// Filter by search
const filterFn = (item: CollectionObject | EntityObject) => {
if (!searchQuery.value) return true
const label = item instanceof EntityObject
? (item.properties.label || nodeId(item))
: (item.properties.label || nodeId(item))
return label.toLowerCase().includes(searchQuery.value.toLowerCase())
}
return {
collections: collections.filter(filterFn),
entities: entities.filter(filterFn),
const hasItems = computed(() =>
documentManager.currentCollections.value.length > 0 || documentManager.currentEntities.value.length > 0
)
// Search/sort are server-driven: build the current filter/sort and reset-load
// the current location whenever either changes.
function buildFilter(): ListFilter | undefined {
return searchQuery.value ? { label: searchQuery.value } : undefined
}
function buildSort(): ListSort {
return { [sortField.value]: sortOrder.value === 'asc' }
}
async function reload() {
await documentManager.resetAndLoad(buildFilter(), buildSort())
}
let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null
watch(searchQuery, () => {
if (searchDebounceTimer !== null) {
clearTimeout(searchDebounceTimer)
}
searchDebounceTimer = setTimeout(() => {
searchDebounceTimer = null
reload()
}, 300)
})
const hasItems = computed(() =>
sortedItems.value.collections.length > 0 || sortedItems.value.entities.length > 0
)
watch([sortField, sortOrder], () => {
reload()
})
onUnmounted(() => {
if (searchDebounceTimer !== null) {
clearTimeout(searchDebounceTimer)
}
})
const hasSelection = computed(() => selection.hasSelection.value)
@@ -695,7 +665,7 @@ async function readEntriesRecursively(
async function handleUploadAll() {
try {
await upload.uploadAll()
await documentManager.refresh()
await reload()
notify('Uploads completed', 'success')
} catch (error) {
console.error('Failed to upload files:', error)
@@ -712,7 +682,7 @@ function handleUploadDialogClose() {
// Refresh
async function handleRefresh() {
try {
await documentManager.refresh()
await reload()
} catch (error) {
console.error('Failed to refresh files:', error)
notify(getErrorMessage(error, 'Failed to refresh files'), 'error')
@@ -725,7 +695,7 @@ onMounted(async () => {
await documentManager.initialize()
await servicesStore.list()
syncActiveService(availableServices.value)
await documentManager.refresh()
await reload()
console.log('[Files] - Initialized with:', {
provider: activeProviderId.value,
service: activeServiceId.value,
@@ -818,9 +788,11 @@ onMounted(async () => {
<!-- Grid View -->
<FilesGridView
v-else-if="viewMode === 'grid'"
:collections="sortedItems.collections"
:entities="sortedItems.entities"
:collections="documentManager.currentCollections.value"
:entities="documentManager.currentEntities.value"
:selected-ids="selectedIds"
:has-more="documentManager.hasMore.value"
:is-loading-more="documentManager.isLoadingMore.value"
:preview-source="previewsAvailable ? resolvePreviewSource : undefined"
:thumbnail-source="previewsAvailable ? resolveThumbnailSource : undefined"
@item-click="handleItemClick"
@@ -830,14 +802,17 @@ onMounted(async () => {
@delete="handleDeleteItem"
@download="handleDownloadItem"
@show-details="handleShowDetails"
@load-more="documentManager.loadMore"
/>
<!-- List View -->
<FilesListView
v-else-if="viewMode === 'list'"
:collections="sortedItems.collections"
:entities="sortedItems.entities"
:collections="documentManager.currentCollections.value"
:entities="documentManager.currentEntities.value"
:selected-ids="selectedIds"
:has-more="documentManager.hasMore.value"
:is-loading-more="documentManager.isLoadingMore.value"
:preview-source="previewsAvailable ? resolvePreviewSource : undefined"
:thumbnail-source="previewsAvailable ? resolveThumbnailSource : undefined"
@item-click="handleItemClick"
@@ -847,14 +822,17 @@ onMounted(async () => {
@delete="handleDeleteItem"
@download="handleDownloadItem"
@show-details="handleShowDetails"
@load-more="documentManager.loadMore"
/>
<!-- Details View -->
<FilesDetailsView
v-else-if="viewMode === 'details'"
:collections="sortedItems.collections"
:entities="sortedItems.entities"
:collections="documentManager.currentCollections.value"
:entities="documentManager.currentEntities.value"
:selected-ids="selectedIds"
:has-more="documentManager.hasMore.value"
:is-loading-more="documentManager.isLoadingMore.value"
:preview-source="previewsAvailable ? resolvePreviewSource : undefined"
:thumbnail-source="previewsAvailable ? resolveThumbnailSource : undefined"
@item-click="handleItemClick"
@@ -864,6 +842,7 @@ onMounted(async () => {
@delete="handleDeleteItem"
@download="handleDownloadItem"
@show-details="handleShowDetails"
@load-more="documentManager.loadMore"
/>
</div>
</div>
-222
View File
@@ -1,222 +0,0 @@
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
import { useProvidersStore, useServicesStore, useNodesStore, ROOT_ID } from '@DocumentsManager/stores'
import { CollectionObject } from '@DocumentsManager/models/collection'
import { EntityObject } from '@DocumentsManager/models/entity'
import type { SortField, SortOrder, ViewMode, BreadcrumbItem } from '@/types'
type NodeRecord = CollectionObject | EntityObject
export const useDocumentsStore = defineStore('documentsStore', () => {
const providersStore = useProvidersStore()
const servicesStore = useServicesStore()
const nodesStore = useNodesStore()
const activeProviderId = ref('default')
const activeServiceId = ref<string | number>('personal')
const currentLocation = ref<string>(ROOT_ID)
const viewMode = ref<ViewMode>('grid')
const sortField = ref<SortField>('label')
const sortOrder = ref<SortOrder>('asc')
const searchQuery = ref('')
const sidebarVisible = ref(true)
const transceiving = computed(() =>
providersStore.transceiving || servicesStore.transceiving || nodesStore.transceiving,
)
const provider = computed(() => providersStore.provider(activeProviderId.value))
const service = computed(() => servicesStore.service(activeProviderId.value, activeServiceId.value))
const currentCollections = computed(() =>
nodesStore.getChildCollections(activeProviderId.value, activeServiceId.value, currentLocation.value),
)
const currentEntities = computed(() =>
nodesStore.getChildEntities(activeProviderId.value, activeServiceId.value, currentLocation.value),
)
const breadcrumbs = computed<BreadcrumbItem[]>(() => {
const nodes = nodesStore.getPath(activeProviderId.value, activeServiceId.value, currentLocation.value)
return [
{ id: ROOT_ID, label: 'Home', isRoot: true },
...nodes.map((node) => ({
id: String(node.identifier),
label: node.properties.label,
isRoot: false,
})),
]
})
function sortCollections(items: CollectionObject[]): CollectionObject[] {
return [...items].sort((a, b) => {
const aVal = sortField.value === 'label'
? a.properties.label
: sortField.value === 'modifiedOn'
? (a.modified?.getTime() ?? 0)
: sortField.value === 'createdOn'
? (a.created?.getTime() ?? 0)
: ''
const bVal = sortField.value === 'label'
? b.properties.label
: sortField.value === 'modifiedOn'
? (b.modified?.getTime() ?? 0)
: sortField.value === 'createdOn'
? (b.created?.getTime() ?? 0)
: ''
const cmp = String(aVal).localeCompare(String(bVal))
return sortOrder.value === 'asc' ? cmp : -cmp
})
}
function sortEntities(items: EntityObject[]): EntityObject[] {
return [...items].sort((a, b) => {
const aVal = sortField.value === 'label'
? a.properties.label
: sortField.value === 'mime'
? (a.properties.mime ?? '')
: sortField.value === 'size'
? a.properties.size
: sortField.value === 'modifiedOn'
? (a.modified?.getTime() ?? 0)
: sortField.value === 'createdOn'
? (a.created?.getTime() ?? 0)
: ''
const bVal = sortField.value === 'label'
? b.properties.label
: sortField.value === 'mime'
? (b.properties.mime ?? '')
: sortField.value === 'size'
? b.properties.size
: sortField.value === 'modifiedOn'
? (b.modified?.getTime() ?? 0)
: sortField.value === 'createdOn'
? (b.created?.getTime() ?? 0)
: ''
const cmp = String(aVal).localeCompare(String(bVal))
return sortOrder.value === 'asc' ? cmp : -cmp
})
}
function filterNode(node: NodeRecord): boolean {
if (!searchQuery.value) return true
return node.properties.label.toLowerCase().includes(searchQuery.value.toLowerCase())
}
const sortedItems = computed(() => ({
collections: sortCollections(currentCollections.value).filter(filterNode),
entities: sortEntities(currentEntities.value).filter(filterNode),
}))
const hasItems = computed(() =>
sortedItems.value.collections.length > 0 || sortedItems.value.entities.length > 0,
)
const isAtRoot = computed(() => currentLocation.value === ROOT_ID)
async function initialize() {
await providersStore.list([activeProviderId.value])
await servicesStore.list()
await refresh()
}
async function refresh() {
await nodesStore.fetchNodes(activeProviderId.value, activeServiceId.value, currentLocation.value)
}
async function navigateTo(collectionId: string | null) {
currentLocation.value = collectionId || ROOT_ID
await refresh()
}
async function navigateToRoot() {
await navigateTo(ROOT_ID)
}
async function navigateUp() {
if (currentLocation.value === ROOT_ID) return
const currentNode = nodesStore.getNode(activeProviderId.value, activeServiceId.value, currentLocation.value)
if (!currentNode) return
const parentId = currentNode.collection ? String(currentNode.collection) : ROOT_ID
await navigateTo(parentId)
}
async function createFolder(label: string) {
await nodesStore.createCollection(
activeProviderId.value,
activeServiceId.value,
currentLocation.value,
{ label, owner: '' },
)
await refresh()
}
async function renameNode(node: NodeRecord, label: string) {
if (node instanceof CollectionObject) {
await nodesStore.updateCollection(
activeProviderId.value,
activeServiceId.value,
node.identifier,
{ label, owner: node.properties.owner },
)
} else {
await nodesStore.updateEntity(
activeProviderId.value,
activeServiceId.value,
node.collection,
node.identifier,
{
'@type': 'documents.properties',
urid: node.properties.urid,
size: node.properties.size,
label,
mime: node.properties.mime,
format: node.properties.format,
encoding: node.properties.encoding,
},
)
}
await refresh()
}
async function deleteNode(node: NodeRecord) {
if (node instanceof CollectionObject) {
await nodesStore.deleteCollection(activeProviderId.value, activeServiceId.value, node.identifier)
} else {
await nodesStore.deleteEntity(activeProviderId.value, activeServiceId.value, node.collection, node.identifier)
}
await refresh()
}
return {
activeProviderId,
activeServiceId,
currentLocation,
viewMode,
sortField,
sortOrder,
searchQuery,
sidebarVisible,
transceiving,
provider,
service,
currentCollections,
currentEntities,
sortedItems,
hasItems,
breadcrumbs,
isAtRoot,
initialize,
refresh,
navigateTo,
navigateToRoot,
navigateUp,
createFolder,
renameNode,
deleteNode,
ROOT_ID,
}
})
-1
View File
@@ -1 +0,0 @@
export { useDocumentsStore } from './documentsStore'
+1 -1
View File
@@ -4,7 +4,7 @@
export type ViewMode = 'grid' | 'list' | 'details'
export type SortField = 'label' | 'size' | 'modifiedOn' | 'createdOn' | 'mime'
export type SortField = 'label' | 'size' | 'modifiedOn' | 'createdOn'
export type SortOrder = 'asc' | 'desc'