Skip to content

Commit

Permalink
Merge pull request #44653 from nextcloud/backport/44409/stable29
Browse files Browse the repository at this point in the history
  • Loading branch information
skjnldsv committed Apr 4, 2024
2 parents 69a3038 + ceee29b commit 7a4e0c0
Show file tree
Hide file tree
Showing 105 changed files with 1,068 additions and 381 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,6 @@ composer.phar
core/js/mimetypelist.js

# Tests - cypress
cypress/downloads
cypress/snapshots
cypress/videos
cypress/downloads
124 changes: 124 additions & 0 deletions __tests__/FileSystemAPIUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { basename } from 'node:path'
import mime from 'mime'

class FileSystemEntry {

private _isFile: boolean
private _fullPath: string

constructor(isFile: boolean, fullPath: string) {
this._isFile = isFile
this._fullPath = fullPath
}

get isFile() {
return !!this._isFile
}

get isDirectory() {
return !this.isFile
}

get name() {
return basename(this._fullPath)
}

}

export class FileSystemFileEntry extends FileSystemEntry {

private _contents: string
private _lastModified: number

constructor(fullPath: string, contents: string, lastModified = Date.now()) {
super(true, fullPath)
this._contents = contents
this._lastModified = lastModified
}

file(success: (file: File) => void) {
const lastModified = this._lastModified
// Faking the mime by using the file extension
const type = mime.getType(this.name) || ''
success(new File([this._contents], this.name, { lastModified, type }))
}

}

export class FileSystemDirectoryEntry extends FileSystemEntry {

private _entries: FileSystemEntry[]

constructor(fullPath: string, entries: FileSystemEntry[]) {
super(false, fullPath)
this._entries = entries || []
}

createReader() {
let read = false
return {
readEntries: (success: (entries: FileSystemEntry[]) => void) => {
if (read) {
return success([])
}
read = true
success(this._entries)
},
}
}

}

/**
* This mocks the File API's File class
* It will allow us to test the Filesystem API as well as the
* File API in the same test suite.
*/
export class DataTransferItem {

private _type: string
private _entry: FileSystemEntry

getAsEntry?: () => FileSystemEntry

constructor(type = '', entry: FileSystemEntry, isFileSystemAPIAvailable = true) {
this._type = type
this._entry = entry

// Only when the Files API is available we are
// able to get the entry
if (isFileSystemAPIAvailable) {
this.getAsEntry = () => this._entry
}
}

get kind() {
return 'file'
}

get type() {
return this._type
}

getAsFile(): File|null {
if (this._entry.isFile && this._entry instanceof FileSystemFileEntry) {
let file: File | null = null
this._entry.file((f) => {
file = f
})
return file
}

// The browser will return an empty File object if the entry is a directory
return new File([], this._entry.name, { type: '' })
}

}

export const fileSystemEntryToDataTransferItem = (entry: FileSystemEntry, isFileSystemAPIAvailable = true): DataTransferItem => {
return new DataTransferItem(
entry.isFile ? 'text/plain' : 'httpd/unix-directory',
entry,
isFileSystemAPIAvailable,
)
}
124 changes: 111 additions & 13 deletions apps/files/src/components/BreadCrumbs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@
:force-icon-text="true"
:title="titleForSection(index, section)"
:aria-description="ariaForSection(section)"
@click.native="onClick(section.to)">
@click.native="onClick(section.to)"
@dragover.native="onDragOver($event, section.dir)"
@drop="onDrop($event, section.dir)">
<template v-if="index === 0" #icon>
<NcIconSvgWrapper :size="20"
:svg="viewIcon" />
Expand All @@ -49,20 +51,25 @@
</template>

<script lang="ts">
import type { Node } from '@nextcloud/files'
import { Permission, type Node } from '@nextcloud/files'
import { translate as t} from '@nextcloud/l10n'
import { basename } from 'path'
import homeSvg from '@mdi/svg/svg/home.svg?raw'
import { defineComponent } from 'vue'
import { translate as t} from '@nextcloud/l10n'
import HomeSvg from '@mdi/svg/svg/home.svg?raw'
import NcBreadcrumb from '@nextcloud/vue/dist/Components/NcBreadcrumb.js'
import NcBreadcrumbs from '@nextcloud/vue/dist/Components/NcBreadcrumbs.js'
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
import { defineComponent } from 'vue'
import { onDropInternalFiles, dataTransferToFileTree, onDropExternalFiles } from '../services/DropService'
import { showError } from '@nextcloud/dialogs'
import { useDragAndDropStore } from '../store/dragging.ts'
import { useFilesStore } from '../store/files.ts'
import { usePathsStore } from '../store/paths.ts'
import { useSelectionStore } from '../store/selection.ts'
import { useUploaderStore } from '../store/uploader.ts'
import filesListWidthMixin from '../mixins/filesListWidth.ts'
import logger from '../logger'
export default defineComponent({
name: 'BreadCrumbs',
Expand All @@ -73,25 +80,29 @@ export default defineComponent({
NcIconSvgWrapper,
},
mixins: [
filesListWidthMixin,
],
props: {
path: {
type: String,
default: '/',
},
},
mixins: [
filesListWidthMixin,
],
setup() {
const draggingStore = useDragAndDropStore()
const filesStore = useFilesStore()
const pathsStore = usePathsStore()
const selectionStore = useSelectionStore()
const uploaderStore = useUploaderStore()
return {
draggingStore,
filesStore,
pathsStore,
selectionStore,
uploaderStore,
}
},
Expand All @@ -110,14 +121,16 @@ export default defineComponent({
},
sections() {
return this.dirs.map((dir: string) => {
return this.dirs.map((dir: string, index: number) => {
const fileid = this.getFileIdFromPath(dir)
const to = { ...this.$route, params: { fileid }, query: { dir } }
return {
dir,
exact: true,
name: this.getDirDisplayName(dir),
to,
// disable drop on current directory
disableDrop: index === this.dirs.length - 1,
}
})
},
Expand All @@ -128,13 +141,27 @@ export default defineComponent({
// Hide breadcrumbs if an upload is ongoing
shouldShowBreadcrumbs(): boolean {
return this.filesListWidth > 400 && !this.isUploadInProgress
// If we're uploading files, only show the breadcrumbs
// if the files list is greater than 768px wide
if (this.isUploadInProgress) {
return this.filesListWidth > 768
}
// If we're not uploading, we have enough space from 400px
return this.filesListWidth > 400
},
// used to show the views icon for the first breadcrumb
viewIcon(): string {
return this.currentView?.icon ?? homeSvg
}
return this.currentView?.icon ?? HomeSvg
},
selectedFiles() {
return this.selectionStore.selected
},
draggingFiles() {
return this.draggingStore.dragging
},
},
methods: {
Expand All @@ -160,6 +187,77 @@ export default defineComponent({
}
},
onDragOver(event: DragEvent, path: string) {
// Cannot drop on the current directory
if (path === this.dirs[this.dirs.length - 1]) {
event.dataTransfer.dropEffect = 'none'
return
}
// Handle copy/move drag and drop
if (event.ctrlKey) {
event.dataTransfer.dropEffect = 'copy'
} else {
event.dataTransfer.dropEffect = 'move'
}
},
async onDrop(event: DragEvent, path: string) {
// skip if native drop like text drag and drop from files names
if (!this.draggingFiles && !event.dataTransfer?.items?.length) {
return
}
// Do not stop propagation, so the main content
// drop event can be triggered too and clear the
// dragover state on the DragAndDropNotice component.
event.preventDefault()
// Caching the selection
const selection = this.draggingFiles
const items = [...event.dataTransfer?.items || []] as DataTransferItem[]
// We need to process the dataTransfer ASAP before the
// browser clears it. This is why we cache the items too.
const fileTree = await dataTransferToFileTree(items)
// We might not have the target directory fetched yet
const contents = await this.currentView?.getContents(path)
const folder = contents?.folder
if (!folder) {
showError(this.t('files', 'Target folder does not exist any more'))
return
}
const canDrop = (folder.permissions & Permission.CREATE) !== 0
const isCopy = event.ctrlKey
// If another button is pressed, cancel it. This
// allows cancelling the drag with the right click.
if (!canDrop || event.button !== 0) {
return
}
logger.debug('Dropped', { event, folder, selection, fileTree })
// Check whether we're uploading files
if (fileTree.contents.length > 0) {
await onDropExternalFiles(fileTree, folder, contents.contents)
return
}
// Else we're moving/copying files
const nodes = selection.map(fileid => this.filesStore.getNode(fileid)) as Node[]
await onDropInternalFiles(nodes, folder, contents.contents, isCopy)
// Reset selection after we dropped the files
// if the dropped files are within the selection
if (selection.some(fileid => this.selectedFiles.includes(fileid))) {
logger.debug('Dropped selection, resetting select store...')
this.selectionStore.reset()
}
},
titleForSection(index, section) {
if (section?.to?.query?.dir === this.$route.query.dir) {
return t('files', 'Reload current directory')
Expand Down
Loading

0 comments on commit 7a4e0c0

Please sign in to comment.