refactor: standardize design
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -5,7 +5,9 @@
|
||||
export { useFileManager } from './useFileManager'
|
||||
export { useFileSelection } from './useFileSelection'
|
||||
export { useFileUpload } from './useFileUpload'
|
||||
export { useFileViewer } from './useFileViewer'
|
||||
|
||||
export type { UseFileManagerOptions } from './useFileManager'
|
||||
export type { UseFileSelectionOptions } from './useFileSelection'
|
||||
export type { UseFileUploadOptions, FileUploadProgress } from './useFileUpload'
|
||||
export type { UseFileViewerReturn } from './useFileViewer'
|
||||
|
||||
@@ -5,15 +5,16 @@
|
||||
|
||||
import { computed, ref } from 'vue'
|
||||
import type { Ref, ComputedRef } from 'vue'
|
||||
import { useProvidersStore } from '@FilesManager/stores/providersStore'
|
||||
import { useServicesStore } from '@FilesManager/stores/servicesStore'
|
||||
import { useNodesStore, ROOT_ID } from '@FilesManager/stores/nodesStore'
|
||||
import type { FilterCondition, SortCondition, RangeCondition } from '@FilesManager/types/common'
|
||||
import { FileCollectionObject } from '@FilesManager/models/collection'
|
||||
import { FileEntityObject } from '@FilesManager/models/entity'
|
||||
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/file_manager'
|
||||
const TRANSFER_BASE_URL = '/m/documents_manager'
|
||||
|
||||
export interface UseFileManagerOptions {
|
||||
providerId: string
|
||||
@@ -32,24 +33,24 @@ export function useFileManager(options: UseFileManagerOptions) {
|
||||
const currentLocation: Ref<string> = ref(ROOT_ID)
|
||||
|
||||
// Loading/error state
|
||||
const isLoading = computed(() => nodesStore.loading)
|
||||
const isLoading = computed(() => nodesStore.transceiving)
|
||||
const error = computed(() => nodesStore.error)
|
||||
|
||||
// Provider and service
|
||||
const provider = computed(() => providersStore.getProvider(providerId))
|
||||
const service = computed(() => servicesStore.getService(providerId, serviceId))
|
||||
const rootId = computed(() => servicesStore.getRootId(providerId, serviceId) || ROOT_ID)
|
||||
const provider = computed(() => providersStore.provider(providerId))
|
||||
const service = computed(() => servicesStore.service(providerId, serviceId))
|
||||
const rootId = computed(() => ROOT_ID)
|
||||
|
||||
// Current children
|
||||
const currentChildren = computed(() =>
|
||||
nodesStore.getChildren(providerId, serviceId, currentLocation.value)
|
||||
)
|
||||
|
||||
const currentCollections: ComputedRef<FileCollectionObject[]> = computed(() =>
|
||||
const currentCollections: ComputedRef<CollectionObject[]> = computed(() =>
|
||||
nodesStore.getChildCollections(providerId, serviceId, currentLocation.value)
|
||||
)
|
||||
|
||||
const currentEntities: ComputedRef<FileEntityObject[]> = computed(() =>
|
||||
const currentEntities: ComputedRef<EntityObject[]> = computed(() =>
|
||||
nodesStore.getChildEntities(providerId, serviceId, currentLocation.value)
|
||||
)
|
||||
|
||||
@@ -77,7 +78,7 @@ export function useFileManager(options: UseFileManagerOptions) {
|
||||
}
|
||||
const currentNode = nodesStore.getNode(providerId, serviceId, currentLocation.value)
|
||||
if (currentNode) {
|
||||
await navigateTo(currentNode.in || ROOT_ID)
|
||||
await navigateTo(currentNode.collection ? String(currentNode.collection) : ROOT_ID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,15 +89,14 @@ export function useFileManager(options: UseFileManagerOptions) {
|
||||
|
||||
// Refresh current location
|
||||
const refresh = async (
|
||||
filter?: FilterCondition[] | null,
|
||||
sort?: SortCondition[] | null,
|
||||
range?: RangeCondition | null
|
||||
filter?: ListFilter,
|
||||
sort?: ListSort,
|
||||
range?: ListRange
|
||||
) => {
|
||||
await nodesStore.fetchNodes(
|
||||
providerId,
|
||||
serviceId,
|
||||
currentLocation.value === ROOT_ID ? null : currentLocation.value,
|
||||
false,
|
||||
currentLocation.value === ROOT_ID ? ROOT_ID : currentLocation.value,
|
||||
filter,
|
||||
sort,
|
||||
range
|
||||
@@ -104,12 +104,12 @@ export function useFileManager(options: UseFileManagerOptions) {
|
||||
}
|
||||
|
||||
// Create a new folder
|
||||
const createFolder = async (label: string): Promise<FileCollectionObject> => {
|
||||
const createFolder = async (label: string): Promise<CollectionObject> => {
|
||||
return await nodesStore.createCollection(
|
||||
providerId,
|
||||
serviceId,
|
||||
currentLocation.value === ROOT_ID ? ROOT_ID : currentLocation.value,
|
||||
{ label }
|
||||
{ label, owner: '' }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -117,12 +117,22 @@ export function useFileManager(options: UseFileManagerOptions) {
|
||||
const createFile = async (
|
||||
label: string,
|
||||
mime: string = 'application/octet-stream'
|
||||
): Promise<FileEntityObject> => {
|
||||
): Promise<EntityObject> => {
|
||||
const properties: DocumentInterface = {
|
||||
'@type': 'documents.properties',
|
||||
urid: null,
|
||||
size: 0,
|
||||
label,
|
||||
mime,
|
||||
format: null,
|
||||
encoding: null,
|
||||
}
|
||||
|
||||
return await nodesStore.createEntity(
|
||||
providerId,
|
||||
serviceId,
|
||||
currentLocation.value === ROOT_ID ? ROOT_ID : currentLocation.value,
|
||||
{ label, mime }
|
||||
properties
|
||||
)
|
||||
}
|
||||
|
||||
@@ -133,10 +143,22 @@ export function useFileManager(options: UseFileManagerOptions) {
|
||||
throw new Error('Node not found')
|
||||
}
|
||||
|
||||
if (node['@type'] === 'files.collection') {
|
||||
return await nodesStore.modifyCollection(providerId, serviceId, nodeId, { label: newLabel })
|
||||
if (node instanceof CollectionObject) {
|
||||
return await nodesStore.updateCollection(providerId, serviceId, nodeId, {
|
||||
label: newLabel,
|
||||
owner: node.properties.owner,
|
||||
})
|
||||
} else {
|
||||
return await nodesStore.modifyEntity(providerId, serviceId, node.in, nodeId, { label: newLabel })
|
||||
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(providerId, serviceId, node.collection, nodeId, properties)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,61 +169,35 @@ export function useFileManager(options: UseFileManagerOptions) {
|
||||
throw new Error('Node not found')
|
||||
}
|
||||
|
||||
if (node['@type'] === 'files.collection') {
|
||||
return await nodesStore.destroyCollection(providerId, serviceId, nodeId)
|
||||
if (node instanceof CollectionObject) {
|
||||
return await nodesStore.deleteCollection(providerId, serviceId, nodeId)
|
||||
} else {
|
||||
return await nodesStore.destroyEntity(providerId, serviceId, node.in, nodeId)
|
||||
}
|
||||
}
|
||||
|
||||
// Copy a node
|
||||
const copyNode = async (nodeId: string, destinationId?: string | null) => {
|
||||
const node = nodesStore.getNode(providerId, serviceId, nodeId)
|
||||
if (!node) {
|
||||
throw new Error('Node not found')
|
||||
}
|
||||
|
||||
const destination = destinationId ?? currentLocation.value
|
||||
|
||||
if (node['@type'] === 'files.collection') {
|
||||
return await nodesStore.copyCollection(providerId, serviceId, nodeId, destination)
|
||||
} else {
|
||||
return await nodesStore.copyEntity(providerId, serviceId, node.in, nodeId, destination)
|
||||
}
|
||||
}
|
||||
|
||||
// Move a node
|
||||
const moveNode = async (nodeId: string, destinationId?: string | null) => {
|
||||
const node = nodesStore.getNode(providerId, serviceId, nodeId)
|
||||
if (!node) {
|
||||
throw new Error('Node not found')
|
||||
}
|
||||
|
||||
const destination = destinationId ?? currentLocation.value
|
||||
|
||||
if (node['@type'] === 'files.collection') {
|
||||
return await nodesStore.moveCollection(providerId, serviceId, nodeId, destination)
|
||||
} else {
|
||||
return await nodesStore.moveEntity(providerId, serviceId, node.in, nodeId, destination)
|
||||
return await nodesStore.deleteEntity(providerId, serviceId, node.collection, nodeId)
|
||||
}
|
||||
}
|
||||
|
||||
// Read file content
|
||||
const readFile = async (entityId: string): Promise<string | null> => {
|
||||
const node = nodesStore.getNode(providerId, serviceId, entityId)
|
||||
if (!node || node['@type'] !== 'files.entity') {
|
||||
if (!node || !(node instanceof EntityObject)) {
|
||||
throw new Error('Entity not found')
|
||||
}
|
||||
return await nodesStore.readEntity(providerId, serviceId, node.in || ROOT_ID, entityId)
|
||||
return await nodesStore.readEntity(providerId, serviceId, node.collection || ROOT_ID, entityId)
|
||||
}
|
||||
|
||||
// Write file content
|
||||
const writeFile = async (entityId: string, content: string): Promise<number> => {
|
||||
const node = nodesStore.getNode(providerId, serviceId, entityId)
|
||||
if (!node || node['@type'] !== 'files.entity') {
|
||||
if (!node || !(node instanceof EntityObject)) {
|
||||
throw new Error('Entity not found')
|
||||
}
|
||||
return await nodesStore.writeEntity(providerId, serviceId, node.in, entityId, content)
|
||||
return await nodesStore.writeEntity(providerId, serviceId, 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(providerId)}/${encodeURIComponent(serviceId)}/${encodeURIComponent(collection)}/${encodeURIComponent(entityId)}`
|
||||
}
|
||||
|
||||
// Download a single file
|
||||
@@ -214,7 +210,6 @@ export function useFileManager(options: UseFileManagerOptions) {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
// Download a collection (folder) as ZIP
|
||||
const downloadCollection = (collectionId: string): void => {
|
||||
// Use path parameters: /download/collection/{provider}/{service}/{identifier}
|
||||
const url = `${TRANSFER_BASE_URL}/download/collection/${encodeURIComponent(providerId)}/${encodeURIComponent(serviceId)}/${encodeURIComponent(collectionId)}`
|
||||
@@ -222,7 +217,6 @@ export function useFileManager(options: UseFileManagerOptions) {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
// Download multiple items as ZIP archive
|
||||
const downloadArchive = (ids: string[], name: string = 'download', collectionId?: string | null): void => {
|
||||
const collection = collectionId ?? currentLocation.value
|
||||
const params = new URLSearchParams({
|
||||
@@ -244,12 +238,8 @@ export function useFileManager(options: UseFileManagerOptions) {
|
||||
|
||||
// Initialize - fetch providers, services, and initial nodes if autoFetch
|
||||
const initialize = async () => {
|
||||
if (!providersStore.initialized) {
|
||||
await providersStore.fetchProviders()
|
||||
}
|
||||
if (!servicesStore.initialized) {
|
||||
await servicesStore.fetchServices()
|
||||
}
|
||||
await providersStore.list()
|
||||
await servicesStore.list({ [providerId]: true })
|
||||
if (autoFetch) {
|
||||
await refresh()
|
||||
}
|
||||
@@ -284,10 +274,9 @@ export function useFileManager(options: UseFileManagerOptions) {
|
||||
createFile,
|
||||
renameNode,
|
||||
deleteNode,
|
||||
copyNode,
|
||||
moveNode,
|
||||
readFile,
|
||||
writeFile,
|
||||
getEntityUrl,
|
||||
downloadEntity,
|
||||
downloadCollection,
|
||||
downloadArchive,
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
|
||||
import { ref, computed } from 'vue'
|
||||
import type { Ref, ComputedRef } from 'vue'
|
||||
import { FileCollectionObject } from '@FilesManager/models/collection'
|
||||
import { FileEntityObject } from '@FilesManager/models/entity'
|
||||
import { CollectionObject } from '@DocumentsManager/models/collection'
|
||||
import { EntityObject } from '@DocumentsManager/models/entity'
|
||||
|
||||
type NodeRecord = FileCollectionObject | FileEntityObject
|
||||
type NodeRecord = CollectionObject | EntityObject
|
||||
|
||||
export interface UseFileSelectionOptions {
|
||||
multiple?: boolean
|
||||
@@ -17,148 +17,110 @@ export interface UseFileSelectionOptions {
|
||||
}
|
||||
|
||||
export function useFileSelection(options: UseFileSelectionOptions = {}) {
|
||||
const {
|
||||
multiple = true,
|
||||
allowFolders = true,
|
||||
allowFiles = true
|
||||
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())
|
||||
|
||||
// Get selected count
|
||||
const count: ComputedRef<number> = computed(() => selectedIds.value.size)
|
||||
|
||||
// Check if any selected
|
||||
const hasSelection: ComputedRef<boolean> = computed(() => selectedIds.value.size > 0)
|
||||
|
||||
// Get selected IDs as array
|
||||
const selectedIdArray: ComputedRef<string[]> = computed(() =>
|
||||
Array.from(selectedIds.value)
|
||||
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),
|
||||
)
|
||||
|
||||
// Get selected nodes as array
|
||||
const selectedNodeArray: ComputedRef<NodeRecord[]> = computed(() =>
|
||||
Array.from(selectedNodes.value.values())
|
||||
const selectedEntities: ComputedRef<EntityObject[]> = computed(() =>
|
||||
selectedNodeArray.value.filter((node): node is EntityObject => node instanceof EntityObject),
|
||||
)
|
||||
|
||||
// Get selected collections only
|
||||
const selectedCollections: ComputedRef<FileCollectionObject[]> = computed(() =>
|
||||
selectedNodeArray.value.filter(
|
||||
(node): node is FileCollectionObject => node['@type'] === 'files.collection'
|
||||
)
|
||||
)
|
||||
const isSelected = (nodeId: string): boolean => selectedIds.value.has(nodeId)
|
||||
|
||||
// Get selected entities only
|
||||
const selectedEntities: ComputedRef<FileEntityObject[]> = computed(() =>
|
||||
selectedNodeArray.value.filter(
|
||||
(node): node is FileEntityObject => node['@type'] === 'files.entity'
|
||||
)
|
||||
)
|
||||
|
||||
// Check if a node is selected
|
||||
const isSelected = (nodeId: string): boolean => {
|
||||
return selectedIds.value.has(nodeId)
|
||||
}
|
||||
|
||||
// Check if node type is allowed
|
||||
const isTypeAllowed = (node: NodeRecord): boolean => {
|
||||
if (node['@type'] === 'files.collection' && !allowFolders) {
|
||||
return false
|
||||
}
|
||||
if (node['@type'] === 'files.entity' && !allowFiles) {
|
||||
return false
|
||||
}
|
||||
if (node instanceof CollectionObject && !allowFolders) return false
|
||||
if (node instanceof EntityObject && !allowFiles) return false
|
||||
return true
|
||||
}
|
||||
|
||||
// Select a node
|
||||
const select = (node: NodeRecord) => {
|
||||
if (!isTypeAllowed(node)) {
|
||||
return
|
||||
}
|
||||
if (!isTypeAllowed(node)) return
|
||||
|
||||
if (!multiple) {
|
||||
// Clear previous selection for single select
|
||||
selectedIds.value.clear()
|
||||
selectedNodes.value.clear()
|
||||
}
|
||||
|
||||
selectedIds.value.add(node.id)
|
||||
selectedNodes.value.set(node.id, node)
|
||||
const nodeId = String(node.identifier)
|
||||
selectedIds.value.add(nodeId)
|
||||
selectedNodes.value.set(nodeId, node)
|
||||
}
|
||||
|
||||
// Deselect a node
|
||||
const deselect = (nodeId: string) => {
|
||||
selectedIds.value.delete(nodeId)
|
||||
selectedNodes.value.delete(nodeId)
|
||||
}
|
||||
|
||||
// Toggle selection
|
||||
const toggle = (node: NodeRecord) => {
|
||||
if (isSelected(node.id)) {
|
||||
deselect(node.id)
|
||||
} else {
|
||||
select(node)
|
||||
const nodeId = String(node.identifier)
|
||||
if (isSelected(nodeId)) {
|
||||
deselect(nodeId)
|
||||
return
|
||||
}
|
||||
select(node)
|
||||
}
|
||||
|
||||
// Select multiple nodes
|
||||
const selectMultiple = (nodes: NodeRecord[]) => {
|
||||
if (!multiple) {
|
||||
// For single select, only select the last one
|
||||
const lastNode = nodes[nodes.length - 1]
|
||||
if (lastNode && isTypeAllowed(lastNode)) {
|
||||
selectedIds.value.clear()
|
||||
selectedNodes.value.clear()
|
||||
selectedIds.value.add(lastNode.id)
|
||||
selectedNodes.value.set(lastNode.id, lastNode)
|
||||
const nodeId = String(lastNode.identifier)
|
||||
selectedIds.value.add(nodeId)
|
||||
selectedNodes.value.set(nodeId, lastNode)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for (const node of nodes) {
|
||||
if (isTypeAllowed(node)) {
|
||||
selectedIds.value.add(node.id)
|
||||
selectedNodes.value.set(node.id, node)
|
||||
const nodeId = String(node.identifier)
|
||||
selectedIds.value.add(nodeId)
|
||||
selectedNodes.value.set(nodeId, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Select all from a list
|
||||
const selectAll = (nodes: NodeRecord[]) => {
|
||||
if (!multiple) {
|
||||
return
|
||||
}
|
||||
if (!multiple) return
|
||||
selectMultiple(nodes)
|
||||
}
|
||||
|
||||
// Clear selection
|
||||
const clear = () => {
|
||||
selectedIds.value.clear()
|
||||
selectedNodes.value.clear()
|
||||
}
|
||||
|
||||
// Set selection (replace current)
|
||||
const setSelection = (nodes: NodeRecord[]) => {
|
||||
clear()
|
||||
selectMultiple(nodes)
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
selectedIds,
|
||||
selectedNodes,
|
||||
|
||||
// Computed
|
||||
count,
|
||||
hasSelection,
|
||||
selectedIdArray,
|
||||
selectedNodeArray,
|
||||
selectedCollections,
|
||||
selectedEntities,
|
||||
|
||||
// Methods
|
||||
isSelected,
|
||||
select,
|
||||
deselect,
|
||||
|
||||
@@ -6,15 +6,16 @@
|
||||
|
||||
import { ref, computed } from 'vue'
|
||||
import type { Ref, ComputedRef } from 'vue'
|
||||
import { useNodesStore, ROOT_ID } from '@FilesManager/stores/nodesStore'
|
||||
import { FileEntityObject } from '@FilesManager/models/entity'
|
||||
import { useNodesStore, ROOT_ID } from '@DocumentsManager/stores/nodesStore'
|
||||
import { EntityObject } from '@DocumentsManager/models/entity'
|
||||
import type { DocumentInterface } from '@DocumentsManager/types/document'
|
||||
|
||||
export interface FileUploadProgress {
|
||||
file: File
|
||||
progress: number
|
||||
status: 'pending' | 'uploading' | 'completed' | 'error'
|
||||
error?: string
|
||||
entity?: FileEntityObject
|
||||
entity?: EntityObject
|
||||
/** Relative path within folder upload (e.g., "folder/subfolder/file.txt") */
|
||||
relativePath?: string
|
||||
}
|
||||
@@ -230,9 +231,9 @@ export function useFileUpload(options: UseFileUploadOptions) {
|
||||
providerId,
|
||||
serviceId,
|
||||
parentId,
|
||||
{ label: folderName }
|
||||
{ label: folderName, owner: '' }
|
||||
)
|
||||
folderIdMap.set(folderPath, collection.id)
|
||||
folderIdMap.set(folderPath, String(collection.identifier))
|
||||
} catch (e) {
|
||||
console.error(`Failed to create folder: ${folderPath}`, e)
|
||||
// Try to continue with other folders
|
||||
@@ -246,7 +247,7 @@ export function useFileUpload(options: UseFileUploadOptions) {
|
||||
const uploadFile = async (
|
||||
uploadId: string,
|
||||
folderIdMap?: Map<string, string>
|
||||
): Promise<FileEntityObject | null> => {
|
||||
): Promise<EntityObject | null> => {
|
||||
const upload = uploads.value.get(uploadId)
|
||||
if (!upload || upload.status !== 'pending') {
|
||||
return null
|
||||
@@ -278,12 +279,16 @@ export function useFileUpload(options: UseFileUploadOptions) {
|
||||
const entity = await nodesStore.createEntity(
|
||||
providerId,
|
||||
serviceId,
|
||||
targetCollection,
|
||||
targetCollection || ROOT_ID,
|
||||
{
|
||||
'@type': 'documents.properties',
|
||||
urid: null,
|
||||
format: null,
|
||||
encoding: null,
|
||||
label: upload.file.name,
|
||||
mime: upload.file.type || 'application/octet-stream',
|
||||
size: upload.file.size,
|
||||
}
|
||||
} as DocumentInterface
|
||||
)
|
||||
|
||||
upload.progress = 75
|
||||
@@ -292,8 +297,8 @@ export function useFileUpload(options: UseFileUploadOptions) {
|
||||
await nodesStore.writeEntity(
|
||||
providerId,
|
||||
serviceId,
|
||||
targetCollection,
|
||||
entity.id,
|
||||
targetCollection || ROOT_ID,
|
||||
String(entity.identifier),
|
||||
content
|
||||
)
|
||||
|
||||
@@ -325,9 +330,9 @@ export function useFileUpload(options: UseFileUploadOptions) {
|
||||
}
|
||||
|
||||
// Upload all pending files
|
||||
const uploadAll = async (): Promise<FileEntityObject[]> => {
|
||||
const uploadAll = async (): Promise<EntityObject[]> => {
|
||||
isUploading.value = true
|
||||
const entities: FileEntityObject[] = []
|
||||
const entities: EntityObject[] = []
|
||||
|
||||
try {
|
||||
// Check if any uploads have relative paths (folder upload)
|
||||
@@ -377,7 +382,7 @@ export function useFileUpload(options: UseFileUploadOptions) {
|
||||
}
|
||||
|
||||
// Retry a failed upload
|
||||
const retryUpload = async (uploadId: string): Promise<FileEntityObject | null> => {
|
||||
const retryUpload = async (uploadId: string): Promise<EntityObject | null> => {
|
||||
const upload = uploads.value.get(uploadId)
|
||||
if (!upload || upload.status !== 'error') {
|
||||
return null
|
||||
|
||||
81
src/composables/useFileViewer.ts
Normal file
81
src/composables/useFileViewer.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* useFileViewer — resolves which registered viewer can handle a given MIME type.
|
||||
*
|
||||
* Other modules register viewers at the `documents_file_viewer` integration
|
||||
* point by including an entry in their `integrations.ts`:
|
||||
*
|
||||
* ```ts
|
||||
* documents_file_viewer: [{
|
||||
* id: 'my_viewer',
|
||||
* meta: { mimeTypes: ['application/pdf'] },
|
||||
* component: () => import('./components/MyViewer.vue'),
|
||||
* }]
|
||||
* ```
|
||||
*
|
||||
* Viewer components receive the props: `url: string`, `entity: EntityObject`, `mime: string`.
|
||||
*/
|
||||
|
||||
import { useIntegrationStore } from '@KTXC'
|
||||
import type { EntityObject } from '@DocumentsManager/models/entity'
|
||||
|
||||
const INTEGRATION_POINT = 'documents_file_viewer'
|
||||
|
||||
function mimeMatchesPattern(mime: string, pattern: string): boolean {
|
||||
if (pattern.endsWith('/*')) {
|
||||
// e.g. 'image/*' matches 'image/png', 'image/jpeg', …
|
||||
return mime.startsWith(pattern.slice(0, -1))
|
||||
}
|
||||
return mime === pattern
|
||||
}
|
||||
|
||||
function viewerMatchesMime(
|
||||
mime: string,
|
||||
mimeTypes?: string[],
|
||||
mimePatterns?: string[],
|
||||
): boolean {
|
||||
if (mimeTypes?.includes(mime)) return true
|
||||
if (mimePatterns) {
|
||||
for (const pattern of mimePatterns) {
|
||||
if (mimeMatchesPattern(mime, pattern)) return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export function useFileViewer() {
|
||||
const integrationStore = useIntegrationStore()
|
||||
|
||||
/**
|
||||
* Returns the highest-priority registered viewer that can handle `mime`,
|
||||
* or `null` if none is found.
|
||||
*/
|
||||
function findViewer(mime: string) {
|
||||
// getItems() already returns items sorted by priority (ascending)
|
||||
const viewers = integrationStore.getItems(INTEGRATION_POINT)
|
||||
for (const viewer of viewers) {
|
||||
if (
|
||||
viewerMatchesMime(
|
||||
mime,
|
||||
viewer.meta?.mimeTypes as string[] | undefined,
|
||||
viewer.meta?.mimePatterns as string[] | undefined,
|
||||
)
|
||||
) {
|
||||
return viewer
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience: returns true if any registered viewer can open this entity.
|
||||
*/
|
||||
function canOpen(entity: EntityObject): boolean {
|
||||
const mime = entity.properties.mime
|
||||
if (!mime) return false
|
||||
return findViewer(mime) !== null
|
||||
}
|
||||
|
||||
return { findViewer, canOpen }
|
||||
}
|
||||
|
||||
export type UseFileViewerReturn = ReturnType<typeof useFileViewer>
|
||||
Reference in New Issue
Block a user