Merge pull request #44652 from nextcloud/backport/44409/stable28

This commit is contained in:
John Molakvoæ 2024-04-04 17:59:00 +02:00 committed by GitHub
commit 233e8639cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
196 changed files with 4617 additions and 2726 deletions

1
.gitignore vendored
View file

@ -168,5 +168,6 @@ composer.phar
core/js/mimetypelist.js
# Tests - cypress
cypress/downloads
cypress/snapshots
cypress/videos

View 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,
)
}

View file

@ -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')

View file

@ -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,
},
})

View file

@ -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()
}

View file

@ -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'))
}

View 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)
})
})

View 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

View file

@ -30,6 +30,7 @@ export default defineConfig({
experimentalInteractiveRunEvents: true,
// faster video processing
video: !process.env.CI,
videoCompression: false,
// Visual regression testing

View file

@ -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

File diff suppressed because one or more lines are too long

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
View file

@ -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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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
View 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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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
View file

@ -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
View file

@ -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

File diff suppressed because one or more lines are too long

1
dist/448-448.js.map vendored

File diff suppressed because one or more lines are too long

View file

@ -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)}}]);

View file

@ -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

File diff suppressed because one or more lines are too long

View file

@ -1,9 +0,0 @@
//! authors : Tim Wood, Iskren Chernev, Moment.js contributors
//! license : MIT
//! moment.js
//! momentjs.com
//! version : 2.29.4

File diff suppressed because one or more lines are too long

3
dist/5951-5951.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
dist/614-614.js vendored

File diff suppressed because one or more lines are too long

1
dist/614-614.js.map vendored

File diff suppressed because one or more lines are too long

3
dist/6826-6826.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

3
dist/7816-7816.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

3
dist/8618-8618.js vendored Normal file
View 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
View 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

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -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

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

4
dist/core-common.js vendored

File diff suppressed because one or more lines are too long

View file

@ -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

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

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

4
dist/core-login.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/core-main.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -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

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

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

File diff suppressed because one or more lines are too long

View file

@ -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

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

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

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