mirror of
https://github.com/nextcloud/server.git
synced 2026-06-06 23:34:22 -04:00
Merge pull request #44652 from nextcloud/backport/44409/stable28
This commit is contained in:
commit
233e8639cc
196 changed files with 4617 additions and 2726 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -168,5 +168,6 @@ composer.phar
|
|||
core/js/mimetypelist.js
|
||||
|
||||
# Tests - cypress
|
||||
cypress/downloads
|
||||
cypress/snapshots
|
||||
cypress/videos
|
||||
|
|
|
|||
124
__tests__/FileSystemAPIUtils.ts
Normal file
124
__tests__/FileSystemAPIUtils.ts
Normal file
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -11,7 +11,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" />
|
||||
|
|
@ -25,19 +27,28 @@
|
|||
</NcBreadcrumbs>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import type { Node } from '@nextcloud/files'
|
||||
|
||||
import { basename } from 'path'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import homeSvg from '@mdi/svg/svg/home.svg?raw'
|
||||
import { defineComponent } from 'vue'
|
||||
import { Permission } from '@nextcloud/files'
|
||||
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 Vue 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 filesListWidthMixin from '../mixins/filesListWidth.ts'
|
||||
import logger from '../logger'
|
||||
|
||||
export default Vue.extend({
|
||||
export default defineComponent({
|
||||
name: 'BreadCrumbs',
|
||||
|
||||
components: {
|
||||
|
|
@ -46,6 +57,10 @@ export default Vue.extend({
|
|||
NcIconSvgWrapper,
|
||||
},
|
||||
|
||||
mixins: [
|
||||
filesListWidthMixin,
|
||||
],
|
||||
|
||||
props: {
|
||||
path: {
|
||||
type: String,
|
||||
|
|
@ -54,11 +69,15 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
setup() {
|
||||
const draggingStore = useDragAndDropStore()
|
||||
const filesStore = useFilesStore()
|
||||
const pathsStore = usePathsStore()
|
||||
const selectionStore = useSelectionStore()
|
||||
return {
|
||||
draggingStore,
|
||||
filesStore,
|
||||
pathsStore,
|
||||
selectionStore,
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -76,7 +95,7 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
sections() {
|
||||
return this.dirs.map(dir => {
|
||||
return this.dirs.map((dir: string, index: number) => {
|
||||
const fileid = this.getFileIdFromPath(dir)
|
||||
const to = { ...this.$route, params: { fileid }, query: { dir } }
|
||||
return {
|
||||
|
|
@ -84,14 +103,24 @@ export default Vue.extend({
|
|||
exact: true,
|
||||
name: this.getDirDisplayName(dir),
|
||||
to,
|
||||
// disable drop on current directory
|
||||
disableDrop: index === this.dirs.length - 1,
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// used to show the views icon for the first breadcrumb
|
||||
viewIcon() {
|
||||
return this.currentView?.icon ?? homeSvg
|
||||
}
|
||||
return this.currentView?.icon ?? HomeSvg
|
||||
},
|
||||
|
||||
selectedFiles() {
|
||||
return this.selectionStore.selected
|
||||
},
|
||||
|
||||
draggingFiles() {
|
||||
return this.draggingStore.dragging
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
|
@ -117,6 +146,77 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
|
||||
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')
|
||||
|
|
|
|||
|
|
@ -46,14 +46,14 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { Folder, Permission } from '@nextcloud/files'
|
||||
import { showError, showSuccess } from '@nextcloud/dialogs'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { UploadStatus } from '@nextcloud/upload'
|
||||
|
||||
import TrayArrowDownIcon from 'vue-material-design-icons/TrayArrowDown.vue'
|
||||
|
||||
import logger from '../logger.js'
|
||||
import { handleDrop } from '../services/DropService'
|
||||
import { dataTransferToFileTree, onDropExternalFiles } from '../services/DropService'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DragAndDropNotice',
|
||||
|
|
@ -76,6 +76,10 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
computed: {
|
||||
currentView() {
|
||||
return this.$navigation.active
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the current folder has create permissions
|
||||
*/
|
||||
|
|
@ -146,8 +150,6 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
async onDrop(event: DragEvent) {
|
||||
logger.debug('Dropped on DragAndDropNotice', { event })
|
||||
|
||||
// cantUploadLabel is null if we can upload
|
||||
if (this.cantUploadLabel) {
|
||||
showError(this.cantUploadLabel)
|
||||
|
|
@ -161,38 +163,53 @@ export default defineComponent({
|
|||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
if (event.dataTransfer && event.dataTransfer.items.length > 0) {
|
||||
// Start upload
|
||||
logger.debug(`Uploading files to ${this.currentFolder.path}`)
|
||||
// Process finished uploads
|
||||
const uploads = await handleDrop(event.dataTransfer)
|
||||
logger.debug('Upload terminated', { uploads })
|
||||
// Caching the selection
|
||||
const items: DataTransferItem[] = [...event.dataTransfer?.items || []]
|
||||
|
||||
if (uploads.some((upload) => upload.status === UploadStatus.FAILED)) {
|
||||
showError(t('files', 'Some files could not be uploaded'))
|
||||
const failedUploads = uploads.filter((upload) => upload.status === UploadStatus.FAILED)
|
||||
logger.debug('Some files could not be uploaded', { failedUploads })
|
||||
} else {
|
||||
showSuccess(t('files', 'Files uploaded successfully'))
|
||||
}
|
||||
// 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)
|
||||
|
||||
// Scroll to last successful upload in current directory if terminated
|
||||
const lastUpload = uploads.findLast((upload) => upload.status !== UploadStatus.FAILED
|
||||
&& !upload.file.webkitRelativePath.includes('/')
|
||||
&& upload.response?.headers?.['oc-fileid'])
|
||||
|
||||
if (lastUpload !== undefined) {
|
||||
this.$router.push({
|
||||
...this.$route,
|
||||
params: {
|
||||
view: this.$route.params?.view ?? 'files',
|
||||
fileid: parseInt(lastUpload.response!.headers['oc-fileid']),
|
||||
},
|
||||
})
|
||||
}
|
||||
// We might not have the target directory fetched yet
|
||||
const contents = await this.currentView?.getContents(this.currentFolder.path)
|
||||
const folder = contents?.folder
|
||||
if (!folder) {
|
||||
showError(this.t('files', 'Target folder does not exist any more'))
|
||||
return
|
||||
}
|
||||
|
||||
// If another button is pressed, cancel it. This
|
||||
// allows cancelling the drag with the right click.
|
||||
if (event.button) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.debug('Dropped', { event, folder, fileTree })
|
||||
|
||||
// Check whether we're uploading files
|
||||
const uploads = await onDropExternalFiles(fileTree, folder, contents.contents)
|
||||
|
||||
// Scroll to last successful upload in current directory if terminated
|
||||
const lastUpload = uploads.findLast((upload) => upload.status !== UploadStatus.FAILED
|
||||
&& !upload.file.webkitRelativePath.includes('/')
|
||||
&& upload.response?.headers?.['oc-fileid']
|
||||
// Only use the last ID if it's in the current folder
|
||||
&& upload.source.replace(folder.source, '').split('/').length === 2)
|
||||
|
||||
if (lastUpload !== undefined) {
|
||||
logger.debug('Scrolling to last upload in current folder', { lastUpload })
|
||||
this.$router.push({
|
||||
...this.$route,
|
||||
params: {
|
||||
view: this.$route.params?.view ?? 'files',
|
||||
fileid: parseInt(lastUpload.response!.headers['oc-fileid']),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
this.dragover = false
|
||||
},
|
||||
|
||||
t,
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -22,21 +22,19 @@
|
|||
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
import { extname, join } from 'path'
|
||||
import { extname } from 'path'
|
||||
import { FileType, Permission, Folder, File as NcFile, NodeStatus, Node, View } from '@nextcloud/files'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { showError, showSuccess } from '@nextcloud/dialogs'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { Upload, getUploader } from '@nextcloud/upload'
|
||||
import { vOnClickOutside } from '@vueuse/components'
|
||||
import Vue, { defineComponent } from 'vue'
|
||||
|
||||
import { action as sidebarAction } from '../actions/sidebarAction.ts'
|
||||
import { getDragAndDropPreview } from '../utils/dragUtils.ts'
|
||||
import { handleCopyMoveNodeTo } from '../actions/moveOrCopyAction.ts'
|
||||
import { hashCode } from '../utils/hashUtils.ts'
|
||||
import { MoveCopyAction } from '../actions/moveOrCopyActionUtils.ts'
|
||||
import { dataTransferToFileTree, onDropExternalFiles, onDropInternalFiles } from '../services/DropService.ts'
|
||||
import logger from '../logger.js'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
|
||||
Vue.directive('onClickOutside', vOnClickOutside)
|
||||
|
||||
|
|
@ -306,79 +304,53 @@ export default defineComponent({
|
|||
|
||||
async onDrop(event: DragEvent) {
|
||||
// skip if native drop like text drag and drop from files names
|
||||
if (!this.draggingFiles && !event.dataTransfer?.files?.length) {
|
||||
if (!this.draggingFiles && !event.dataTransfer?.items?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
// If another button is pressed, cancel it
|
||||
// This allows cancelling the drag with the right click
|
||||
if (!this.canDrop || event.button !== 0) {
|
||||
// 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(this.source.path)
|
||||
const folder = contents?.folder
|
||||
if (!folder) {
|
||||
showError(this.t('files', 'Target folder does not exist any more'))
|
||||
return
|
||||
}
|
||||
|
||||
// If another button is pressed, cancel it. This
|
||||
// allows cancelling the drag with the right click.
|
||||
if (!this.canDrop || event.button) {
|
||||
return
|
||||
}
|
||||
|
||||
const isCopy = event.ctrlKey
|
||||
this.dragover = false
|
||||
|
||||
logger.debug('Dropped', { event, selection: this.draggingFiles })
|
||||
logger.debug('Dropped', { event, folder, selection, fileTree })
|
||||
|
||||
// Check whether we're uploading files
|
||||
if (event.dataTransfer?.files
|
||||
&& event.dataTransfer.files.length > 0) {
|
||||
const uploader = getUploader()
|
||||
|
||||
// Check whether the uploader is in the same folder
|
||||
// This should never happen™
|
||||
if (!uploader.destination.path.startsWith(uploader.destination.path)) {
|
||||
logger.error('The current uploader destination is not the same as the current folder')
|
||||
showError(t('files', 'An error occurred while uploading. Please try again later.'))
|
||||
return
|
||||
}
|
||||
|
||||
logger.debug(`Uploading files to ${this.source.path}`)
|
||||
const queue = [] as Promise<Upload>[]
|
||||
for (const file of event.dataTransfer.files) {
|
||||
// Because the uploader destination is properly set to the current folder
|
||||
// we can just use the basename as the relative path.
|
||||
queue.push(uploader.upload(join(this.source.basename, file.name), file))
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(queue)
|
||||
const errors = results.filter(result => result.status === 'rejected')
|
||||
if (errors.length > 0) {
|
||||
logger.error('Error while uploading files', { errors })
|
||||
showError(t('files', 'Some files could not be uploaded'))
|
||||
return
|
||||
}
|
||||
|
||||
logger.debug('Files uploaded successfully')
|
||||
showSuccess(t('files', 'Files uploaded successfully'))
|
||||
if (fileTree.contents.length > 0) {
|
||||
await onDropExternalFiles(fileTree, folder, contents.contents)
|
||||
return
|
||||
}
|
||||
|
||||
const nodes = this.draggingFiles.map(fileid => this.filesStore.getNode(fileid)) as Node[]
|
||||
nodes.forEach(async (node: Node) => {
|
||||
Vue.set(node, 'status', NodeStatus.LOADING)
|
||||
try {
|
||||
// TODO: resolve potential conflicts prior and force overwrite
|
||||
await handleCopyMoveNodeTo(node, this.source, isCopy ? MoveCopyAction.COPY : MoveCopyAction.MOVE)
|
||||
} catch (error) {
|
||||
logger.error('Error while moving file', { error })
|
||||
if (isCopy) {
|
||||
showError(t('files', 'Could not copy {file}. {message}', { file: node.basename, message: error.message || '' }))
|
||||
} else {
|
||||
showError(t('files', 'Could not move {file}. {message}', { file: node.basename, message: error.message || '' }))
|
||||
}
|
||||
} finally {
|
||||
Vue.set(node, 'status', undefined)
|
||||
}
|
||||
})
|
||||
// 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 (this.draggingFiles.some(fileid => this.selectedFiles.includes(fileid))) {
|
||||
if (selection.some(fileid => this.selectedFiles.includes(fileid))) {
|
||||
logger.debug('Dropped selection, resetting select store...')
|
||||
this.selectionStore.reset()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
* @copyright Copyright (c) 2023 Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
*
|
||||
* @author Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
|
|
@ -11,7 +12,7 @@
|
|||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* but WITHOUT ANY WARRANTY without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
|
|
@ -21,123 +22,196 @@
|
|||
*/
|
||||
|
||||
import type { Upload } from '@nextcloud/upload'
|
||||
import type { FileStat, ResponseDataDetailed } from 'webdav'
|
||||
import type { RootDirectory } from './DropServiceUtils'
|
||||
|
||||
import { davGetClient, davGetDefaultPropfind, davResultToNode, davRootPath } from '@nextcloud/files'
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { getUploader } from '@nextcloud/upload'
|
||||
import { Folder, Node, NodeStatus, davRootPath } from '@nextcloud/files'
|
||||
import { getUploader, hasConflict } from '@nextcloud/upload'
|
||||
import { join } from 'path'
|
||||
import { joinPaths } from '@nextcloud/paths'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { showError, showInfo, showSuccess, showWarning } from '@nextcloud/dialogs'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import Vue from 'vue'
|
||||
|
||||
import { Directory, traverseTree, resolveConflict, createDirectoryIfNotExists } from './DropServiceUtils'
|
||||
import { handleCopyMoveNodeTo } from '../actions/moveOrCopyAction'
|
||||
import { MoveCopyAction } from '../actions/moveOrCopyActionUtils'
|
||||
import logger from '../logger.js'
|
||||
|
||||
export const handleDrop = async (data: DataTransfer): Promise<Upload[]> => {
|
||||
// TODO: Maybe handle `getAsFileSystemHandle()` in the future
|
||||
|
||||
const uploads = [] as Upload[]
|
||||
// we need to cache the entries to prevent Blink engine bug that clears the list (`data.items`) after first access props of one of the entries
|
||||
const entries = [...data.items]
|
||||
/**
|
||||
* This function converts a list of DataTransferItems to a file tree.
|
||||
* It uses the Filesystem API if available, otherwise it falls back to the File API.
|
||||
* The File API will NOT be available if the browser is not in a secure context (e.g. HTTP).
|
||||
* ⚠️ When using this method, you need to use it as fast as possible, as the DataTransferItems
|
||||
* will be cleared after the first access to the props of one of the entries.
|
||||
*
|
||||
* @param items the list of DataTransferItems
|
||||
*/
|
||||
export const dataTransferToFileTree = async (items: DataTransferItem[]): Promise<RootDirectory> => {
|
||||
// Check if the browser supports the Filesystem API
|
||||
// We need to cache the entries to prevent Blink engine bug that clears
|
||||
// the list (`data.items`) after first access props of one of the entries
|
||||
const entries = items
|
||||
.filter((item) => {
|
||||
if (item.kind !== 'file') {
|
||||
logger.debug('Skipping dropped item', { kind: item.kind, type: item.type })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
.map((item) => {
|
||||
}).map((item) => {
|
||||
// MDN recommends to try both, as it might be renamed in the future
|
||||
return (item as unknown as { getAsEntry?: () => FileSystemEntry|undefined})?.getAsEntry?.() ?? item.webkitGetAsEntry() ?? item
|
||||
})
|
||||
return (item as unknown as { getAsEntry?: () => FileSystemEntry|undefined })?.getAsEntry?.()
|
||||
?? item?.webkitGetAsEntry?.()
|
||||
?? item
|
||||
}) as (FileSystemEntry | DataTransferItem)[]
|
||||
|
||||
let warned = false
|
||||
const fileTree = new Directory('root') as RootDirectory
|
||||
|
||||
// Traverse the file tree
|
||||
for (const entry of entries) {
|
||||
// Handle browser issues if Filesystem API is not available. Fallback to File API
|
||||
if (entry instanceof DataTransferItem) {
|
||||
logger.debug('Could not get FilesystemEntry of item, falling back to file')
|
||||
logger.warn('Could not get FilesystemEntry of item, falling back to file')
|
||||
|
||||
const file = entry.getAsFile()
|
||||
if (file === null) {
|
||||
logger.warn('Could not process DataTransferItem', { type: entry.type, kind: entry.kind })
|
||||
showError(t('files', 'One of the dropped files could not be processed'))
|
||||
} else {
|
||||
uploads.push(await handleFileUpload(file))
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
logger.debug('Handle recursive upload', { entry: entry.name })
|
||||
// Use Filesystem API
|
||||
uploads.push(...await handleRecursiveUpload(entry))
|
||||
|
||||
// Warn the user that the browser does not support the Filesystem API
|
||||
// we therefore cannot upload directories recursively.
|
||||
if (file.type === 'httpd/unix-directory' || !file.type) {
|
||||
if (!warned) {
|
||||
logger.warn('Browser does not support Filesystem API. Directories will not be uploaded')
|
||||
showWarning(t('files', 'Your browser does not support the Filesystem API. Directories will not be uploaded'))
|
||||
warned = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
fileTree.contents.push(file)
|
||||
continue
|
||||
}
|
||||
|
||||
// Use Filesystem API
|
||||
try {
|
||||
fileTree.contents.push(await traverseTree(entry))
|
||||
} catch (error) {
|
||||
// Do not throw, as we want to continue with the other files
|
||||
logger.error('Error while traversing file tree', { error })
|
||||
}
|
||||
}
|
||||
return uploads
|
||||
|
||||
return fileTree
|
||||
}
|
||||
|
||||
const handleFileUpload = async (file: File, path: string = '') => {
|
||||
export const onDropExternalFiles = async (root: RootDirectory, destination: Folder, contents: Node[]): Promise<Upload[]> => {
|
||||
const uploader = getUploader()
|
||||
|
||||
try {
|
||||
return await uploader.upload(`${path}${file.name}`, file)
|
||||
} catch (e) {
|
||||
showError(t('files', 'Uploading "{filename}" failed', { filename: file.name }))
|
||||
throw e
|
||||
// Check for conflicts on root elements
|
||||
if (await hasConflict(root.contents, contents)) {
|
||||
root.contents = await resolveConflict(root.contents, destination, contents)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRecursiveUpload = async (entry: FileSystemEntry, path: string = ''): Promise<Upload[]> => {
|
||||
if (entry.isFile) {
|
||||
return [
|
||||
await new Promise<Upload>((resolve, reject) => {
|
||||
(entry as FileSystemFileEntry).file(
|
||||
async (file) => resolve(await handleFileUpload(file, path)),
|
||||
(error) => reject(error),
|
||||
)
|
||||
}),
|
||||
]
|
||||
} else {
|
||||
const directory = entry as FileSystemDirectoryEntry
|
||||
|
||||
// TODO: Implement this on `@nextcloud/upload`
|
||||
const absolutPath = joinPaths(davRootPath, getUploader().destination.path, path, directory.name)
|
||||
|
||||
logger.debug('Handle directory recursively', { name: directory.name, absolutPath })
|
||||
|
||||
const davClient = davGetClient()
|
||||
const dirExists = await davClient.exists(absolutPath)
|
||||
if (!dirExists) {
|
||||
logger.debug('Directory does not exist, creating it', { absolutPath })
|
||||
await davClient.createDirectory(absolutPath, { recursive: true })
|
||||
const stat = await davClient.stat(absolutPath, { details: true, data: davGetDefaultPropfind() }) as ResponseDataDetailed<FileStat>
|
||||
emit('files:node:created', davResultToNode(stat.data))
|
||||
}
|
||||
|
||||
const entries = await readDirectory(directory)
|
||||
// sorted so we upload files first before starting next level
|
||||
const promises = entries.sort((a) => a.isFile ? -1 : 1)
|
||||
.map((file) => handleRecursiveUpload(file, `${path}${directory.name}/`))
|
||||
return (await Promise.all(promises)).flat()
|
||||
if (root.contents.length === 0) {
|
||||
logger.info('No files to upload', { root })
|
||||
showInfo(t('files', 'No files to upload'))
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a directory using Filesystem API
|
||||
* @param directory the directory to read
|
||||
*/
|
||||
function readDirectory(directory: FileSystemDirectoryEntry) {
|
||||
const dirReader = directory.createReader()
|
||||
// Let's process the files
|
||||
logger.debug(`Uploading files to ${destination.path}`, { root, contents: root.contents })
|
||||
const queue = [] as Promise<Upload>[]
|
||||
|
||||
return new Promise<FileSystemEntry[]>((resolve, reject) => {
|
||||
const entries = [] as FileSystemEntry[]
|
||||
const getEntries = () => {
|
||||
dirReader.readEntries((results) => {
|
||||
if (results.length) {
|
||||
entries.push(...results)
|
||||
getEntries()
|
||||
} else {
|
||||
resolve(entries)
|
||||
const uploadDirectoryContents = async (directory: Directory, path: string) => {
|
||||
for (const file of directory.contents) {
|
||||
// This is the relative path to the resource
|
||||
// from the current uploader destination
|
||||
const relativePath = join(path, file.name)
|
||||
|
||||
// If the file is a directory, we need to create it first
|
||||
// then browse its tree and upload its contents.
|
||||
if (file instanceof Directory) {
|
||||
const absolutePath = joinPaths(davRootPath, destination.path, relativePath)
|
||||
try {
|
||||
console.debug('Processing directory', { relativePath })
|
||||
await createDirectoryIfNotExists(absolutePath)
|
||||
await uploadDirectoryContents(file, relativePath)
|
||||
} catch (error) {
|
||||
showError(t('files', 'Unable to create the directory {directory}', { directory: file.name }))
|
||||
logger.error('', { error, absolutePath, directory: file })
|
||||
}
|
||||
}, (error) => {
|
||||
reject(error)
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
getEntries()
|
||||
})
|
||||
// If we've reached a file, we can upload it
|
||||
logger.debug('Uploading file to ' + join(destination.path, relativePath), { file })
|
||||
|
||||
// Overriding the root to avoid changing the current uploader context
|
||||
queue.push(uploader.upload(relativePath, file, destination.source))
|
||||
}
|
||||
}
|
||||
|
||||
// Pause the uploader to prevent it from starting
|
||||
// while we compute the queue
|
||||
uploader.pause()
|
||||
|
||||
// Upload the files. Using '/' as the starting point
|
||||
// as we already adjusted the uploader destination
|
||||
await uploadDirectoryContents(root, '/')
|
||||
uploader.start()
|
||||
|
||||
// Wait for all promises to settle
|
||||
const results = await Promise.allSettled(queue)
|
||||
|
||||
// Check for errors
|
||||
const errors = results.filter(result => result.status === 'rejected')
|
||||
if (errors.length > 0) {
|
||||
logger.error('Error while uploading files', { errors })
|
||||
showError(t('files', 'Some files could not be uploaded'))
|
||||
return []
|
||||
}
|
||||
|
||||
logger.debug('Files uploaded successfully')
|
||||
showSuccess(t('files', 'Files uploaded successfully'))
|
||||
|
||||
return Promise.all(queue)
|
||||
}
|
||||
|
||||
export const onDropInternalFiles = async (nodes: Node[], destination: Folder, contents: Node[], isCopy = false) => {
|
||||
const queue = [] as Promise<void>[]
|
||||
|
||||
// Check for conflicts on root elements
|
||||
if (await hasConflict(nodes, contents)) {
|
||||
nodes = await resolveConflict(nodes, destination, contents)
|
||||
}
|
||||
|
||||
if (nodes.length === 0) {
|
||||
logger.info('No files to process', { nodes })
|
||||
showInfo(t('files', 'No files to process'))
|
||||
return
|
||||
}
|
||||
|
||||
for (const node of nodes) {
|
||||
Vue.set(node, 'status', NodeStatus.LOADING)
|
||||
// TODO: resolve potential conflicts prior and force overwrite
|
||||
queue.push(handleCopyMoveNodeTo(node, destination, isCopy ? MoveCopyAction.COPY : MoveCopyAction.MOVE))
|
||||
}
|
||||
|
||||
// Wait for all promises to settle
|
||||
const results = await Promise.allSettled(queue)
|
||||
nodes.forEach(node => Vue.set(node, 'status', undefined))
|
||||
|
||||
// Check for errors
|
||||
const errors = results.filter(result => result.status === 'rejected')
|
||||
if (errors.length > 0) {
|
||||
logger.error('Error while copying or moving files', { errors })
|
||||
showError(isCopy ? t('files', 'Some files could not be copied') : t('files', 'Some files could not be moved'))
|
||||
return
|
||||
}
|
||||
|
||||
logger.debug('Files copy/move successful')
|
||||
showSuccess(isCopy ? t('files', 'Files copied successfully') : t('files', 'Files moved successfully'))
|
||||
}
|
||||
|
|
|
|||
142
apps/files/src/services/DropServiceUtils.spec.ts
Normal file
142
apps/files/src/services/DropServiceUtils.spec.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import { describe, it, expect } from '@jest/globals'
|
||||
|
||||
import { FileSystemDirectoryEntry, FileSystemFileEntry, fileSystemEntryToDataTransferItem, DataTransferItem as DataTransferItemMock } from '../../../../__tests__/FileSystemAPIUtils'
|
||||
import { join } from 'node:path'
|
||||
import { Directory, traverseTree } from './DropServiceUtils'
|
||||
import { dataTransferToFileTree } from './DropService'
|
||||
import logger from '../logger'
|
||||
|
||||
const dataTree = {
|
||||
'file0.txt': ['Hello, world!', 1234567890],
|
||||
dir1: {
|
||||
'file1.txt': ['Hello, world!', 4567891230],
|
||||
'file2.txt': ['Hello, world!', 7891234560],
|
||||
},
|
||||
dir2: {
|
||||
'file3.txt': ['Hello, world!', 1234567890],
|
||||
},
|
||||
}
|
||||
|
||||
// This is mocking a file tree using the FileSystem API
|
||||
const buildFileSystemDirectoryEntry = (path: string, tree: any): FileSystemDirectoryEntry => {
|
||||
const entries = Object.entries(tree).map(([name, contents]) => {
|
||||
const fullPath = join(path, name)
|
||||
if (Array.isArray(contents)) {
|
||||
return new FileSystemFileEntry(fullPath, contents[0], contents[1])
|
||||
} else {
|
||||
return buildFileSystemDirectoryEntry(fullPath, contents)
|
||||
}
|
||||
})
|
||||
return new FileSystemDirectoryEntry(path, entries)
|
||||
}
|
||||
|
||||
const buildDataTransferItemArray = (path: string, tree: any, isFileSystemAPIAvailable = true): DataTransferItemMock[] => {
|
||||
return Object.entries(tree).map(([name, contents]) => {
|
||||
const fullPath = join(path, name)
|
||||
if (Array.isArray(contents)) {
|
||||
const entry = new FileSystemFileEntry(fullPath, contents[0], contents[1])
|
||||
return fileSystemEntryToDataTransferItem(entry, isFileSystemAPIAvailable)
|
||||
}
|
||||
|
||||
const entry = buildFileSystemDirectoryEntry(fullPath, contents)
|
||||
return fileSystemEntryToDataTransferItem(entry, isFileSystemAPIAvailable)
|
||||
})
|
||||
}
|
||||
|
||||
describe('Filesystem API traverseTree', () => {
|
||||
it('Should traverse a file tree from root', async () => {
|
||||
// Fake a FileSystemEntry tree
|
||||
const root = buildFileSystemDirectoryEntry('root', dataTree)
|
||||
const tree = await traverseTree(root as unknown as FileSystemEntry) as Directory
|
||||
|
||||
expect(tree.name).toBe('root')
|
||||
expect(tree).toBeInstanceOf(Directory)
|
||||
expect(tree.contents).toHaveLength(3)
|
||||
expect(tree.size).toBe(13 * 4) // 13 bytes from 'Hello, world!'
|
||||
})
|
||||
|
||||
it('Should traverse a file tree from a subdirectory', async () => {
|
||||
// Fake a FileSystemEntry tree
|
||||
const dir2 = buildFileSystemDirectoryEntry('dir2', dataTree.dir2)
|
||||
const tree = await traverseTree(dir2 as unknown as FileSystemEntry) as Directory
|
||||
|
||||
expect(tree.name).toBe('dir2')
|
||||
expect(tree).toBeInstanceOf(Directory)
|
||||
expect(tree.contents).toHaveLength(1)
|
||||
expect(tree.contents[0].name).toBe('file3.txt')
|
||||
expect(tree.size).toBe(13) // 13 bytes from 'Hello, world!'
|
||||
})
|
||||
|
||||
it('Should properly compute the last modified', async () => {
|
||||
// Fake a FileSystemEntry tree
|
||||
const root = buildFileSystemDirectoryEntry('root', dataTree)
|
||||
const rootTree = await traverseTree(root as unknown as FileSystemEntry) as Directory
|
||||
|
||||
expect(rootTree.lastModified).toBe(7891234560)
|
||||
|
||||
// Fake a FileSystemEntry tree
|
||||
const dir2 = buildFileSystemDirectoryEntry('root', dataTree.dir2)
|
||||
const dir2Tree = await traverseTree(dir2 as unknown as FileSystemEntry) as Directory
|
||||
expect(dir2Tree.lastModified).toBe(1234567890)
|
||||
})
|
||||
})
|
||||
|
||||
describe('DropService dataTransferToFileTree', () => {
|
||||
|
||||
beforeAll(() => {
|
||||
// DataTransferItem doesn't exists in jsdom, let's mock
|
||||
// a dumb one so we can check the instanceof
|
||||
// @ts-expect-error jsdom doesn't have DataTransferItem
|
||||
window.DataTransferItem = DataTransferItemMock
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
// @ts-expect-error jsdom doesn't have DataTransferItem
|
||||
delete window.DataTransferItem
|
||||
})
|
||||
|
||||
it('Should return a RootDirectory with Filesystem API', async () => {
|
||||
jest.spyOn(logger, 'error').mockImplementation(() => jest.fn())
|
||||
jest.spyOn(logger, 'warn').mockImplementation(() => jest.fn())
|
||||
|
||||
const dataTransferItems = buildDataTransferItemArray('root', dataTree)
|
||||
const fileTree = await dataTransferToFileTree(dataTransferItems as unknown as DataTransferItem[])
|
||||
|
||||
expect(fileTree.name).toBe('root')
|
||||
expect(fileTree).toBeInstanceOf(Directory)
|
||||
expect(fileTree.contents).toHaveLength(3)
|
||||
|
||||
// The file tree should be recursive when using the Filesystem API
|
||||
expect(fileTree.contents[1]).toBeInstanceOf(Directory)
|
||||
expect((fileTree.contents[1] as Directory).contents).toHaveLength(2)
|
||||
expect(fileTree.contents[2]).toBeInstanceOf(Directory)
|
||||
expect((fileTree.contents[2] as Directory).contents).toHaveLength(1)
|
||||
|
||||
expect(logger.error).not.toBeCalled()
|
||||
expect(logger.warn).not.toBeCalled()
|
||||
})
|
||||
|
||||
it('Should return a RootDirectory with legacy File API ignoring recursive directories', async () => {
|
||||
jest.spyOn(logger, 'error').mockImplementation(() => jest.fn())
|
||||
jest.spyOn(logger, 'warn').mockImplementation(() => jest.fn())
|
||||
|
||||
const dataTransferItems = buildDataTransferItemArray('root', dataTree, false)
|
||||
|
||||
const fileTree = await dataTransferToFileTree(dataTransferItems as unknown as DataTransferItem[])
|
||||
|
||||
expect(fileTree.name).toBe('root')
|
||||
expect(fileTree).toBeInstanceOf(Directory)
|
||||
expect(fileTree.contents).toHaveLength(1)
|
||||
|
||||
// The file tree should be recursive when using the Filesystem API
|
||||
expect(fileTree.contents[0]).not.toBeInstanceOf(Directory)
|
||||
expect((fileTree.contents[0].name)).toBe('file0.txt')
|
||||
|
||||
expect(logger.error).not.toBeCalled()
|
||||
expect(logger.warn).toHaveBeenNthCalledWith(1, 'Could not get FilesystemEntry of item, falling back to file')
|
||||
expect(logger.warn).toHaveBeenNthCalledWith(2, 'Could not get FilesystemEntry of item, falling back to file')
|
||||
expect(logger.warn).toHaveBeenNthCalledWith(3, 'Browser does not support Filesystem API. Directories will not be uploaded')
|
||||
expect(logger.warn).toHaveBeenNthCalledWith(4, 'Could not get FilesystemEntry of item, falling back to file')
|
||||
expect(logger.warn).toHaveBeenCalledTimes(4)
|
||||
})
|
||||
})
|
||||
195
apps/files/src/services/DropServiceUtils.ts
Normal file
195
apps/files/src/services/DropServiceUtils.ts
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2024 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
import type { FileStat, ResponseDataDetailed } from 'webdav'
|
||||
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { Folder, Node, davGetClient, davGetDefaultPropfind, davResultToNode } from '@nextcloud/files'
|
||||
import { openConflictPicker } from '@nextcloud/upload'
|
||||
import { showError, showInfo } from '@nextcloud/dialogs'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
|
||||
import logger from '../logger.js'
|
||||
|
||||
/**
|
||||
* This represents a Directory in the file tree
|
||||
* We extend the File class to better handling uploading
|
||||
* and stay as close as possible as the Filesystem API.
|
||||
* This also allow us to hijack the size or lastModified
|
||||
* properties to compute them dynamically.
|
||||
*/
|
||||
export class Directory extends File {
|
||||
|
||||
/* eslint-disable no-use-before-define */
|
||||
_contents: (Directory|File)[]
|
||||
|
||||
constructor(name, contents: (Directory|File)[] = []) {
|
||||
super([], name, { type: 'httpd/unix-directory' })
|
||||
this._contents = contents
|
||||
}
|
||||
|
||||
set contents(contents: (Directory|File)[]) {
|
||||
this._contents = contents
|
||||
}
|
||||
|
||||
get contents(): (Directory|File)[] {
|
||||
return this._contents
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this._computeDirectorySize(this)
|
||||
}
|
||||
|
||||
get lastModified() {
|
||||
if (this._contents.length === 0) {
|
||||
return Date.now()
|
||||
}
|
||||
return this._computeDirectoryMtime(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last modification time of a file tree
|
||||
* This is not perfect, but will get us a pretty good approximation
|
||||
* @param directory the directory to traverse
|
||||
*/
|
||||
_computeDirectoryMtime(directory: Directory): number {
|
||||
return directory.contents.reduce((acc, file) => {
|
||||
return file.lastModified > acc
|
||||
// If the file is a directory, the lastModified will
|
||||
// also return the results of its _computeDirectoryMtime method
|
||||
// Fancy recursion, huh?
|
||||
? file.lastModified
|
||||
: acc
|
||||
}, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of a file tree
|
||||
* @param directory the directory to traverse
|
||||
*/
|
||||
_computeDirectorySize(directory: Directory): number {
|
||||
return directory.contents.reduce((acc: number, entry: Directory|File) => {
|
||||
// If the file is a directory, the size will
|
||||
// also return the results of its _computeDirectorySize method
|
||||
// Fancy recursion, huh?
|
||||
return acc + entry.size
|
||||
}, 0)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export type RootDirectory = Directory & {
|
||||
name: 'root'
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverse a file tree using the Filesystem API
|
||||
* @param entry the entry to traverse
|
||||
*/
|
||||
export const traverseTree = async (entry: FileSystemEntry): Promise<Directory|File> => {
|
||||
// Handle file
|
||||
if (entry.isFile) {
|
||||
return new Promise<File>((resolve, reject) => {
|
||||
(entry as FileSystemFileEntry).file(resolve, reject)
|
||||
})
|
||||
}
|
||||
|
||||
// Handle directory
|
||||
logger.debug('Handling recursive file tree', { entry: entry.name })
|
||||
const directory = entry as FileSystemDirectoryEntry
|
||||
const entries = await readDirectory(directory)
|
||||
const contents = (await Promise.all(entries.map(traverseTree))).flat()
|
||||
return new Directory(directory.name, contents)
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a directory using Filesystem API
|
||||
* @param directory the directory to read
|
||||
*/
|
||||
const readDirectory = (directory: FileSystemDirectoryEntry): Promise<FileSystemEntry[]> => {
|
||||
const dirReader = directory.createReader()
|
||||
|
||||
return new Promise<FileSystemEntry[]>((resolve, reject) => {
|
||||
const entries = [] as FileSystemEntry[]
|
||||
const getEntries = () => {
|
||||
dirReader.readEntries((results) => {
|
||||
if (results.length) {
|
||||
entries.push(...results)
|
||||
getEntries()
|
||||
} else {
|
||||
resolve(entries)
|
||||
}
|
||||
}, (error) => {
|
||||
reject(error)
|
||||
})
|
||||
}
|
||||
|
||||
getEntries()
|
||||
})
|
||||
}
|
||||
|
||||
export const createDirectoryIfNotExists = async (absolutePath: string) => {
|
||||
const davClient = davGetClient()
|
||||
const dirExists = await davClient.exists(absolutePath)
|
||||
if (!dirExists) {
|
||||
logger.debug('Directory does not exist, creating it', { absolutePath })
|
||||
await davClient.createDirectory(absolutePath, { recursive: true })
|
||||
const stat = await davClient.stat(absolutePath, { details: true, data: davGetDefaultPropfind() }) as ResponseDataDetailed<FileStat>
|
||||
emit('files:node:created', davResultToNode(stat.data))
|
||||
}
|
||||
}
|
||||
|
||||
export const resolveConflict = async <T extends ((Directory|File)|Node)>(files: Array<T>, destination: Folder, contents: Node[]): Promise<T[]> => {
|
||||
try {
|
||||
// List all conflicting files
|
||||
const conflicts = files.filter((file: File|Node) => {
|
||||
return contents.find((node: Node) => node.basename === (file instanceof File ? file.name : file.basename))
|
||||
}).filter(Boolean) as (File|Node)[]
|
||||
|
||||
// List of incoming files that are NOT in conflict
|
||||
const uploads = files.filter((file: File|Node) => {
|
||||
return !conflicts.includes(file)
|
||||
})
|
||||
|
||||
// Let the user choose what to do with the conflicting files
|
||||
const { selected, renamed } = await openConflictPicker(destination.path, conflicts, contents)
|
||||
|
||||
logger.debug('Conflict resolution', { uploads, selected, renamed })
|
||||
|
||||
// If the user selected nothing, we cancel the upload
|
||||
if (selected.length === 0 && renamed.length === 0) {
|
||||
// User skipped
|
||||
showInfo(t('files', 'Conflicts resolution skipped'))
|
||||
logger.info('User skipped the conflict resolution')
|
||||
return []
|
||||
}
|
||||
|
||||
// Update the list of files to upload
|
||||
return [...uploads, ...selected, ...renamed] as (typeof files)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
// User cancelled
|
||||
showError(t('files', 'Upload cancelled'))
|
||||
logger.error('User cancelled the upload')
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -30,6 +30,7 @@ export default defineConfig({
|
|||
experimentalInteractiveRunEvents: true,
|
||||
|
||||
// faster video processing
|
||||
video: !process.env.CI,
|
||||
videoCompression: false,
|
||||
|
||||
// Visual regression testing
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@ describe('files: Drag and Drop', { testIsolation: true }, () => {
|
|||
|
||||
cy.intercept('PUT', /\/remote.php\/dav\/files\//).as('uploadFile')
|
||||
|
||||
// Make sure the drop notice is not visible
|
||||
cy.get('[data-cy-files-drag-drop-area]').should('not.be.visible')
|
||||
|
||||
// Trigger the drop notice
|
||||
cy.get('main.app-content').trigger('dragover', { dataTransfer })
|
||||
cy.get('[data-cy-files-drag-drop-area]').should('be.visible')
|
||||
|
|
@ -27,6 +29,11 @@ describe('files: Drag and Drop', { testIsolation: true }, () => {
|
|||
|
||||
cy.wait('@uploadFile')
|
||||
|
||||
// Make sure the upload is finished
|
||||
cy.get('[data-cy-files-drag-drop-area]').should('not.be.visible')
|
||||
cy.get('[data-cy-upload-picker] progress').should('not.be.visible')
|
||||
cy.get('@uploadFile.all').should('have.length', 1)
|
||||
|
||||
getRowForFile('single-file.txt').should('be.visible')
|
||||
getRowForFile('single-file.txt').find('[data-cy-files-list-row-size]').should('contain', '6 KB')
|
||||
})
|
||||
|
|
@ -38,6 +45,9 @@ describe('files: Drag and Drop', { testIsolation: true }, () => {
|
|||
|
||||
cy.intercept('PUT', /\/remote.php\/dav\/files\//).as('uploadFile')
|
||||
|
||||
// Make sure the drop notice is not visible
|
||||
cy.get('[data-cy-files-drag-drop-area]').should('not.be.visible')
|
||||
|
||||
// Trigger the drop notice
|
||||
cy.get('main.app-content').trigger('dragover', { dataTransfer })
|
||||
cy.get('[data-cy-files-drag-drop-area]').should('be.visible')
|
||||
|
|
@ -56,7 +66,71 @@ describe('files: Drag and Drop', { testIsolation: true }, () => {
|
|||
|
||||
cy.wait('@uploadFile')
|
||||
|
||||
// Make sure the upload is finished
|
||||
cy.get('[data-cy-files-drag-drop-area]').should('not.be.visible')
|
||||
cy.get('[data-cy-upload-picker] progress').should('not.be.visible')
|
||||
cy.get('@uploadFile.all').should('have.length', 2)
|
||||
|
||||
getRowForFile('first.txt').should('be.visible')
|
||||
getRowForFile('second.txt').should('be.visible')
|
||||
})
|
||||
|
||||
it('will ignore legacy Folders', () => {
|
||||
cy.window().then((win) => {
|
||||
// Remove the Filesystem API to force the legacy File API
|
||||
// See how cypress mocks the Filesystem API in https://github.com/cypress-io/cypress/blob/74109094a92df3bef073dda15f17194f31850d7d/packages/driver/src/cy/commands/actions/selectFile.ts#L24-L37
|
||||
Object.defineProperty(win.DataTransferItem.prototype, 'getAsEntry', { get: undefined })
|
||||
Object.defineProperty(win.DataTransferItem.prototype, 'webkitGetAsEntry', { get: undefined })
|
||||
})
|
||||
|
||||
const dataTransfer = new DataTransfer()
|
||||
dataTransfer.items.add(new File([], 'first.txt'))
|
||||
dataTransfer.items.add(new File([], 'second.txt'))
|
||||
|
||||
// Legacy File API (not FileSystem API), will treat Folders as Files
|
||||
// with empty type and empty content
|
||||
dataTransfer.items.add(new File([], 'Foo', { type: 'httpd/unix-directory' }))
|
||||
dataTransfer.items.add(new File([], 'Bar'))
|
||||
|
||||
cy.intercept('PUT', /\/remote.php\/dav\/files\//).as('uploadFile')
|
||||
|
||||
// Make sure the drop notice is not visible
|
||||
cy.get('[data-cy-files-drag-drop-area]').should('not.be.visible')
|
||||
|
||||
// Trigger the drop notice
|
||||
cy.get('main.app-content').trigger('dragover', { dataTransfer })
|
||||
cy.get('[data-cy-files-drag-drop-area]').should('be.visible')
|
||||
|
||||
// Upload drop a file
|
||||
cy.get('[data-cy-files-drag-drop-area]').selectFile([
|
||||
{
|
||||
fileName: 'first.txt',
|
||||
contents: ['Hello'],
|
||||
},
|
||||
{
|
||||
fileName: 'second.txt',
|
||||
contents: ['World'],
|
||||
},
|
||||
{
|
||||
fileName: 'Foo',
|
||||
contents: {},
|
||||
},
|
||||
{
|
||||
fileName: 'Bar',
|
||||
contents: { mimeType: 'httpd/unix-directory' },
|
||||
},
|
||||
], { action: 'drag-drop' })
|
||||
|
||||
cy.wait('@uploadFile')
|
||||
|
||||
// Make sure the upload is finished
|
||||
cy.get('[data-cy-files-drag-drop-area]').should('not.be.visible')
|
||||
cy.get('[data-cy-upload-picker] progress').should('not.be.visible')
|
||||
cy.get('@uploadFile.all').should('have.length', 2)
|
||||
|
||||
getRowForFile('first.txt').should('be.visible')
|
||||
getRowForFile('second.txt').should('be.visible')
|
||||
getRowForFile('Foo').should('not.exist')
|
||||
getRowForFile('Bar').should('not.exist')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
3
dist/1359-1359.js
vendored
Normal file
3
dist/1359-1359.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/1359-1359.js.map
vendored
Normal file
1
dist/1359-1359.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
dist/1462-1462.js
vendored
2
dist/1462-1462.js
vendored
|
|
@ -1,2 +0,0 @@
|
|||
"use strict";(self.webpackChunknextcloud=self.webpackChunknextcloud||[]).push([[1462],{552:(t,e,n)=>{n.d(e,{Z:()=>i});var o=n(87537),r=n.n(o),s=n(23645),a=n.n(s)()(r());a.push([t.id,"\n.comments-action[data-v-fffab6ae] {\n\tpadding: 0;\n}\n","",{version:3,sources:["webpack://./apps/comments/src/views/ActivityCommentAction.vue"],names:[],mappings:";AAmEA;CACA,UAAA;AACA",sourcesContent:['\x3c!--\n - @copyright Copyright (c) 2023 Ferdinand Thiessen <opensource@fthiessen.de>\n -\n - @author Ferdinand Thiessen <opensource@fthiessen.de>\n -\n - @license AGPL-3.0-or-later\n -\n - This program is free software: you can redistribute it and/or modify\n - it under the terms of the GNU Affero General Public License as\n - published by the Free Software Foundation, either version 3 of the\n - License, or (at your option) any later version.\n -\n - This program is distributed in the hope that it will be useful,\n - but WITHOUT ANY WARRANTY; without even the implied warranty of\n - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n - GNU Affero General Public License for more details.\n -\n - You should have received a copy of the GNU Affero General Public License\n - along with this program. If not, see <http://www.gnu.org/licenses/>.\n -\n --\x3e\n\n<template>\n\t<Comment v-bind="editorData"\n\t\t:auto-complete="autoComplete"\n\t\t:resource-type="resourceType"\n\t\t:editor="true"\n\t\t:user-data="userData"\n\t\t:resource-id="resourceId"\n\t\tclass="comments-action"\n\t\t@new="onNewComment" />\n</template>\n\n<script lang="ts">\nimport { defineComponent } from \'vue\'\nimport Comment from \'../components/Comment.vue\'\nimport CommentView from \'../mixins/CommentView.js\'\nimport logger from \'../logger\'\nimport { showError } from \'@nextcloud/dialogs\'\nimport { translate as t } from \'@nextcloud/l10n\'\n\nexport default defineComponent({\n\tcomponents: {\n\t\tComment,\n\t},\n\tmixins: [CommentView],\n\tprops: {\n\t\treloadCallback: {\n\t\t\ttype: Function,\n\t\t\trequired: true,\n\t\t},\n\t},\n\tmethods: {\n\t\tonNewComment() {\n\t\t\ttry {\n\t\t\t\t// just force reload\n\t\t\t\tthis.reloadCallback()\n\t\t\t} catch (e) {\n\t\t\t\tshowError(t(\'comments\', \'Could not reload comments\'))\n\t\t\t\tlogger.debug(e)\n\t\t\t}\n\t\t},\n\t},\n})\n<\/script>\n\n<style scoped>\n.comments-action {\n\tpadding: 0;\n}\n</style>\n'],sourceRoot:""}]);const i=a},91462:(t,e,n)=>{n.d(e,{default:()=>x});var o=n(20144),r=n(49251),s=n(94534),a=n(59953),i=n(64024),m=n(31352);const c=(0,o.aZ)({components:{Comment:r.Z},mixins:[s.Z],props:{reloadCallback:{type:Function,required:!0}},methods:{onNewComment(){try{this.reloadCallback()}catch(t){(0,i.x2)((0,m.Iu)("comments","Could not reload comments")),a.Z.debug(t)}}}});var l=n(93379),u=n.n(l),d=n(7795),p=n.n(d),f=n(90569),h=n.n(f),C=n(3565),A=n.n(C),b=n(19216),w=n.n(b),y=n(44589),g=n.n(y),v=n(552),T={};T.styleTagTransform=g(),T.setAttributes=A(),T.insert=h().bind(null,"head"),T.domAPI=p(),T.insertStyleElement=w(),u()(v.Z,T),v.Z&&v.Z.locals&&v.Z.locals;const x=(0,n(51900).Z)(c,(function(){var t=this,e=t._self._c;return t._self._setupProxy,e("Comment",t._b({staticClass:"comments-action",attrs:{"auto-complete":t.autoComplete,"resource-type":t.resourceType,editor:!0,"user-data":t.userData,"resource-id":t.resourceId},on:{new:t.onNewComment}},"Comment",t.editorData,!1))}),[],!1,null,"fffab6ae",null).exports}}]);
|
||||
//# sourceMappingURL=1462-1462.js.map?v=03cc0c51ba09241e2635
|
||||
3
dist/2379-2379.js
vendored
Normal file
3
dist/2379-2379.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/2379-2379.js.map
vendored
Normal file
1
dist/2379-2379.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
3
dist/2674-2674.js
vendored
Normal file
3
dist/2674-2674.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/2674-2674.js.map
vendored
Normal file
1
dist/2674-2674.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
dist/2913-2913.js
vendored
Normal file
2
dist/2913-2913.js
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
"use strict";(self.webpackChunknextcloud=self.webpackChunknextcloud||[]).push([[2913],{57786:(t,e,n)=>{n.d(e,{A:()=>i});var o=n(71354),r=n.n(o),s=n(76314),a=n.n(s)()(r());a.push([t.id,"\n.comments-action[data-v-fffab6ae] {\n\tpadding: 0;\n}\n","",{version:3,sources:["webpack://./apps/comments/src/views/ActivityCommentAction.vue"],names:[],mappings:";AAmEA;CACA,UAAA;AACA",sourcesContent:['\x3c!--\n - @copyright Copyright (c) 2023 Ferdinand Thiessen <opensource@fthiessen.de>\n -\n - @author Ferdinand Thiessen <opensource@fthiessen.de>\n -\n - @license AGPL-3.0-or-later\n -\n - This program is free software: you can redistribute it and/or modify\n - it under the terms of the GNU Affero General Public License as\n - published by the Free Software Foundation, either version 3 of the\n - License, or (at your option) any later version.\n -\n - This program is distributed in the hope that it will be useful,\n - but WITHOUT ANY WARRANTY; without even the implied warranty of\n - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n - GNU Affero General Public License for more details.\n -\n - You should have received a copy of the GNU Affero General Public License\n - along with this program. If not, see <http://www.gnu.org/licenses/>.\n -\n --\x3e\n\n<template>\n\t<Comment v-bind="editorData"\n\t\t:auto-complete="autoComplete"\n\t\t:resource-type="resourceType"\n\t\t:editor="true"\n\t\t:user-data="userData"\n\t\t:resource-id="resourceId"\n\t\tclass="comments-action"\n\t\t@new="onNewComment" />\n</template>\n\n<script lang="ts">\nimport { defineComponent } from \'vue\'\nimport Comment from \'../components/Comment.vue\'\nimport CommentView from \'../mixins/CommentView.js\'\nimport logger from \'../logger\'\nimport { showError } from \'@nextcloud/dialogs\'\nimport { translate as t } from \'@nextcloud/l10n\'\n\nexport default defineComponent({\n\tcomponents: {\n\t\tComment,\n\t},\n\tmixins: [CommentView],\n\tprops: {\n\t\treloadCallback: {\n\t\t\ttype: Function,\n\t\t\trequired: true,\n\t\t},\n\t},\n\tmethods: {\n\t\tonNewComment() {\n\t\t\ttry {\n\t\t\t\t// just force reload\n\t\t\t\tthis.reloadCallback()\n\t\t\t} catch (e) {\n\t\t\t\tshowError(t(\'comments\', \'Could not reload comments\'))\n\t\t\t\tlogger.debug(e)\n\t\t\t}\n\t\t},\n\t},\n})\n<\/script>\n\n<style scoped>\n.comments-action {\n\tpadding: 0;\n}\n</style>\n'],sourceRoot:""}]);const i=a},72913:(t,e,n)=>{n.d(e,{default:()=>x});var o=n(85471),r=n(65463),s=n(70452),a=n(96689),i=n(85168),m=n(53334);const c=(0,o.pM)({components:{Comment:r.A},mixins:[s.A],props:{reloadCallback:{type:Function,required:!0}},methods:{onNewComment(){try{this.reloadCallback()}catch(t){(0,i.Qg)((0,m.Tl)("comments","Could not reload comments")),a.A.debug(t)}}}});var l=n(85072),u=n.n(l),d=n(97825),p=n.n(d),f=n(77659),h=n.n(f),A=n(55056),C=n.n(A),b=n(10540),w=n.n(b),g=n(41113),y=n.n(g),v=n(57786),T={};T.styleTagTransform=y(),T.setAttributes=C(),T.insert=h().bind(null,"head"),T.domAPI=p(),T.insertStyleElement=w(),u()(v.A,T),v.A&&v.A.locals&&v.A.locals;const x=(0,n(14486).A)(c,(function(){var t=this,e=t._self._c;return t._self._setupProxy,e("Comment",t._b({staticClass:"comments-action",attrs:{"auto-complete":t.autoComplete,"resource-type":t.resourceType,editor:!0,"user-data":t.userData,"resource-id":t.resourceId},on:{new:t.onNewComment}},"Comment",t.editorData,!1))}),[],!1,null,"fffab6ae",null).exports}}]);
|
||||
//# sourceMappingURL=2913-2913.js.map?v=1ccb2adaaea884424d3c
|
||||
File diff suppressed because one or more lines are too long
2
dist/3777-3777.js
vendored
Normal file
2
dist/3777-3777.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/3777-3777.js.map
vendored
Normal file
1
dist/3777-3777.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
dist/3841-3841.js
vendored
Normal file
2
dist/3841-3841.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/3841-3841.js.map
vendored
Normal file
1
dist/3841-3841.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
3
dist/430-430.js
vendored
3
dist/430-430.js
vendored
|
|
@ -1,3 +0,0 @@
|
|||
/*! For license information please see 430-430.js.LICENSE.txt */
|
||||
"use strict";(self.webpackChunknextcloud=self.webpackChunknextcloud||[]).push([[430],{50430:(e,c,l)=>{l.d(c,{FilePickerVue:()=>n});const n=(0,l(20144).RC)((()=>Promise.all([l.e(7874),l.e(5951)]).then(l.bind(l,75916))))}}]);
|
||||
//# sourceMappingURL=430-430.js.map?v=77fc3fbf1e72551ad8b1
|
||||
1
dist/430-430.js.map
vendored
1
dist/430-430.js.map
vendored
|
|
@ -1 +0,0 @@
|
|||
{"version":3,"file":"430-430.js?v=77fc3fbf1e72551ad8b1","mappings":";mIAsBA,MAAMA,GAAI,gBAAE,IAAM","sources":["webpack:///nextcloud/node_modules/@nextcloud/dialogs/dist/chunks/index-Xjd5r2aR.mjs"],"sourcesContent":["import { defineAsyncComponent as e } from \"vue\";\n/**\n * @copyright Copyright (c) 2023 Ferdinand Thiessen <opensource@fthiessen.de>\n *\n * @author Ferdinand Thiessen <opensource@fthiessen.de>\n *\n * @license AGPL-3.0-or-later\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <http://www.gnu.org/licenses/>.\n *\n */\nconst i = e(() => import(\"./FilePicker-gEH28Uzn.mjs\"));\nexport {\n i as FilePickerVue\n};\n"],"names":["i"],"sourceRoot":""}
|
||||
3
dist/448-448.js
vendored
3
dist/448-448.js
vendored
File diff suppressed because one or more lines are too long
1
dist/448-448.js.map
vendored
1
dist/448-448.js.map
vendored
File diff suppressed because one or more lines are too long
2
dist/6318-6318.js → dist/5528-5528.js
vendored
2
dist/6318-6318.js → dist/5528-5528.js
vendored
|
|
@ -1 +1 @@
|
|||
"use strict";(self.webpackChunknextcloud=self.webpackChunknextcloud||[]).push([[6318],{46318:(e,u,t)=>{t.r(u),t.d(u,{NcAutoCompleteResult:()=>c.N,NcMentionBubble:()=>l.N,default:()=>c.a});var l=t(45293),c=t(5840)}}]);
|
||||
"use strict";(self.webpackChunknextcloud=self.webpackChunknextcloud||[]).push([[5528],{95528:(e,u,t)=>{t.r(u),t.d(u,{NcAutoCompleteResult:()=>c.N,NcMentionBubble:()=>l.N,default:()=>c.a});var l=t(11285),c=t(98379)}}]);
|
||||
4
dist/4322-4322.js → dist/5632-5632.js
vendored
4
dist/4322-4322.js → dist/5632-5632.js
vendored
|
|
@ -1,2 +1,2 @@
|
|||
"use strict";(self.webpackChunknextcloud=self.webpackChunknextcloud||[]).push([[4322],{69376:(t,e,n)=>{n.d(e,{Z:()=>a});var o=n(87537),s=n.n(o),m=n(23645),r=n.n(m)()(s());r.push([t.id,"\n.comments-activity[data-v-188dae4a] {\n\tpadding: 0;\n}\n","",{version:3,sources:["webpack://./apps/comments/src/views/ActivityCommentEntry.vue"],names:[],mappings:";AAmFA;CACA,UAAA;AACA",sourcesContent:['\x3c!--\n - @copyright Copyright (c) 2023 Ferdinand Thiessen <opensource@fthiessen.de>\n -\n - @author Ferdinand Thiessen <opensource@fthiessen.de>\n -\n - @license AGPL-3.0-or-later\n -\n - This program is free software: you can redistribute it and/or modify\n - it under the terms of the GNU Affero General Public License as\n - published by the Free Software Foundation, either version 3 of the\n - License, or (at your option) any later version.\n -\n - This program is distributed in the hope that it will be useful,\n - but WITHOUT ANY WARRANTY; without even the implied warranty of\n - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n - GNU Affero General Public License for more details.\n -\n - You should have received a copy of the GNU Affero General Public License\n - along with this program. If not, see <http://www.gnu.org/licenses/>.\n -\n --\x3e\n\n<template>\n\t<Comment ref="comment"\n\t\ttag="li"\n\t\tv-bind="comment.props"\n\t\t:auto-complete="autoComplete"\n\t\t:resource-type="resourceType"\n\t\t:message="commentMessage"\n\t\t:resource-id="resourceId"\n\t\t:user-data="genMentionsData(comment.props.mentions)"\n\t\tclass="comments-activity"\n\t\t@delete="reloadCallback()" />\n</template>\n\n<script lang="ts">\nimport { translate as t } from \'@nextcloud/l10n\'\n\nimport Comment from \'../components/Comment.vue\'\nimport CommentView from \'../mixins/CommentView\'\n\nexport default {\n\tname: \'ActivityCommentEntry\',\n\n\tcomponents: {\n\t\tComment,\n\t},\n\n\tmixins: [CommentView],\n\tprops: {\n\t\tcomment: {\n\t\t\ttype: Object,\n\t\t\trequired: true,\n\t\t},\n\t\treloadCallback: {\n\t\t\ttype: Function,\n\t\t\trequired: true,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tcommentMessage: \'\',\n\t\t}\n\t},\n\n\twatch: {\n\t\tcomment() {\n\t\t\tthis.commentMessage = this.comment.props.message\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.commentMessage = this.comment.props.message\n\t},\n\n\tmethods: {\n\t\tt,\n\t},\n}\n<\/script>\n\n<style scoped>\n.comments-activity {\n\tpadding: 0;\n}\n</style>\n'],sourceRoot:""}]);const a=r},24322:(t,e,n)=>{n.d(e,{default:()=>v});var o=n(31352),s=n(49251),m=n(94534);const r={name:"ActivityCommentEntry",components:{Comment:s.Z},mixins:[m.Z],props:{comment:{type:Object,required:!0},reloadCallback:{type:Function,required:!0}},data:()=>({commentMessage:""}),watch:{comment(){this.commentMessage=this.comment.props.message}},mounted(){this.commentMessage=this.comment.props.message},methods:{t:o.Iu}};var a=n(93379),i=n.n(a),c=n(7795),p=n.n(c),u=n(90569),l=n.n(u),d=n(3565),h=n.n(d),f=n(19216),g=n.n(f),y=n(44589),C=n.n(y),A=n(69376),b={};b.styleTagTransform=C(),b.setAttributes=h(),b.insert=l().bind(null,"head"),b.domAPI=p(),b.insertStyleElement=g(),i()(A.Z,b),A.Z&&A.Z.locals&&A.Z.locals;const v=(0,n(51900).Z)(r,(function(){var t=this;return(0,t._self._c)("Comment",t._b({ref:"comment",staticClass:"comments-activity",attrs:{tag:"li","auto-complete":t.autoComplete,"resource-type":t.resourceType,message:t.commentMessage,"resource-id":t.resourceId,"user-data":t.genMentionsData(t.comment.props.mentions)},on:{delete:function(e){return t.reloadCallback()}}},"Comment",t.comment.props,!1))}),[],!1,null,"188dae4a",null).exports}}]);
|
||||
//# sourceMappingURL=4322-4322.js.map?v=412f17bea41d1e48d8f3
|
||||
"use strict";(self.webpackChunknextcloud=self.webpackChunknextcloud||[]).push([[5632],{67695:(t,e,n)=>{n.d(e,{A:()=>a});var o=n(71354),s=n.n(o),m=n(76314),r=n.n(m)()(s());r.push([t.id,"\n.comments-activity[data-v-188dae4a] {\n\tpadding: 0;\n}\n","",{version:3,sources:["webpack://./apps/comments/src/views/ActivityCommentEntry.vue"],names:[],mappings:";AAmFA;CACA,UAAA;AACA",sourcesContent:['\x3c!--\n - @copyright Copyright (c) 2023 Ferdinand Thiessen <opensource@fthiessen.de>\n -\n - @author Ferdinand Thiessen <opensource@fthiessen.de>\n -\n - @license AGPL-3.0-or-later\n -\n - This program is free software: you can redistribute it and/or modify\n - it under the terms of the GNU Affero General Public License as\n - published by the Free Software Foundation, either version 3 of the\n - License, or (at your option) any later version.\n -\n - This program is distributed in the hope that it will be useful,\n - but WITHOUT ANY WARRANTY; without even the implied warranty of\n - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n - GNU Affero General Public License for more details.\n -\n - You should have received a copy of the GNU Affero General Public License\n - along with this program. If not, see <http://www.gnu.org/licenses/>.\n -\n --\x3e\n\n<template>\n\t<Comment ref="comment"\n\t\ttag="li"\n\t\tv-bind="comment.props"\n\t\t:auto-complete="autoComplete"\n\t\t:resource-type="resourceType"\n\t\t:message="commentMessage"\n\t\t:resource-id="resourceId"\n\t\t:user-data="genMentionsData(comment.props.mentions)"\n\t\tclass="comments-activity"\n\t\t@delete="reloadCallback()" />\n</template>\n\n<script lang="ts">\nimport { translate as t } from \'@nextcloud/l10n\'\n\nimport Comment from \'../components/Comment.vue\'\nimport CommentView from \'../mixins/CommentView\'\n\nexport default {\n\tname: \'ActivityCommentEntry\',\n\n\tcomponents: {\n\t\tComment,\n\t},\n\n\tmixins: [CommentView],\n\tprops: {\n\t\tcomment: {\n\t\t\ttype: Object,\n\t\t\trequired: true,\n\t\t},\n\t\treloadCallback: {\n\t\t\ttype: Function,\n\t\t\trequired: true,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tcommentMessage: \'\',\n\t\t}\n\t},\n\n\twatch: {\n\t\tcomment() {\n\t\t\tthis.commentMessage = this.comment.props.message\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.commentMessage = this.comment.props.message\n\t},\n\n\tmethods: {\n\t\tt,\n\t},\n}\n<\/script>\n\n<style scoped>\n.comments-activity {\n\tpadding: 0;\n}\n</style>\n'],sourceRoot:""}]);const a=r},25632:(t,e,n)=>{n.d(e,{default:()=>v});var o=n(53334),s=n(65463),m=n(70452);const r={name:"ActivityCommentEntry",components:{Comment:s.A},mixins:[m.A],props:{comment:{type:Object,required:!0},reloadCallback:{type:Function,required:!0}},data:()=>({commentMessage:""}),watch:{comment(){this.commentMessage=this.comment.props.message}},mounted(){this.commentMessage=this.comment.props.message},methods:{t:o.Tl}};var a=n(85072),i=n.n(a),c=n(97825),l=n.n(c),p=n(77659),u=n.n(p),d=n(55056),h=n.n(d),A=n(10540),f=n.n(A),g=n(41113),y=n.n(g),C=n(67695),b={};b.styleTagTransform=y(),b.setAttributes=h(),b.insert=u().bind(null,"head"),b.domAPI=l(),b.insertStyleElement=f(),i()(C.A,b),C.A&&C.A.locals&&C.A.locals;const v=(0,n(14486).A)(r,(function(){var t=this;return(0,t._self._c)("Comment",t._b({ref:"comment",staticClass:"comments-activity",attrs:{tag:"li","auto-complete":t.autoComplete,"resource-type":t.resourceType,message:t.commentMessage,"resource-id":t.resourceId,"user-data":t.genMentionsData(t.comment.props.mentions)},on:{delete:function(e){return t.reloadCallback()}}},"Comment",t.comment.props,!1))}),[],!1,null,"188dae4a",null).exports}}]);
|
||||
//# sourceMappingURL=5632-5632.js.map?v=f16542372833977f05d1
|
||||
File diff suppressed because one or more lines are too long
3
dist/5935-5935.js
vendored
3
dist/5935-5935.js
vendored
File diff suppressed because one or more lines are too long
9
dist/5935-5935.js.LICENSE.txt
vendored
9
dist/5935-5935.js.LICENSE.txt
vendored
|
|
@ -1,9 +0,0 @@
|
|||
//! authors : Tim Wood, Iskren Chernev, Moment.js contributors
|
||||
|
||||
//! license : MIT
|
||||
|
||||
//! moment.js
|
||||
|
||||
//! momentjs.com
|
||||
|
||||
//! version : 2.29.4
|
||||
1
dist/5935-5935.js.map
vendored
1
dist/5935-5935.js.map
vendored
File diff suppressed because one or more lines are too long
3
dist/5951-5951.js
vendored
3
dist/5951-5951.js
vendored
File diff suppressed because one or more lines are too long
1
dist/5951-5951.js.map
vendored
1
dist/5951-5951.js.map
vendored
File diff suppressed because one or more lines are too long
2
dist/614-614.js
vendored
2
dist/614-614.js
vendored
File diff suppressed because one or more lines are too long
1
dist/614-614.js.map
vendored
1
dist/614-614.js.map
vendored
File diff suppressed because one or more lines are too long
3
dist/6826-6826.js
vendored
3
dist/6826-6826.js
vendored
File diff suppressed because one or more lines are too long
1
dist/6826-6826.js.map
vendored
1
dist/6826-6826.js.map
vendored
File diff suppressed because one or more lines are too long
3
dist/7462-7462.js
vendored
Normal file
3
dist/7462-7462.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/7462-7462.js.map
vendored
Normal file
1
dist/7462-7462.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
3
dist/7816-7816.js
vendored
3
dist/7816-7816.js
vendored
File diff suppressed because one or more lines are too long
1
dist/7816-7816.js.map
vendored
1
dist/7816-7816.js.map
vendored
File diff suppressed because one or more lines are too long
3
dist/7883-7883.js
vendored
Normal file
3
dist/7883-7883.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/7883-7883.js.map
vendored
Normal file
1
dist/7883-7883.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
3
dist/8618-8618.js
vendored
Normal file
3
dist/8618-8618.js
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
/*! For license information please see 8618-8618.js.LICENSE.txt */
|
||||
"use strict";(self.webpackChunknextcloud=self.webpackChunknextcloud||[]).push([[8618],{68618:(e,c,l)=>{l.d(c,{FilePickerVue:()=>n});const n=(0,l(85471).$V)((()=>Promise.all([l.e(4208),l.e(1359)]).then(l.bind(l,61421))))}}]);
|
||||
//# sourceMappingURL=8618-8618.js.map?v=1e8f15db3b14455fef8f
|
||||
1
dist/8618-8618.js.map
vendored
Normal file
1
dist/8618-8618.js.map
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"8618-8618.js?v=1e8f15db3b14455fef8f","mappings":";oIAsBA,MAAMA,GAAI,gBAAE,IAAM","sources":["webpack:///nextcloud/node_modules/@nextcloud/dialogs/dist/chunks/index-RkOaxczZ.mjs"],"sourcesContent":["import { defineAsyncComponent as e } from \"vue\";\n/**\n * @copyright Copyright (c) 2023 Ferdinand Thiessen <opensource@fthiessen.de>\n *\n * @author Ferdinand Thiessen <opensource@fthiessen.de>\n *\n * @license AGPL-3.0-or-later\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <http://www.gnu.org/licenses/>.\n *\n */\nconst i = e(() => import(\"./FilePicker-DBGB1Rec.mjs\"));\nexport {\n i as FilePickerVue\n};\n"],"names":["i"],"sourceRoot":""}
|
||||
3
dist/9845-9845.js
vendored
3
dist/9845-9845.js
vendored
File diff suppressed because one or more lines are too long
1
dist/9845-9845.js.map
vendored
1
dist/9845-9845.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/comments-comments-app.js
vendored
4
dist/comments-comments-app.js
vendored
File diff suppressed because one or more lines are too long
2
dist/comments-comments-app.js.map
vendored
2
dist/comments-comments-app.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/comments-comments-tab.js
vendored
4
dist/comments-comments-tab.js
vendored
File diff suppressed because one or more lines are too long
2
dist/comments-comments-tab.js.map
vendored
2
dist/comments-comments-tab.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/comments-comments.js
vendored
4
dist/comments-comments.js
vendored
|
|
@ -1,3 +1,3 @@
|
|||
/*! For license information please see comments-comments.js.LICENSE.txt */
|
||||
(()=>{var e={4921:()=>{OCA.Comments.ActivityTabViewPlugin={prepareModelForDisplay(e,t,n){if("comments"===e.get("app")&&"comments"===e.get("type")&&"ActivityTabView"===n&&(t.addClass("comment"),e.get("message")&&this._isLong(e.get("message")))){t.addClass("collapsed");const e=$("<div>").addClass("message-overlay");t.find(".activitymessage").after(e),t.on("click",this._onClickCollapsedComment)}},_onClickCollapsedComment(e){let t=$(e.target);t.is(".comment")||(t=t.closest(".comment")),t.removeClass("collapsed")},_isLong:e=>e.length>250||(e.match(/\n/g)||[]).length>1},OC.Plugins.register("OCA.Activity.RenderingPlugins",OCA.Comments.ActivityTabViewPlugin)},75387:()=>{OCA.Comments||(OCA.Comments={})},4543:()=>{var e;e=Handlebars.template,(OCA.Comments.Templates=OCA.Comments.Templates||{}).filesplugin=e({compiler:[8,">= 4.3.0"],main:function(e,t,n,s,o){var l,a=null!=t?t:e.nullContext||{},i=e.hooks.helperMissing,r="function",m=e.escapeExpression,c=e.lookupProperty||function(e,t){if(Object.prototype.hasOwnProperty.call(e,t))return e[t]};return'<a class="action action-comment permanent" title="'+m(typeof(l=null!=(l=c(n,"countMessage")||(null!=t?c(t,"countMessage"):t))?l:i)===r?l.call(a,{name:"countMessage",hash:{},data:o,loc:{start:{line:1,column:50},end:{line:1,column:66}}}):l)+'" href="#">\n\t<img class="svg" src="'+m(typeof(l=null!=(l=c(n,"iconUrl")||(null!=t?c(t,"iconUrl"):t))?l:i)===r?l.call(a,{name:"iconUrl",hash:{},data:o,loc:{start:{line:2,column:23},end:{line:2,column:34}}}):l)+'"/>\n</a>\n'},useData:!0})}},t={};function n(s){var o=t[s];if(void 0!==o)return o.exports;var l=t[s]={exports:{}};return e[s](l,l.exports,n),l.exports}n.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return n.d(t,{a:t}),t},n.d=(e,t)=>{for(var s in t)n.o(t,s)&&!n.o(e,s)&&Object.defineProperty(e,s,{enumerable:!0,get:t[s]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{"use strict";n(75387),n(4543),n(4921),window.OCA.Comments=OCA.Comments})()})();
|
||||
//# sourceMappingURL=comments-comments.js.map?v=939781fd2ba7f3a0ee0e
|
||||
(()=>{var e={24723:()=>{OCA.Comments.ActivityTabViewPlugin={prepareModelForDisplay(e,t,n){if("comments"===e.get("app")&&"comments"===e.get("type")&&"ActivityTabView"===n&&(t.addClass("comment"),e.get("message")&&this._isLong(e.get("message")))){t.addClass("collapsed");const e=$("<div>").addClass("message-overlay");t.find(".activitymessage").after(e),t.on("click",this._onClickCollapsedComment)}},_onClickCollapsedComment(e){let t=$(e.target);t.is(".comment")||(t=t.closest(".comment")),t.removeClass("collapsed")},_isLong:e=>e.length>250||(e.match(/\n/g)||[]).length>1},OC.Plugins.register("OCA.Activity.RenderingPlugins",OCA.Comments.ActivityTabViewPlugin)},32290:()=>{OCA.Comments||(OCA.Comments={})},80094:()=>{var e;e=Handlebars.template,(OCA.Comments.Templates=OCA.Comments.Templates||{}).filesplugin=e({compiler:[8,">= 4.3.0"],main:function(e,t,n,s,o){var l,a=null!=t?t:e.nullContext||{},i=e.hooks.helperMissing,r="function",m=e.escapeExpression,c=e.lookupProperty||function(e,t){if(Object.prototype.hasOwnProperty.call(e,t))return e[t]};return'<a class="action action-comment permanent" title="'+m(typeof(l=null!=(l=c(n,"countMessage")||(null!=t?c(t,"countMessage"):t))?l:i)===r?l.call(a,{name:"countMessage",hash:{},data:o,loc:{start:{line:1,column:50},end:{line:1,column:66}}}):l)+'" href="#">\n\t<img class="svg" src="'+m(typeof(l=null!=(l=c(n,"iconUrl")||(null!=t?c(t,"iconUrl"):t))?l:i)===r?l.call(a,{name:"iconUrl",hash:{},data:o,loc:{start:{line:2,column:23},end:{line:2,column:34}}}):l)+'"/>\n</a>\n'},useData:!0})}},t={};function n(s){var o=t[s];if(void 0!==o)return o.exports;var l=t[s]={exports:{}};return e[s](l,l.exports,n),l.exports}n.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return n.d(t,{a:t}),t},n.d=(e,t)=>{for(var s in t)n.o(t,s)&&!n.o(e,s)&&Object.defineProperty(e,s,{enumerable:!0,get:t[s]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{"use strict";n(32290),n(80094),n(24723),window.OCA.Comments=OCA.Comments})()})();
|
||||
//# sourceMappingURL=comments-comments.js.map?v=0afe3d690a08aa44bf7a
|
||||
2
dist/comments-comments.js.map
vendored
2
dist/comments-comments.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/comments-init.js
vendored
4
dist/comments-init.js
vendored
File diff suppressed because one or more lines are too long
2
dist/comments-init.js.map
vendored
2
dist/comments-init.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-common.js
vendored
4
dist/core-common.js
vendored
File diff suppressed because one or more lines are too long
26
dist/core-common.js.LICENSE.txt
vendored
26
dist/core-common.js.LICENSE.txt
vendored
|
|
@ -68,7 +68,7 @@
|
|||
* @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
/*! @license DOMPurify 3.0.6 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.0.6/LICENSE */
|
||||
/*! @license DOMPurify 3.0.11 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.0.11/LICENSE */
|
||||
|
||||
/*! https://mths.be/punycode v1.4.1 by @mathias */
|
||||
|
||||
|
|
@ -653,6 +653,28 @@
|
|||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2024 Maksim Sukharev <antreesy.web@gmail.com>
|
||||
*
|
||||
* @author Maksim Sukharev <antreesy.web@gmail.com>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
/**!
|
||||
* @fileOverview Kickass library to create and place poppers near their reference elements.
|
||||
* @version 1.16.1
|
||||
|
|
@ -688,4 +710,4 @@
|
|||
|
||||
//! momentjs.com
|
||||
|
||||
//! version : 2.29.4
|
||||
//! version : 2.30.1
|
||||
|
|
|
|||
2
dist/core-common.js.map
vendored
2
dist/core-common.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-files_client.js
vendored
4
dist/core-files_client.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-files_client.js.map
vendored
2
dist/core-files_client.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-install.js
vendored
4
dist/core-install.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-install.js.map
vendored
2
dist/core-install.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-legacy-unified-search.js
vendored
4
dist/core-legacy-unified-search.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-legacy-unified-search.js.map
vendored
2
dist/core-legacy-unified-search.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-login.js
vendored
4
dist/core-login.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-login.js.map
vendored
2
dist/core-login.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-main.js
vendored
4
dist/core-main.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-main.js.map
vendored
2
dist/core-main.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-maintenance.js
vendored
4
dist/core-maintenance.js
vendored
|
|
@ -1,3 +1,3 @@
|
|||
/*! For license information please see core-maintenance.js.LICENSE.txt */
|
||||
(()=>{"use strict";var e,t={49e3:(e,t,o)=>{var n=o(93664),r=o(79753),i=o(25108);const a=(0,r.getRootUrl)()+"/status.php",l=()=>{i.info("checking the Nextcloud maintenance status"),n.Z.get(a).then((e=>e.data)).then((e=>{if(!1===e.maintenance)return i.info("Nextcloud is not in maintenance mode anymore -> reloading"),void window.location.reload();i.info("Nextcloud is still in maintenance mode"),setTimeout(l,2e4)})).catch(i.error.bind(void 0))};l()}},o={};function n(e){var r=o[e];if(void 0!==r)return r.exports;var i=o[e]={id:e,loaded:!1,exports:{}};return t[e].call(i.exports,i,i.exports,n),i.loaded=!0,i.exports}n.m=t,e=[],n.O=(t,o,r,i)=>{if(!o){var a=1/0;for(u=0;u<e.length;u++){o=e[u][0],r=e[u][1],i=e[u][2];for(var l=!0,d=0;d<o.length;d++)(!1&i||a>=i)&&Object.keys(n.O).every((e=>n.O[e](o[d])))?o.splice(d--,1):(l=!1,i<a&&(a=i));if(l){e.splice(u--,1);var c=r();void 0!==c&&(t=c)}}return t}i=i||0;for(var u=e.length;u>0&&e[u-1][2]>i;u--)e[u]=e[u-1];e[u]=[o,r,i]},n.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return n.d(t,{a:t}),t},n.d=(e,t)=>{for(var o in t)n.o(t,o)&&!n.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:t[o]})},n.e=()=>Promise.resolve(),n.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),n.j=1802,(()=>{n.b=document.baseURI||self.location.href;var e={1802:0};n.O.j=t=>0===e[t];var t=(t,o)=>{var r,i,a=o[0],l=o[1],d=o[2],c=0;if(a.some((t=>0!==e[t]))){for(r in l)n.o(l,r)&&(n.m[r]=l[r]);if(d)var u=d(n)}for(t&&t(o);c<a.length;c++)i=a[c],n.o(e,i)&&e[i]&&e[i][0](),e[i]=0;return n.O(u)},o=self.webpackChunknextcloud=self.webpackChunknextcloud||[];o.forEach(t.bind(null,0)),o.push=t.bind(null,o.push.bind(o))})(),n.nc=void 0;var r=n.O(void 0,[7874],(()=>n(49e3)));r=n.O(r)})();
|
||||
//# sourceMappingURL=core-maintenance.js.map?v=8fc2dcd0d3065d8a51ad
|
||||
(()=>{"use strict";var e,t={77748:(e,t,n)=>{var o=n(26287),r=n(99498),i=n(96763);const a=(0,r.aU)()+"/status.php",l=()=>{i.info("checking the Nextcloud maintenance status"),o.A.get(a).then((e=>e.data)).then((e=>{if(!1===e.maintenance)return i.info("Nextcloud is not in maintenance mode anymore -> reloading"),void window.location.reload();i.info("Nextcloud is still in maintenance mode"),setTimeout(l,2e4)})).catch(i.error.bind(void 0))};l()}},n={};function o(e){var r=n[e];if(void 0!==r)return r.exports;var i=n[e]={id:e,loaded:!1,exports:{}};return t[e].call(i.exports,i,i.exports,o),i.loaded=!0,i.exports}o.m=t,e=[],o.O=(t,n,r,i)=>{if(!n){var a=1/0;for(u=0;u<e.length;u++){n=e[u][0],r=e[u][1],i=e[u][2];for(var l=!0,d=0;d<n.length;d++)(!1&i||a>=i)&&Object.keys(o.O).every((e=>o.O[e](n[d])))?n.splice(d--,1):(l=!1,i<a&&(a=i));if(l){e.splice(u--,1);var c=r();void 0!==c&&(t=c)}}return t}i=i||0;for(var u=e.length;u>0&&e[u-1][2]>i;u--)e[u]=e[u-1];e[u]=[n,r,i]},o.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return o.d(t,{a:t}),t},o.d=(e,t)=>{for(var n in t)o.o(t,n)&&!o.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},o.e=()=>Promise.resolve(),o.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),o.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),o.j=2076,(()=>{o.b=document.baseURI||self.location.href;var e={2076:0};o.O.j=t=>0===e[t];var t=(t,n)=>{var r,i,a=n[0],l=n[1],d=n[2],c=0;if(a.some((t=>0!==e[t]))){for(r in l)o.o(l,r)&&(o.m[r]=l[r]);if(d)var u=d(o)}for(t&&t(n);c<a.length;c++)i=a[c],o.o(e,i)&&e[i]&&e[i][0](),e[i]=0;return o.O(u)},n=self.webpackChunknextcloud=self.webpackChunknextcloud||[];n.forEach(t.bind(null,0)),n.push=t.bind(null,n.push.bind(n))})(),o.nc=void 0;var r=o.O(void 0,[4208],(()=>o(77748)));r=o.O(r)})();
|
||||
//# sourceMappingURL=core-maintenance.js.map?v=19735ee2a2994fc2343a
|
||||
2
dist/core-maintenance.js.map
vendored
2
dist/core-maintenance.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-profile.js
vendored
4
dist/core-profile.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-profile.js.map
vendored
2
dist/core-profile.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-recommendedapps.js
vendored
4
dist/core-recommendedapps.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-recommendedapps.js.map
vendored
2
dist/core-recommendedapps.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-systemtags.js
vendored
4
dist/core-systemtags.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-systemtags.js.map
vendored
2
dist/core-systemtags.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-unified-search.js
vendored
4
dist/core-unified-search.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-unified-search.js.map
vendored
2
dist/core-unified-search.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-unsupported-browser-redirect.js
vendored
4
dist/core-unsupported-browser-redirect.js
vendored
|
|
@ -1,3 +1,3 @@
|
|||
/*! For license information please see core-unsupported-browser-redirect.js.LICENSE.txt */
|
||||
(()=>{"use strict";var e,r,t,o={66592:(e,r,t)=>{var o=t(77958);t.nc=btoa((0,o.IH)()),window.TESTING||OC?.config?.no_unsupported_browser_warning||window.addEventListener("DOMContentLoaded",(async function(){const{testSupportedBrowser:e}=await Promise.all([t.e(7874),t.e(7816)]).then(t.bind(t,77816));e()}))}},n={};function a(e){var r=n[e];if(void 0!==r)return r.exports;var t=n[e]={id:e,loaded:!1,exports:{}};return o[e].call(t.exports,t,t.exports,a),t.loaded=!0,t.exports}a.m=o,e=[],a.O=(r,t,o,n)=>{if(!t){var i=1/0;for(u=0;u<e.length;u++){t=e[u][0],o=e[u][1],n=e[u][2];for(var l=!0,c=0;c<t.length;c++)(!1&n||i>=n)&&Object.keys(a.O).every((e=>a.O[e](t[c])))?t.splice(c--,1):(l=!1,n<i&&(i=n));if(l){e.splice(u--,1);var d=o();void 0!==d&&(r=d)}}return r}n=n||0;for(var u=e.length;u>0&&e[u-1][2]>n;u--)e[u]=e[u-1];e[u]=[t,o,n]},a.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return a.d(r,{a:r}),r},a.d=(e,r)=>{for(var t in r)a.o(r,t)&&!a.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},a.f={},a.e=e=>Promise.all(Object.keys(a.f).reduce(((r,t)=>(a.f[t](e,r),r)),[])),a.u=e=>e+"-"+e+".js?v=31ba25cd341e62240289",a.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),a.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r={},t="nextcloud:",a.l=(e,o,n,i)=>{if(r[e])r[e].push(o);else{var l,c;if(void 0!==n)for(var d=document.getElementsByTagName("script"),u=0;u<d.length;u++){var s=d[u];if(s.getAttribute("src")==e||s.getAttribute("data-webpack")==t+n){l=s;break}}l||(c=!0,(l=document.createElement("script")).charset="utf-8",l.timeout=120,a.nc&&l.setAttribute("nonce",a.nc),l.setAttribute("data-webpack",t+n),l.src=e),r[e]=[o];var p=(t,o)=>{l.onerror=l.onload=null,clearTimeout(f);var n=r[e];if(delete r[e],l.parentNode&&l.parentNode.removeChild(l),n&&n.forEach((e=>e(o))),t)return t(o)},f=setTimeout(p.bind(null,void 0,{type:"timeout",target:l}),12e4);l.onerror=p.bind(null,l.onerror),l.onload=p.bind(null,l.onload),c&&document.head.appendChild(l)}},a.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),a.j=8876,(()=>{var e;a.g.importScripts&&(e=a.g.location+"");var r=a.g.document;if(!e&&r&&(r.currentScript&&(e=r.currentScript.src),!e)){var t=r.getElementsByTagName("script");if(t.length)for(var o=t.length-1;o>-1&&!e;)e=t[o--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),a.p=e})(),(()=>{a.b=document.baseURI||self.location.href;var e={8876:0};a.f.j=(r,t)=>{var o=a.o(e,r)?e[r]:void 0;if(0!==o)if(o)t.push(o[2]);else{var n=new Promise(((t,n)=>o=e[r]=[t,n]));t.push(o[2]=n);var i=a.p+a.u(r),l=new Error;a.l(i,(t=>{if(a.o(e,r)&&(0!==(o=e[r])&&(e[r]=void 0),o)){var n=t&&("load"===t.type?"missing":t.type),i=t&&t.target&&t.target.src;l.message="Loading chunk "+r+" failed.\n("+n+": "+i+")",l.name="ChunkLoadError",l.type=n,l.request=i,o[1](l)}}),"chunk-"+r,r)}},a.O.j=r=>0===e[r];var r=(r,t)=>{var o,n,i=t[0],l=t[1],c=t[2],d=0;if(i.some((r=>0!==e[r]))){for(o in l)a.o(l,o)&&(a.m[o]=l[o]);if(c)var u=c(a)}for(r&&r(t);d<i.length;d++)n=i[d],a.o(e,n)&&e[n]&&e[n][0](),e[n]=0;return a.O(u)},t=self.webpackChunknextcloud=self.webpackChunknextcloud||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})(),a.nc=void 0;var i=a.O(void 0,[7874],(()=>a(66592)));i=a.O(i)})();
|
||||
//# sourceMappingURL=core-unsupported-browser-redirect.js.map?v=ffab83193f453bfc4c6d
|
||||
(()=>{"use strict";var e,r,t,o={47210:(e,r,t)=>{var o,n=t(92457);t.nc=btoa((0,n.do)()),window.TESTING||null!==(o=OC)&&void 0!==o&&null!==(o=o.config)&&void 0!==o&&o.no_unsupported_browser_warning||window.addEventListener("DOMContentLoaded",(async function(){const{testSupportedBrowser:e}=await Promise.all([t.e(4208),t.e(7883)]).then(t.bind(t,77883));e()}))}},n={};function a(e){var r=n[e];if(void 0!==r)return r.exports;var t=n[e]={id:e,loaded:!1,exports:{}};return o[e].call(t.exports,t,t.exports,a),t.loaded=!0,t.exports}a.m=o,e=[],a.O=(r,t,o,n)=>{if(!t){var i=1/0;for(u=0;u<e.length;u++){t=e[u][0],o=e[u][1],n=e[u][2];for(var l=!0,d=0;d<t.length;d++)(!1&n||i>=n)&&Object.keys(a.O).every((e=>a.O[e](t[d])))?t.splice(d--,1):(l=!1,n<i&&(i=n));if(l){e.splice(u--,1);var c=o();void 0!==c&&(r=c)}}return r}n=n||0;for(var u=e.length;u>0&&e[u-1][2]>n;u--)e[u]=e[u-1];e[u]=[t,o,n]},a.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return a.d(r,{a:r}),r},a.d=(e,r)=>{for(var t in r)a.o(r,t)&&!a.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},a.f={},a.e=e=>Promise.all(Object.keys(a.f).reduce(((r,t)=>(a.f[t](e,r),r)),[])),a.u=e=>e+"-"+e+".js?v=fc747da304ce9938861a",a.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),a.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r={},t="nextcloud:",a.l=(e,o,n,i)=>{if(r[e])r[e].push(o);else{var l,d;if(void 0!==n)for(var c=document.getElementsByTagName("script"),u=0;u<c.length;u++){var s=c[u];if(s.getAttribute("src")==e||s.getAttribute("data-webpack")==t+n){l=s;break}}l||(d=!0,(l=document.createElement("script")).charset="utf-8",l.timeout=120,a.nc&&l.setAttribute("nonce",a.nc),l.setAttribute("data-webpack",t+n),l.src=e),r[e]=[o];var p=(t,o)=>{l.onerror=l.onload=null,clearTimeout(f);var n=r[e];if(delete r[e],l.parentNode&&l.parentNode.removeChild(l),n&&n.forEach((e=>e(o))),t)return t(o)},f=setTimeout(p.bind(null,void 0,{type:"timeout",target:l}),12e4);l.onerror=p.bind(null,l.onerror),l.onload=p.bind(null,l.onload),d&&document.head.appendChild(l)}},a.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),a.j=3604,(()=>{var e;a.g.importScripts&&(e=a.g.location+"");var r=a.g.document;if(!e&&r&&(r.currentScript&&(e=r.currentScript.src),!e)){var t=r.getElementsByTagName("script");if(t.length)for(var o=t.length-1;o>-1&&(!e||!/^http(s?):/.test(e));)e=t[o--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),a.p=e})(),(()=>{a.b=document.baseURI||self.location.href;var e={3604:0};a.f.j=(r,t)=>{var o=a.o(e,r)?e[r]:void 0;if(0!==o)if(o)t.push(o[2]);else{var n=new Promise(((t,n)=>o=e[r]=[t,n]));t.push(o[2]=n);var i=a.p+a.u(r),l=new Error;a.l(i,(t=>{if(a.o(e,r)&&(0!==(o=e[r])&&(e[r]=void 0),o)){var n=t&&("load"===t.type?"missing":t.type),i=t&&t.target&&t.target.src;l.message="Loading chunk "+r+" failed.\n("+n+": "+i+")",l.name="ChunkLoadError",l.type=n,l.request=i,o[1](l)}}),"chunk-"+r,r)}},a.O.j=r=>0===e[r];var r=(r,t)=>{var o,n,i=t[0],l=t[1],d=t[2],c=0;if(i.some((r=>0!==e[r]))){for(o in l)a.o(l,o)&&(a.m[o]=l[o]);if(d)var u=d(a)}for(r&&r(t);c<i.length;c++)n=i[c],a.o(e,n)&&e[n]&&e[n][0](),e[n]=0;return a.O(u)},t=self.webpackChunknextcloud=self.webpackChunknextcloud||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})(),a.nc=void 0;var i=a.O(void 0,[4208],(()=>a(47210)));i=a.O(i)})();
|
||||
//# sourceMappingURL=core-unsupported-browser-redirect.js.map?v=5d1741d9d349d6bf49e8
|
||||
File diff suppressed because one or more lines are too long
4
dist/core-unsupported-browser.js
vendored
4
dist/core-unsupported-browser.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-unsupported-browser.js.map
vendored
2
dist/core-unsupported-browser.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/dashboard-main.js
vendored
4
dist/dashboard-main.js
vendored
File diff suppressed because one or more lines are too long
2
dist/dashboard-main.js.map
vendored
2
dist/dashboard-main.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/dav-settings-admin-caldav.js
vendored
4
dist/dav-settings-admin-caldav.js
vendored
File diff suppressed because one or more lines are too long
2
dist/dav-settings-admin-caldav.js.map
vendored
2
dist/dav-settings-admin-caldav.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/dav-settings-personal-availability.js
vendored
4
dist/dav-settings-personal-availability.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue