fix(files): always ask for confirmation if trashbin app is disabled

Signed-off-by: skjnldsv <skjnldsv@protonmail.com>

Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>

Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
This commit is contained in:
skjnldsv 2024-07-26 16:04:07 +02:00
parent e61807c953
commit 6df8bdf5df
13 changed files with 377 additions and 135 deletions

View file

@ -0,0 +1,22 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Capabilities } from '../../apps/files/src/types'
export const getCapabilities = (): Capabilities => {
return {
files: {
bigfilechunking: true,
blacklisted_files: [],
forbidden_filename_basenames: [],
forbidden_filename_characters: [],
forbidden_filename_extensions: [],
forbidden_filenames: [],
undelete: true,
version_deletion: true,
version_labeling: true,
versioning: true,
},
}
}

View file

@ -22,8 +22,9 @@
import { action } from './deleteAction'
import { expect } from '@jest/globals'
import { File, Folder, Permission, View, FileAction } from '@nextcloud/files'
import eventBus from '@nextcloud/event-bus'
import * as capabilities from '@nextcloud/capabilities'
import axios from '@nextcloud/axios'
import eventBus from '@nextcloud/event-bus'
import logger from '../logger'
@ -111,6 +112,16 @@ describe('Delete action conditions tests', () => {
expect(action.displayName([file], trashbinView)).toBe('Delete permanently')
})
test('Trashbin disabled displayName', () => {
jest.spyOn(capabilities, 'getCapabilities').mockImplementation(() => {
return {
files: {},
}
})
expect(action.displayName([file], view)).toBe('Delete permanently')
expect(capabilities.getCapabilities).toBeCalledTimes(1)
})
test('Shared root node displayName', () => {
expect(action.displayName([file2], view)).toBe('Leave this share')
expect(action.displayName([folder2], view)).toBe('Leave this share')
@ -181,6 +192,9 @@ describe('Delete action enabled tests', () => {
})
describe('Delete action execute tests', () => {
afterEach(() => {
jest.restoreAllMocks()
})
test('Delete action', async () => {
jest.spyOn(axios, 'delete')
jest.spyOn(eventBus, 'emit')
@ -241,9 +255,123 @@ describe('Delete action execute tests', () => {
expect(eventBus.emit).toHaveBeenNthCalledWith(2, 'files:node:deleted', file2)
})
test('Delete action batch large set', async () => {
jest.spyOn(axios, 'delete')
jest.spyOn(eventBus, 'emit')
// Emulate the confirmation dialog to always confirm
const confirmMock = jest.fn().mockImplementation((a, b, c, resolve) => resolve(true))
window.OC = { dialogs: { confirmDestructive: confirmMock } }
const file1 = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/test/foo.txt',
owner: 'test',
mime: 'text/plain',
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
})
const file2 = new File({
id: 2,
source: 'https://cloud.domain.com/remote.php/dav/files/test/bar.txt',
owner: 'test',
mime: 'text/plain',
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
})
const file3 = new File({
id: 3,
source: 'https://cloud.domain.com/remote.php/dav/files/test/baz.txt',
owner: 'test',
mime: 'text/plain',
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
})
const file4 = new File({
id: 4,
source: 'https://cloud.domain.com/remote.php/dav/files/test/qux.txt',
owner: 'test',
mime: 'text/plain',
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
})
const file5 = new File({
id: 5,
source: 'https://cloud.domain.com/remote.php/dav/files/test/quux.txt',
owner: 'test',
mime: 'text/plain',
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
})
const exec = await action.execBatch!([file1, file2, file3, file4, file5], view, '/')
// Enough nodes to trigger a confirmation dialog
expect(confirmMock).toBeCalledTimes(1)
expect(exec).toStrictEqual([true, true, true, true, true])
expect(axios.delete).toBeCalledTimes(5)
expect(axios.delete).toHaveBeenNthCalledWith(1, 'https://cloud.domain.com/remote.php/dav/files/test/foo.txt')
expect(axios.delete).toHaveBeenNthCalledWith(2, 'https://cloud.domain.com/remote.php/dav/files/test/bar.txt')
expect(axios.delete).toHaveBeenNthCalledWith(3, 'https://cloud.domain.com/remote.php/dav/files/test/baz.txt')
expect(axios.delete).toHaveBeenNthCalledWith(4, 'https://cloud.domain.com/remote.php/dav/files/test/qux.txt')
expect(axios.delete).toHaveBeenNthCalledWith(5, 'https://cloud.domain.com/remote.php/dav/files/test/quux.txt')
expect(eventBus.emit).toBeCalledTimes(5)
expect(eventBus.emit).toHaveBeenNthCalledWith(1, 'files:node:deleted', file1)
expect(eventBus.emit).toHaveBeenNthCalledWith(2, 'files:node:deleted', file2)
expect(eventBus.emit).toHaveBeenNthCalledWith(3, 'files:node:deleted', file3)
expect(eventBus.emit).toHaveBeenNthCalledWith(4, 'files:node:deleted', file4)
expect(eventBus.emit).toHaveBeenNthCalledWith(5, 'files:node:deleted', file5)
})
test('Delete action batch trashbin disabled', async () => {
jest.spyOn(axios, 'delete')
jest.spyOn(eventBus, 'emit')
jest.spyOn(capabilities, 'getCapabilities').mockImplementation(() => {
return {
files: {},
}
})
// Emulate the confirmation dialog to always confirm
const confirmMock = jest.fn().mockImplementation((a, b, c, resolve) => resolve(true))
window.OC = { dialogs: { confirmDestructive: confirmMock } }
const file1 = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/test/foo.txt',
owner: 'test',
mime: 'text/plain',
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
})
const file2 = new File({
id: 2,
source: 'https://cloud.domain.com/remote.php/dav/files/test/bar.txt',
owner: 'test',
mime: 'text/plain',
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
})
const exec = await action.execBatch!([file1, file2], view, '/')
// Will trigger a confirmation dialog because trashbin app is disabled
expect(confirmMock).toBeCalledTimes(1)
expect(exec).toStrictEqual([true, true])
expect(axios.delete).toBeCalledTimes(2)
expect(axios.delete).toHaveBeenNthCalledWith(1, 'https://cloud.domain.com/remote.php/dav/files/test/foo.txt')
expect(axios.delete).toHaveBeenNthCalledWith(2, 'https://cloud.domain.com/remote.php/dav/files/test/bar.txt')
expect(eventBus.emit).toBeCalledTimes(2)
expect(eventBus.emit).toHaveBeenNthCalledWith(1, 'files:node:deleted', file1)
expect(eventBus.emit).toHaveBeenNthCalledWith(2, 'files:node:deleted', file2)
})
test('Delete fails', async () => {
jest.spyOn(axios, 'delete').mockImplementation(() => { throw new Error('Mock error') })
jest.spyOn(logger, 'error').mockImplementation(() => jest.fn())
jest.spyOn(eventBus, 'emit')
const file = new File({
id: 1,
@ -262,4 +390,35 @@ describe('Delete action execute tests', () => {
expect(eventBus.emit).toBeCalledTimes(0)
expect(logger.error).toBeCalledTimes(1)
})
test('Delete is cancelled', async () => {
jest.spyOn(axios, 'delete')
jest.spyOn(eventBus, 'emit')
jest.spyOn(capabilities, 'getCapabilities').mockImplementation(() => {
return {
files: {},
}
})
// Emulate the confirmation dialog to always confirm
const confirmMock = jest.fn().mockImplementation((a, b, c, resolve) => resolve(false))
window.OC = { dialogs: { confirmDestructive: confirmMock } }
const file1 = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/test/foo.txt',
owner: 'test',
mime: 'text/plain',
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
})
const exec = await action.execBatch!([file1], view, '/')
expect(confirmMock).toBeCalledTimes(1)
expect(exec).toStrictEqual([null])
expect(axios.delete).toBeCalledTimes(0)
expect(eventBus.emit).toBeCalledTimes(0)
})
})

View file

@ -19,106 +19,17 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { emit } from '@nextcloud/event-bus'
import { Permission, Node, View, FileAction, FileType } from '@nextcloud/files'
import { Permission, Node, View, FileAction } from '@nextcloud/files'
import { showInfo } from '@nextcloud/dialogs'
import { translate as t } from '@nextcloud/l10n'
import axios from '@nextcloud/axios'
import PQueue from 'p-queue'
import CloseSvg from '@mdi/svg/svg/close.svg?raw'
import NetworkOffSvg from '@mdi/svg/svg/network-off.svg?raw'
import TrashCanSvg from '@mdi/svg/svg/trash-can.svg?raw'
import logger from '../logger.js'
import PQueue from 'p-queue'
const canUnshareOnly = (nodes: Node[]) => {
return nodes.every(node => node.attributes['is-mount-root'] === true
&& node.attributes['mount-type'] === 'shared')
}
const canDisconnectOnly = (nodes: Node[]) => {
return nodes.every(node => node.attributes['is-mount-root'] === true
&& node.attributes['mount-type'] === 'external')
}
const isMixedUnshareAndDelete = (nodes: Node[]) => {
if (nodes.length === 1) {
return false
}
const hasSharedItems = nodes.some(node => canUnshareOnly([node]))
const hasDeleteItems = nodes.some(node => !canUnshareOnly([node]))
return hasSharedItems && hasDeleteItems
}
const isAllFiles = (nodes: Node[]) => {
return !nodes.some(node => node.type !== FileType.File)
}
const isAllFolders = (nodes: Node[]) => {
return !nodes.some(node => node.type !== FileType.Folder)
}
const displayName = (nodes: Node[], view: View) => {
/**
* If we're in the trashbin, we can only delete permanently
*/
if (view.id === 'trashbin') {
return t('files', 'Delete permanently')
}
/**
* If we're in the sharing view, we can only unshare
*/
if (isMixedUnshareAndDelete(nodes)) {
return t('files', 'Delete and unshare')
}
/**
* If those nodes are all the root node of a
* share, we can only unshare them.
*/
if (canUnshareOnly(nodes)) {
if (nodes.length === 1) {
return t('files', 'Leave this share')
}
return t('files', 'Leave these shares')
}
/**
* If those nodes are all the root node of an
* external storage, we can only disconnect it.
*/
if (canDisconnectOnly(nodes)) {
if (nodes.length === 1) {
return t('files', 'Disconnect storage')
}
return t('files', 'Disconnect storages')
}
/**
* If we're only selecting files, use proper wording
*/
if (isAllFiles(nodes)) {
if (nodes.length === 1) {
return t('files', 'Delete file')
}
return t('files', 'Delete files')
}
/**
* If we're only selecting folders, use proper wording
*/
if (isAllFolders(nodes)) {
if (nodes.length === 1) {
return t('files', 'Delete folder')
}
return t('files', 'Delete folders')
}
return t('files', 'Delete')
}
import { askConfirmation, canDisconnectOnly, canUnshareOnly, deleteNode, displayName, isTrashbinEnabled } from './deleteUtils'
const queue = new PQueue({ concurrency: 5 })
@ -143,14 +54,22 @@ export const action = new FileAction({
.every(permission => (permission & Permission.DELETE) !== 0)
},
async exec(node: Node, view: View, dir: string) {
async exec(node: Node, view: View) {
try {
await axios.delete(node.encodedSource)
let confirm = true
// Let's delete even if it's moved to the trashbin
// since it has been removed from the current view
// and changing the view will trigger a reload anyway.
emit('files:node:deleted', node)
// If trashbin is disabled, we need to ask for confirmation
if (!isTrashbinEnabled()) {
confirm = await askConfirmation([node], view)
}
// If the user cancels the deletion, we don't want to do anything
if (confirm === false) {
showInfo(t('files', 'Deletion cancelled'))
return null
}
await deleteNode(node)
return true
} catch (error) {
@ -159,32 +78,20 @@ export const action = new FileAction({
}
},
async execBatch(nodes: Node[], view: View, dir: string): Promise<(boolean | null)[]> {
const confirm = await new Promise<boolean>(resolve => {
if (nodes.length >= 5 && !canUnshareOnly(nodes) && !canDisconnectOnly(nodes)) {
// TODO use a proper dialog from @nextcloud/dialogs when available
window.OC.dialogs.confirmDestructive(
t('files', 'You are about to delete {count} items.', { count: nodes.length }),
t('files', 'Confirm deletion'),
{
type: window.OC.dialogs.YES_NO_BUTTONS,
confirm: displayName(nodes, view),
confirmClasses: 'error',
cancel: t('files', 'Cancel'),
},
(decision: boolean) => {
resolve(decision)
},
)
return
}
resolve(true)
})
async execBatch(nodes: Node[], view: View): Promise<(boolean | null)[]> {
let confirm = true
// If trashbin is disabled, we need to ask for confirmation
if (!isTrashbinEnabled()) {
confirm = await askConfirmation(nodes, view)
} else if (nodes.length >= 5 && !canUnshareOnly(nodes) && !canDisconnectOnly(nodes)) {
confirm = await askConfirmation(nodes, view)
}
// If the user cancels the deletion, we don't want to do anything
if (confirm === false) {
showInfo(t('files', 'Deletion cancelled'))
return Promise.all(nodes.map(() => false))
return Promise.all(nodes.map(() => null))
}
// Map each node to a promise that resolves with the result of exec(node)
@ -192,8 +99,13 @@ export const action = new FileAction({
// Create a promise that resolves with the result of exec(node)
const promise = new Promise<boolean>(resolve => {
queue.add(async () => {
const result = await this.exec(node, view, dir)
resolve(result !== null ? result : false)
try {
await deleteNode(node)
resolve(true)
} catch (error) {
logger.error('Error while deleting a file', { error, source: node.source, node })
resolve(false)
}
})
})
return promise

View file

@ -0,0 +1,134 @@
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Capabilities } from '../types'
import type { Node, View } from '@nextcloud/files'
import { emit } from '@nextcloud/event-bus'
import { FileType } from '@nextcloud/files'
import { getCapabilities } from '@nextcloud/capabilities'
import { n, t } from '@nextcloud/l10n'
import axios from '@nextcloud/axios'
export const isTrashbinEnabled = () => (getCapabilities() as Capabilities)?.files?.undelete === true
export const canUnshareOnly = (nodes: Node[]) => {
return nodes.every(node => node.attributes['is-mount-root'] === true
&& node.attributes['mount-type'] === 'shared')
}
export const canDisconnectOnly = (nodes: Node[]) => {
return nodes.every(node => node.attributes['is-mount-root'] === true
&& node.attributes['mount-type'] === 'external')
}
export const isMixedUnshareAndDelete = (nodes: Node[]) => {
if (nodes.length === 1) {
return false
}
const hasSharedItems = nodes.some(node => canUnshareOnly([node]))
const hasDeleteItems = nodes.some(node => !canUnshareOnly([node]))
return hasSharedItems && hasDeleteItems
}
export const isAllFiles = (nodes: Node[]) => {
return !nodes.some(node => node.type !== FileType.File)
}
export const isAllFolders = (nodes: Node[]) => {
return !nodes.some(node => node.type !== FileType.Folder)
}
export const displayName = (nodes: Node[], view: View) => {
/**
* If we're in the trashbin, we can only delete permanently
*/
if (view.id === 'trashbin' || !isTrashbinEnabled()) {
return t('files', 'Delete permanently')
}
/**
* If we're in the sharing view, we can only unshare
*/
if (isMixedUnshareAndDelete(nodes)) {
return t('files', 'Delete and unshare')
}
/**
* If those nodes are all the root node of a
* share, we can only unshare them.
*/
if (canUnshareOnly(nodes)) {
if (nodes.length === 1) {
return t('files', 'Leave this share')
}
return t('files', 'Leave these shares')
}
/**
* If those nodes are all the root node of an
* external storage, we can only disconnect it.
*/
if (canDisconnectOnly(nodes)) {
if (nodes.length === 1) {
return t('files', 'Disconnect storage')
}
return t('files', 'Disconnect storages')
}
/**
* If we're only selecting files, use proper wording
*/
if (isAllFiles(nodes)) {
if (nodes.length === 1) {
return t('files', 'Delete file')
}
return t('files', 'Delete files')
}
/**
* If we're only selecting folders, use proper wording
*/
if (isAllFolders(nodes)) {
if (nodes.length === 1) {
return t('files', 'Delete folder')
}
return t('files', 'Delete folders')
}
return t('files', 'Delete')
}
export const askConfirmation = async (nodes: Node[], view: View) => {
const message = view.id === 'trashbin' || !isTrashbinEnabled()
? n('files', 'You are about to permanently delete {count} item', 'You are about to permanently delete {count} items', nodes.length, { count: nodes.length })
: n('files', 'You are about to delete {count} item', 'You are about to delete {count} items', nodes.length, { count: nodes.length })
return new Promise<boolean>(resolve => {
// TODO: Use the new dialog API
window.OC.dialogs.confirmDestructive(
message,
t('files', 'Confirm deletion'),
{
type: window.OC.dialogs.YES_NO_BUTTONS,
confirm: displayName(nodes, view),
confirmClasses: 'error',
cancel: t('files', 'Cancel'),
},
(decision: boolean) => {
resolve(decision)
},
)
})
}
export const deleteNode = async (node: Node) => {
await axios.delete(node.encodedSource)
// Let's delete even if it's moved to the trashbin
// since it has been removed from the current view
// and changing the view will trigger a reload anyway.
emit('files:node:deleted', node)
}

View file

@ -122,3 +122,18 @@ export interface TemplateFile {
ratio?: number
templates?: Record<string, unknown>[]
}
export type Capabilities = {
files: {
bigfilechunking: boolean
blacklisted_files: string[]
forbidden_filename_basenames: string[]
forbidden_filename_characters: string[]
forbidden_filename_extensions: string[]
forbidden_filenames: string[]
undelete: boolean
version_deletion: boolean
version_labeling: boolean
versioning: boolean
}
}

4
dist/7883-7883.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-unsupported-browser-redirect.js.LICENSE.txt */
(()=>{"use strict";var e,r,t,o={47210:(e,r,t)=>{var o,n=t(21777);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=48d5e80ef4bd4f20f511",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=4be2113a1f4861999ea7
(()=>{"use strict";var e,r,t,o={47210:(e,r,t)=>{var o,n=t(21777);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=b699c56d35999b013ff6",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=9745be31c7866e850ea8

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/files-init.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long