diff --git a/src/components/FileActionsMenu.vue b/src/components/FileActionsMenu.vue index be2dcab..0c1ad0b 100644 --- a/src/components/FileActionsMenu.vue +++ b/src/components/FileActionsMenu.vue @@ -12,6 +12,7 @@ defineProps<{ const emit = defineEmits<{ 'open': [item: CollectionObject | EntityObject] + 'edit': [item: CollectionObject | EntityObject] 'rename': [item: CollectionObject | EntityObject] 'delete': [item: CollectionObject | EntityObject] 'download': [item: CollectionObject | EntityObject] @@ -26,6 +27,8 @@ function handleAction(action: string, item: CollectionObject | EntityObject, eve if (action === 'open') { emit('open', item) + } else if (action === 'edit') { + emit('edit', item) } else if (action === 'rename') { emit('rename', item) } else if (action === 'delete') { @@ -59,6 +62,10 @@ function isEntity(item: CollectionObject | EntityObject): item is EntityObject { mdi-open-in-app Open + handleAction('edit', item, e)"> + mdi-pencil + Edit + handleAction('details', item, e)"> mdi-information-outline Details diff --git a/src/components/FilesInfoPanel.vue b/src/components/FilesInfoPanel.vue index f121b03..f88ec4e 100644 --- a/src/components/FilesInfoPanel.vue +++ b/src/components/FilesInfoPanel.vue @@ -1,17 +1,37 @@ - @@ -172,11 +191,18 @@ const totalSize = computed(() => { - + diff --git a/src/components/FilesSidebar.vue b/src/components/FilesSidebar.vue index a6e7c56..0e29ff6 100644 --- a/src/components/FilesSidebar.vue +++ b/src/components/FilesSidebar.vue @@ -1,16 +1,22 @@ @@ -18,9 +24,9 @@ const emit = defineEmits<{ @@ -53,11 +59,18 @@ const emit = defineEmits<{ Storage + + + No storage services available. + @@ -68,4 +81,8 @@ const emit = defineEmits<{ border-right: 1px solid rgb(var(--v-border-color)) !important; overflow-y: auto; } + +.files-sidebar-empty { + line-height: 1.4; +} diff --git a/src/components/FilesToolbar.vue b/src/components/FilesToolbar.vue index f35cf99..663516a 100644 --- a/src/components/FilesToolbar.vue +++ b/src/components/FilesToolbar.vue @@ -1,5 +1,6 @@ - + - - - mdi-folder-outline - Files - + Files @@ -55,8 +55,7 @@ const viewModeModel = computed({ prepend-inner-icon="mdi-magnify" hide-details single-line - class="mx-4" - style="max-width: 300px;" + class="files-toolbar-search mx-4" /> @@ -131,7 +130,14 @@ const viewModeModel = computed({ border-bottom: 1px solid rgb(var(--v-border-color)) !important; } -.border-b { - border-bottom: 1px solid rgb(var(--v-border-color)); +@media (max-width: 960px) { + .files-toolbar-search { + max-width: 180px; + margin-inline: 8px !important; + } +} + +.files-toolbar-search { + max-width: 300px; } diff --git a/src/components/dialogs/UploadDialog.vue b/src/components/dialogs/UploadDialog.vue index 37d9087..e2a4ba1 100644 --- a/src/components/dialogs/UploadDialog.vue +++ b/src/components/dialogs/UploadDialog.vue @@ -1,12 +1,17 @@ @@ -33,6 +55,31 @@ function handleClose() { Upload Files + + + + + {{ preparingMessage }} + + + {{ preparingProcessedCount }} / {{ preparingTotalCount }} files queued + + + Preparing uploads... + + + + + + + + + + + {{ queuedCount }} queued + + + {{ pendingCount }} pending + + + {{ completedCount }} completed + + + {{ failedCount }} failed + + + - - - - - {{ getUploadStatusIcon(item.status) }} - - - - {{ item.relativePath || item.file.name }} - - - {{ formatSize(item.file.size) }} - — {{ item.error }} - - - - - - - + + + + + + {{ getUploadStatusIcon(entry.item.status) }} + + + + {{ entry.item.relativePath || entry.item.file.name }} + + + {{ formatSize(entry.item.file.size) }} + — {{ entry.item.error }} + + + + + + + + - + mdi-file-upload-outline No files selected - + {{ completedCount > 0 ? 'Done' : 'Cancel' }} @@ -127,6 +193,7 @@ function handleClose() { variant="elevated" @click="emit('upload-all')" :loading="isUploading" + :disabled="isPreparing" > Upload {{ pendingCount }} File(s) @@ -136,8 +203,25 @@ function handleClose() { diff --git a/src/components/editor/FileEditorDialog.vue b/src/components/editor/FileEditorDialog.vue new file mode 100644 index 0000000..f6e35e4 --- /dev/null +++ b/src/components/editor/FileEditorDialog.vue @@ -0,0 +1,197 @@ + + + + + + + + + + mdi-pencil + {{ filename }} + + + + + + + + + + + + + + + + + + + + + mdi-file-edit-outline + No editor available + + {{ mime || 'Unknown file type' }} + + + + + + + + + + + Unsaved Changes + + You have unsaved changes. Are you sure you want to close without saving? + + + + Cancel + Discard + + + + + + diff --git a/src/components/editor/index.ts b/src/components/editor/index.ts new file mode 100644 index 0000000..6e8e099 --- /dev/null +++ b/src/components/editor/index.ts @@ -0,0 +1 @@ +export { default as FileEditorDialog } from './FileEditorDialog.vue' diff --git a/src/components/index.ts b/src/components/index.ts index bbdbeeb..8ac94dd 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -15,3 +15,6 @@ export * from './dialogs' // Viewer export * from './viewer' + +// Editor +export * from './editor' diff --git a/src/components/views/FilesDetailsView.vue b/src/components/views/FilesDetailsView.vue index 986ea74..901f81a 100644 --- a/src/components/views/FilesDetailsView.vue +++ b/src/components/views/FilesDetailsView.vue @@ -19,6 +19,7 @@ const props = defineProps<{ const emit = defineEmits<{ 'item-click': [item: CollectionObject | EntityObject, event: MouseEvent | KeyboardEvent] 'open': [item: CollectionObject | EntityObject] + 'edit': [item: CollectionObject | EntityObject] 'rename': [item: CollectionObject | EntityObject] 'delete': [item: CollectionObject | EntityObject] 'download': [item: CollectionObject | EntityObject] @@ -70,6 +71,7 @@ function isCollection(wrapped: ItemWithType): wrapped is { item: CollectionObjec import('./components/MyEditor.vue'), + * }] + * ``` + * + * Editor components receive the props: + * `entity: EntityObject`, `mime: string`, + * `readFile: (entityId: string) => Promise`, + * `writeFile: (entityId: string, content: string) => Promise` + */ + +import { useIntegrationStore } from '@KTXC' +import type { EntityObject } from '@DocumentsManager/models/entity' + +const INTEGRATION_POINT = 'documents_file_editor' + +function mimeMatchesPattern(mime: string, pattern: string): boolean { + if (pattern.endsWith('/*')) { + return mime.startsWith(pattern.slice(0, -1)) + } + return mime === pattern +} + +function editorMatchesMime( + 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 useFileEditor() { + const integrationStore = useIntegrationStore() + + /** + * Returns the highest-priority registered editor that can handle `mime`, + * or `null` if none is found. + */ + function findEditor(mime: string) { + const editors = integrationStore.getItems(INTEGRATION_POINT) + for (const editor of editors) { + if ( + editorMatchesMime( + mime, + editor.meta?.mimeTypes as string[] | undefined, + editor.meta?.mimePatterns as string[] | undefined, + ) + ) { + return editor + } + } + return null + } + + /** + * Convenience: returns true if any registered editor can handle this entity. + */ + function canEdit(entity: EntityObject): boolean { + const mime = entity.properties.mime + if (!mime) return false + return findEditor(mime) !== null + } + + return { findEditor, canEdit } +} + +export type UseFileEditorReturn = ReturnType diff --git a/src/composables/useFileManager.ts b/src/composables/useFileManager.ts index 8e128a3..bb9daa8 100644 --- a/src/composables/useFileManager.ts +++ b/src/composables/useFileManager.ts @@ -3,7 +3,7 @@ * Provides reactive access to file manager state and actions */ -import { computed, ref } from 'vue' +import { computed, ref, unref } from 'vue' import type { Ref, ComputedRef } from 'vue' import { useProvidersStore } from '@DocumentsManager/stores/providersStore' import { useServicesStore } from '@DocumentsManager/stores/servicesStore' @@ -17,8 +17,8 @@ import { EntityObject } from '@DocumentsManager/models/entity' const TRANSFER_BASE_URL = '/m/documents_manager' export interface UseFileManagerOptions { - providerId: string - serviceId: string + providerId: string | Ref | ComputedRef + serviceId: string | Ref | ComputedRef autoFetch?: boolean } @@ -29,6 +29,9 @@ export function useFileManager(options: UseFileManagerOptions) { 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) @@ -37,21 +40,21 @@ export function useFileManager(options: UseFileManagerOptions) { const error = computed(() => nodesStore.error) // Provider and service - const provider = computed(() => providersStore.provider(providerId)) - const service = computed(() => servicesStore.service(providerId, serviceId)) + 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(providerId, serviceId, currentLocation.value) + nodesStore.getChildren(currentProviderId(), currentServiceId(), currentLocation.value) ) const currentCollections: ComputedRef = computed(() => - nodesStore.getChildCollections(providerId, serviceId, currentLocation.value) + nodesStore.getChildCollections(currentProviderId(), currentServiceId(), currentLocation.value) ) const currentEntities: ComputedRef = computed(() => - nodesStore.getChildEntities(providerId, serviceId, currentLocation.value) + nodesStore.getChildEntities(currentProviderId(), currentServiceId(), currentLocation.value) ) // Breadcrumb path @@ -59,7 +62,7 @@ export function useFileManager(options: UseFileManagerOptions) { if (currentLocation.value === ROOT_ID) { return [] } - return nodesStore.getPath(providerId, serviceId, currentLocation.value) + return nodesStore.getPath(currentProviderId(), currentServiceId(), currentLocation.value) }) // Is at root? @@ -76,7 +79,7 @@ export function useFileManager(options: UseFileManagerOptions) { if (currentLocation.value === ROOT_ID) { return } - const currentNode = nodesStore.getNode(providerId, serviceId, currentLocation.value) + const currentNode = nodesStore.getNode(currentProviderId(), currentServiceId(), currentLocation.value) if (currentNode) { await navigateTo(currentNode.collection ? String(currentNode.collection) : ROOT_ID) } @@ -94,8 +97,8 @@ export function useFileManager(options: UseFileManagerOptions) { range?: ListRange ) => { await nodesStore.fetchNodes( - providerId, - serviceId, + currentProviderId(), + currentServiceId(), currentLocation.value === ROOT_ID ? ROOT_ID : currentLocation.value, filter, sort, @@ -106,8 +109,8 @@ export function useFileManager(options: UseFileManagerOptions) { // Create a new folder const createFolder = async (label: string): Promise => { return await nodesStore.createCollection( - providerId, - serviceId, + currentProviderId(), + currentServiceId(), currentLocation.value === ROOT_ID ? ROOT_ID : currentLocation.value, { label, owner: '' } ) @@ -129,8 +132,8 @@ export function useFileManager(options: UseFileManagerOptions) { } return await nodesStore.createEntity( - providerId, - serviceId, + currentProviderId(), + currentServiceId(), currentLocation.value === ROOT_ID ? ROOT_ID : currentLocation.value, properties ) @@ -138,13 +141,13 @@ export function useFileManager(options: UseFileManagerOptions) { // Rename a node const renameNode = async (nodeId: string, newLabel: string) => { - const node = nodesStore.getNode(providerId, serviceId, nodeId) + const node = nodesStore.getNode(currentProviderId(), currentServiceId(), nodeId) if (!node) { throw new Error('Node not found') } if (node instanceof CollectionObject) { - return await nodesStore.updateCollection(providerId, serviceId, nodeId, { + return await nodesStore.updateCollection(currentProviderId(), currentServiceId(), nodeId, { label: newLabel, owner: node.properties.owner, }) @@ -158,53 +161,53 @@ export function useFileManager(options: UseFileManagerOptions) { format: node.properties.format, encoding: node.properties.encoding, } - return await nodesStore.updateEntity(providerId, serviceId, node.collection, nodeId, properties) + return await nodesStore.updateEntity(currentProviderId(), currentServiceId(), node.collection, nodeId, properties) } } // Delete a node const deleteNode = async (nodeId: string): Promise => { - const node = nodesStore.getNode(providerId, serviceId, nodeId) + const node = nodesStore.getNode(currentProviderId(), currentServiceId(), nodeId) if (!node) { throw new Error('Node not found') } if (node instanceof CollectionObject) { - return await nodesStore.deleteCollection(providerId, serviceId, nodeId) + return await nodesStore.deleteCollection(currentProviderId(), currentServiceId(), nodeId) } else { - return await nodesStore.deleteEntity(providerId, serviceId, node.collection, nodeId) + return await nodesStore.deleteEntity(currentProviderId(), currentServiceId(), node.collection, nodeId) } } // Read file content const readFile = async (entityId: string): Promise => { - const node = nodesStore.getNode(providerId, serviceId, entityId) + const node = nodesStore.getNode(currentProviderId(), currentServiceId(), entityId) if (!node || !(node instanceof EntityObject)) { throw new Error('Entity not found') } - return await nodesStore.readEntity(providerId, serviceId, node.collection || ROOT_ID, entityId) + 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(providerId, serviceId, entityId) + const node = nodesStore.getNode(currentProviderId(), currentServiceId(), entityId) if (!node || !(node instanceof EntityObject)) { throw new Error('Entity not found') } - return await nodesStore.writeEntity(providerId, serviceId, node.collection, entityId, content) + 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(providerId)}/${encodeURIComponent(serviceId)}/${encodeURIComponent(collection)}/${encodeURIComponent(entityId)}` + 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(providerId)}/${encodeURIComponent(serviceId)}/${encodeURIComponent(collection)}/${encodeURIComponent(entityId)}` + 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') @@ -212,7 +215,7 @@ export function useFileManager(options: UseFileManagerOptions) { 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)}` + const url = `${TRANSFER_BASE_URL}/download/collection/${encodeURIComponent(currentProviderId())}/${encodeURIComponent(currentServiceId())}/${encodeURIComponent(collectionId)}` window.open(url, '_blank') } @@ -220,8 +223,8 @@ export function useFileManager(options: UseFileManagerOptions) { const downloadArchive = (ids: string[], name: string = 'download', collectionId?: string | null): void => { const collection = collectionId ?? currentLocation.value const params = new URLSearchParams({ - provider: providerId, - service: serviceId, + provider: currentProviderId(), + service: currentServiceId(), }) ids.forEach(id => params.append('ids[]', id)) if (name) { @@ -239,7 +242,7 @@ export function useFileManager(options: UseFileManagerOptions) { // Initialize - fetch providers, services, and initial nodes if autoFetch const initialize = async () => { await providersStore.list() - await servicesStore.list({ [providerId]: true }) + await servicesStore.list({ [currentProviderId()]: true }) if (autoFetch) { await refresh() } diff --git a/src/composables/useFileUpload.ts b/src/composables/useFileUpload.ts index 06c36db..572773c 100644 --- a/src/composables/useFileUpload.ts +++ b/src/composables/useFileUpload.ts @@ -4,7 +4,7 @@ * Supports individual files and entire folder uploads with path preservation */ -import { ref, computed } from 'vue' +import { ref, computed, unref } from 'vue' import type { Ref, ComputedRef } from 'vue' import { useNodesStore, ROOT_ID } from '@DocumentsManager/stores/nodesStore' import { EntityObject } from '@DocumentsManager/models/entity' @@ -25,9 +25,19 @@ export interface FileWithPath { relativePath: string } +interface NormalizedUploadItem { + file: File + relativePath?: string +} + +export interface AddUploadsWithPathsOptions { + batchSize?: number + onProgress?: (processed: number, total: number) => void +} + export interface UseFileUploadOptions { - providerId: string - serviceId: string + providerId: string | Ref | ComputedRef + serviceId: string | Ref | ComputedRef collectionId?: string | null maxFileSize?: number allowedTypes?: string[] @@ -44,6 +54,9 @@ export function useFileUpload(options: UseFileUploadOptions) { allowedTypes } = options + const currentProviderId = () => unref(providerId) + const currentServiceId = () => unref(serviceId) + const uploads: Ref> = ref(new Map()) const isUploading = ref(false) @@ -113,25 +126,69 @@ export function useFileUpload(options: UseFileUploadOptions) { return `${pathPart}-${file.size}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` } + const createUploadProgress = (file: File, relativePath?: string): FileUploadProgress => { + const error = validateFile(file) + + return { + file, + progress: 0, + status: error ? 'error' : 'pending', + error: error || undefined, + relativePath, + } + } + + const yieldToBrowser = async (): Promise => { + await new Promise(resolve => { + if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function') { + window.requestAnimationFrame(() => resolve()) + return + } + + setTimeout(resolve, 0) + }) + } + + const normalizeUploadsWithPaths = ( + filesOrList: FileList | File[] | FileWithPath[] + ): NormalizedUploadItem[] => { + if (filesOrList instanceof FileList) { + return Array.from(filesOrList, file => ({ + file, + relativePath: (file as File & { webkitRelativePath?: string }).webkitRelativePath || file.name, + })) + } + + if (filesOrList.length === 0) { + return [] + } + + if (filesOrList[0] instanceof File) { + return (filesOrList as File[]).map(file => ({ + file, + relativePath: (file as File & { webkitRelativePath?: string }).webkitRelativePath || file.name, + })) + } + + return (filesOrList as FileWithPath[]).map(({ file, relativePath }) => ({ file, relativePath })) + } + // Add files to upload queue const addFiles = (files: FileList | File[]): FileUploadProgress[] => { const added: FileUploadProgress[] = [] - + + const nextUploads = new Map(uploads.value) + for (const file of files) { - const error = validateFile(file) + const progress = createUploadProgress(file) const uploadId = generateUploadId(file) - - const progress: FileUploadProgress = { - file, - progress: 0, - status: error ? 'error' : 'pending', - error: error || undefined - } - - uploads.value.set(uploadId, progress) + + nextUploads.set(uploadId, progress) added.push(progress) } - + + uploads.value = nextUploads + return added } @@ -140,47 +197,55 @@ export function useFileUpload(options: UseFileUploadOptions) { filesOrList: FileList | File[] | FileWithPath[] ): FileUploadProgress[] => { const added: FileUploadProgress[] = [] - - // Handle FileList from webkitdirectory input - if (filesOrList instanceof FileList || (Array.isArray(filesOrList) && filesOrList[0] instanceof File && !('relativePath' in filesOrList[0]))) { - const fileList = filesOrList as FileList | File[] - for (const file of fileList) { - // webkitRelativePath is set on files from folder input - const relativePath = (file as any).webkitRelativePath || file.name - const error = validateFile(file) + + const nextUploads = new Map(uploads.value) + + for (const { file, relativePath } of normalizeUploadsWithPaths(filesOrList)) { + const progress = createUploadProgress(file, relativePath) + const uploadId = generateUploadId(file, relativePath) + + nextUploads.set(uploadId, progress) + added.push(progress) + } + + uploads.value = nextUploads + + return added + } + + const addFilesWithPathsBatched = async ( + filesOrList: FileList | File[] | FileWithPath[], + options: AddUploadsWithPathsOptions = {} + ): Promise => { + const items = normalizeUploadsWithPaths(filesOrList) + const total = items.length + const batchSize = Math.max(1, options.batchSize ?? 250) + const added: FileUploadProgress[] = [] + + options.onProgress?.(0, total) + + for (let start = 0; start < total; start += batchSize) { + const batch = items.slice(start, start + batchSize) + const nextUploads = new Map(uploads.value) + + for (const { file, relativePath } of batch) { + const progress = createUploadProgress(file, relativePath) const uploadId = generateUploadId(file, relativePath) - - const progress: FileUploadProgress = { - file, - progress: 0, - status: error ? 'error' : 'pending', - error: error || undefined, - relativePath - } - - uploads.value.set(uploadId, progress) + + nextUploads.set(uploadId, progress) added.push(progress) } - } else { - // Handle FileWithPath array (from drag & drop folder processing) - const filesWithPaths = filesOrList as FileWithPath[] - for (const { file, relativePath } of filesWithPaths) { - const error = validateFile(file) - const uploadId = generateUploadId(file, relativePath) - - const progress: FileUploadProgress = { - file, - progress: 0, - status: error ? 'error' : 'pending', - error: error || undefined, - relativePath - } - - uploads.value.set(uploadId, progress) - added.push(progress) + + uploads.value = nextUploads + + const processed = Math.min(start + batch.length, total) + options.onProgress?.(processed, total) + + if (processed < total) { + await yieldToBrowser() } } - + return added } @@ -228,8 +293,8 @@ export function useFileUpload(options: UseFileUploadOptions) { try { // Create the folder const collection = await nodesStore.createCollection( - providerId, - serviceId, + currentProviderId(), + currentServiceId(), parentId, { label: folderName, owner: '' } ) @@ -277,8 +342,8 @@ export function useFileUpload(options: UseFileUploadOptions) { // Create the entity const entity = await nodesStore.createEntity( - providerId, - serviceId, + currentProviderId(), + currentServiceId(), targetCollection || ROOT_ID, { '@type': 'documents.properties', @@ -295,8 +360,8 @@ export function useFileUpload(options: UseFileUploadOptions) { // Write the content await nodesStore.writeEntity( - providerId, - serviceId, + currentProviderId(), + currentServiceId(), targetCollection || ROOT_ID, String(entity.identifier), content @@ -417,6 +482,7 @@ export function useFileUpload(options: UseFileUploadOptions) { validateFile, addFiles, addFilesWithPaths, + addFilesWithPathsBatched, uploadFile, uploadAll, removeUpload, diff --git a/src/pages/FilesPage.vue b/src/pages/FilesPage.vue index df841ce..005e3b3 100644 --- a/src/pages/FilesPage.vue +++ b/src/pages/FilesPage.vue @@ -1,10 +1,13 @@ - + + + mdi-folder-off-outline + File Manager Not Available + + The File Manager module is not installed or enabled. + This page requires the documents_manager module to function properly. + + + Please contact your system administrator to install and enable the + documents_manager module. + + + + + { - - + + + + - - + + - - - {{ fileManager.error.value }} - + + + - - - File Manager module is not available. Please ensure it is installed and enabled. - + + - - + + - - + + + + - - - - - + + + + @@ -598,6 +843,10 @@ onMounted(async () => { :uploads="upload.uploads.value" :total-progress="upload.totalProgress.value" :is-uploading="upload.isUploading.value" + :is-preparing="isPreparingUploads" + :preparing-message="uploadPreparationMessage" + :preparing-processed-count="uploadPreparationProcessedCount" + :preparing-total-count="uploadPreparationTotalCount" :pending-count="upload.pendingUploads.value.length" :completed-count="upload.completedUploads.value.length" @upload-all="handleUploadAll" @@ -607,12 +856,6 @@ onMounted(async () => { @close="handleUploadDialogClose" /> - - - { :download-entity="fileManager.downloadEntity" @navigate="handleViewerNavigate" /> + + + + + + {{ snackbarMessage }} + + Close + +
No editor available
+ {{ mime || 'Unknown file type' }} +
+ The File Manager module is not installed or enabled. + This page requires the documents_manager module to function properly. +
+ Please contact your system administrator to install and enable the + documents_manager module. +
documents_manager