diff --git a/src/composables/index.ts b/src/composables/index.ts index 9992cfa..81bce87 100644 --- a/src/composables/index.ts +++ b/src/composables/index.ts @@ -1,13 +1,18 @@ /** * Central export point for all Files module composables + * + * useDocumentManager/useDocumentSelection live in documents_manager (they only + * ever depended on its SDK, never on this module's own code) so other modules + * can reuse them too; re-exported here so existing imports from + * '@/composables' keep working unchanged. */ -export { useFileManager } from './useFileManager' -export { useFileSelection } from './useFileSelection' +export { useDocumentManager } from '@DocumentsManager/composables/useDocumentManager' +export { useDocumentSelection } from '@DocumentsManager/composables/useDocumentSelection' export { useFileUpload } from './useFileUpload' export { useFileEditor } from './useFileEditor' -export type { UseFileManagerOptions } from './useFileManager' -export type { UseFileSelectionOptions } from './useFileSelection' +export type { UseDocumentManagerOptions } from '@DocumentsManager/composables/useDocumentManager' +export type { UseDocumentSelectionOptions } from '@DocumentsManager/composables/useDocumentSelection' export type { UseFileUploadOptions, FileUploadProgress } from './useFileUpload' export type { UseFileEditorReturn } from './useFileEditor' diff --git a/src/composables/useFileManager.ts b/src/composables/useFileManager.ts deleted file mode 100644 index 2a1cbda..0000000 --- a/src/composables/useFileManager.ts +++ /dev/null @@ -1,295 +0,0 @@ -/** - * 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 '@DocumentsManager/stores/providersStore' -import { useServicesStore } from '@DocumentsManager/stores/servicesStore' -import { useNodesStore, ROOT_ID } from '@DocumentsManager/stores/nodesStore' -import type { ListFilter, ListSort, ListRange } from '@DocumentsManager/types/common' -import type { DocumentInterface } from '@DocumentsManager/types/document' -import { CollectionObject } from '@DocumentsManager/models/collection' -import { EntityObject } from '@DocumentsManager/models/entity' - -// Base URL for file manager transfer endpoints -const TRANSFER_BASE_URL = '/m/documents_manager' - -export interface UseFileManagerOptions { - providerId: string | Ref | ComputedRef - serviceId: string | Ref | ComputedRef - autoFetch?: boolean -} - -export function useFileManager(options: UseFileManagerOptions) { - 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 = 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 = computed(() => - nodesStore.getChildCollections(currentProviderId(), currentServiceId(), currentLocation.value) - ) - - const currentEntities: ComputedRef = 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 => { - 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 => { - 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 => { - 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 => { - 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 => { - 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 useFileManager diff --git a/src/composables/useFileSelection.ts b/src/composables/useFileSelection.ts deleted file mode 100644 index 8e6a149..0000000 --- a/src/composables/useFileSelection.ts +++ /dev/null @@ -1,135 +0,0 @@ -/** - * File selection composable - * Provides reactive selection management for file manager - */ - -import { ref, computed } from 'vue' -import type { Ref, ComputedRef } from 'vue' -import { CollectionObject } from '@DocumentsManager/models/collection' -import { EntityObject } from '@DocumentsManager/models/entity' - -type NodeRecord = CollectionObject | EntityObject - -export interface UseFileSelectionOptions { - multiple?: boolean - allowFolders?: boolean - allowFiles?: boolean -} - -export function useFileSelection(options: UseFileSelectionOptions = {}) { - const { - multiple = true, - allowFolders = true, - allowFiles = true, - } = options - - const selectedIds: Ref> = ref(new Set()) - const selectedNodes: Ref> = ref(new Map()) - - const count: ComputedRef = computed(() => selectedIds.value.size) - const hasSelection: ComputedRef = computed(() => selectedIds.value.size > 0) - - const selectedIdArray: ComputedRef = computed(() => Array.from(selectedIds.value)) - const selectedNodeArray: ComputedRef = computed(() => Array.from(selectedNodes.value.values())) - - const selectedCollections: ComputedRef = computed(() => - selectedNodeArray.value.filter((node): node is CollectionObject => node instanceof CollectionObject), - ) - - const selectedEntities: ComputedRef = 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 useFileSelection diff --git a/src/pages/Main.vue b/src/pages/Main.vue index c717572..500d632 100644 --- a/src/pages/Main.vue +++ b/src/pages/Main.vue @@ -2,7 +2,7 @@ import { ref, computed, onMounted, watch, nextTick, defineAsyncComponent } from 'vue' import { useDisplay } from 'vuetify' import { useModuleStore, useIntegrationStore } from '@KTXC' -import { useFileManager, useFileSelection, useFileUpload, useFileEditor } from '@/composables' +import { useDocumentManager, useDocumentSelection, useFileUpload, useFileEditor } from '@/composables' import type { ViewMode, SortField, SortOrder, BreadcrumbItem } from '@/types' import { CollectionObject } from '@DocumentsManager/models/collection' import { EntityObject } from '@DocumentsManager/models/entity' @@ -42,13 +42,13 @@ const activeProviderId = ref('default') const activeServiceId = ref('personal') // File manager composable -const fileManager = useFileManager({ +const documentManager = useDocumentManager({ providerId: activeProviderId, serviceId: activeServiceId, }) // Selection composable -const selection = useFileSelection({ multiple: true }) +const selection = useDocumentSelection({ multiple: true }) // Upload composable const upload = useFileUpload({ @@ -57,11 +57,11 @@ const upload = useFileUpload({ }) // Keep upload collection in sync with current location -watch(() => fileManager.currentLocation.value, (newLocation) => { +watch(() => documentManager.currentLocation.value, (newLocation) => { upload.setCollection(newLocation) }) -watch(() => fileManager.error.value, (message) => { +watch(() => documentManager.error.value, (message) => { if (message) { notify(message, 'error') } @@ -129,11 +129,11 @@ function entityToViewerItem(entity: EntityObject): ViewerItem { } function resolveViewerSource(item: ViewerItem): string { - return fileManager.getEntityUrl(item.id, (item.meta?.collection as string | null) ?? null) + return documentManager.getEntityUrl(item.id, (item.meta?.collection as string | null) ?? null) } function downloadViewerItem(item: ViewerItem): void { - fileManager.downloadEntity(item.id, (item.meta?.collection as string | null) ?? null) + documentManager.downloadEntity(item.id, (item.meta?.collection as string | null) ?? null) } // Editor state @@ -241,10 +241,10 @@ function renameCurrentName(node: CollectionObject | EntityObject | null): string // Computed const breadcrumbs = computed(() => { const items: BreadcrumbItem[] = [ - { id: fileManager.ROOT_ID, label: 'Home', isRoot: true } + { id: documentManager.ROOT_ID, label: 'Home', isRoot: true } ] - for (const node of fileManager.breadcrumbs.value) { + for (const node of documentManager.breadcrumbs.value) { items.push({ id: nodeId(node), label: nodeLabel(node), @@ -256,8 +256,8 @@ const breadcrumbs = computed(() => { }) const sortedItems = computed(() => { - const collections = [...fileManager.currentCollections.value] - const entities = [...fileManager.currentEntities.value] + const collections = [...documentManager.currentCollections.value] + const entities = [...documentManager.currentEntities.value] // Sort collections collections.sort((a, b) => { @@ -375,8 +375,8 @@ watch([activeProviderId, activeServiceId], async ([providerId, serviceId], [prev } selection.clear() - upload.setCollection(fileManager.rootId.value) - await fileManager.navigateToRoot() + upload.setCollection(documentManager.rootId.value) + await documentManager.navigateToRoot() }, { flush: 'post' }) async function handleServiceSelect(service: ServiceObject) { @@ -398,12 +398,12 @@ async function handleServiceSelect(service: ServiceObject) { // Navigation methods async function handleBreadcrumbNavigate(item: BreadcrumbItem) { selection.clear() - await fileManager.navigateTo(item.isRoot ? null : item.id) + await documentManager.navigateTo(item.isRoot ? null : item.id) } async function handleFolderOpen(folder: CollectionObject) { selection.clear() - await fileManager.navigateTo(nodeId(folder)) + await documentManager.navigateTo(nodeId(folder)) } // Item interaction methods @@ -440,7 +440,7 @@ function handleShowDetails(item: CollectionObject | EntityObject) { // Folder operations async function handleCreateFolder(name: string) { try { - await fileManager.createFolder(name) + await documentManager.createFolder(name) showNewFolderDialog.value = false notify(`Folder "${name}" created`, 'success') } catch (error) { @@ -460,7 +460,7 @@ async function handleRename(newName: string) { const currentName = renameCurrentName(nodeToRename.value) try { - await fileManager.renameNode(nodeId(nodeToRename.value), newName) + await documentManager.renameNode(nodeId(nodeToRename.value), newName) showRenameDialog.value = false nodeToRename.value = null notify(`Renamed "${currentName}" to "${newName}"`, 'success') @@ -481,7 +481,7 @@ async function handleDelete() { try { for (const node of nodesToDelete.value) { - await fileManager.deleteNode(nodeId(node)) + await documentManager.deleteNode(nodeId(node)) } selection.clear() showDeleteDialog.value = false @@ -500,11 +500,11 @@ async function handleDelete() { function handleDownloadItem(item: CollectionObject | EntityObject) { if (item instanceof EntityObject) { // Download single file - fileManager.downloadEntity(nodeId(item), String(item.collection ?? fileManager.ROOT_ID)) + documentManager.downloadEntity(nodeId(item), String(item.collection ?? documentManager.ROOT_ID)) notify(`Download started for "${nodeLabel(item)}"`, 'info') } else if (item instanceof CollectionObject) { // Download folder as ZIP - fileManager.downloadCollection(nodeId(item)) + documentManager.downloadCollection(nodeId(item)) notify(`Archive download started for "${nodeLabel(item)}"`, 'info') } } @@ -656,7 +656,7 @@ async function readEntriesRecursively( async function handleUploadAll() { try { await upload.uploadAll() - await fileManager.refresh() + await documentManager.refresh() notify('Uploads completed', 'success') } catch (error) { console.error('Failed to upload files:', error) @@ -673,7 +673,7 @@ function handleUploadDialogClose() { // Refresh async function handleRefresh() { try { - await fileManager.refresh() + await documentManager.refresh() } catch (error) { console.error('Failed to refresh files:', error) notify(getErrorMessage(error, 'Failed to refresh files'), 'error') @@ -683,10 +683,10 @@ async function handleRefresh() { // Initialize onMounted(async () => { try { - await fileManager.initialize() + await documentManager.initialize() await servicesStore.list() syncActiveService(availableServices.value) - await fileManager.refresh() + await documentManager.refresh() console.log('[Files] - Initialized with:', { provider: activeProviderId.value, service: activeServiceId.value, @@ -723,7 +723,7 @@ onMounted(async () => { { :active-provider-id="activeProviderId" :active-service-id="activeServiceId" :services="availableServices" - @navigate-home="fileManager.navigateToRoot()" + @navigate-home="documentManager.navigateToRoot()" @select-service="handleServiceSelect" @new-folder="showNewFolderDialog = true" @open-file-picker="openFilePicker" @@ -766,7 +766,7 @@ onMounted(async () => { {
- + {