Merge pull request #51015 from nextcloud/backport/50979/stable31

[stable31] feat(files): allow to ignore warning to change file type
This commit is contained in:
Ferdinand Thiessen 2025-03-04 21:49:44 +01:00 committed by GitHub
commit f7fc17c2c9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 581 additions and 179 deletions

View file

@ -18,6 +18,12 @@ class UserConfig {
'default' => true,
'allowed' => [true, false],
],
[
// Whether to show the "confirm file extension change" warning
'key' => 'show_dialog_file_extension',
'default' => true,
'allowed' => [true, false],
],
[
// Whether to show the hidden files or not in the files list
'key' => 'show_hidden',

View file

@ -23,7 +23,6 @@
<component :is="linkTo.is"
v-else
ref="basename"
:aria-hidden="isRenaming"
class="files-list__row-name-link"
data-cy-files-list-row-name-link
v-bind="linkTo.params">
@ -117,11 +116,11 @@ export default defineComponent({
return this.isRenaming && this.filesListWidth < 512
},
newName: {
get() {
return this.renamingStore.newName
get(): string {
return this.renamingStore.newNodeName
},
set(newName) {
this.renamingStore.newName = newName
set(newName: string) {
this.renamingStore.newNodeName = newName
},
},
@ -249,7 +248,9 @@ export default defineComponent({
try {
const status = await this.renamingStore.rename()
if (status) {
showSuccess(t('files', 'Renamed "{oldName}" to "{newName}"', { oldName, newName }))
showSuccess(
t('files', 'Renamed "{oldName}" to "{newName}"', { oldName, newName: this.source.basename }),
)
this.$nextTick(() => {
const nameContainer = this.$refs.basename as HTMLElement | undefined
nameContainer?.focus()

View file

@ -18,6 +18,7 @@ declare module '@nextcloud/event-bus' {
'files:node:created': Node
'files:node:deleted': Node
'files:node:updated': Node
'files:node:rename': Node
'files:node:renamed': Node
'files:node:moved': { node: Node, oldSource: string }

View file

@ -3,184 +3,165 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Node } from '@nextcloud/files'
import type { RenamingStore } from '../types'
import axios, { isAxiosError } from '@nextcloud/axios'
import { emit, subscribe } from '@nextcloud/event-bus'
import { FileType, NodeStatus } from '@nextcloud/files'
import { DialogBuilder } from '@nextcloud/dialogs'
import { t } from '@nextcloud/l10n'
import { spawnDialog } from '@nextcloud/vue/dist/Functions/dialog.js'
import { basename, dirname, extname } from 'path'
import { defineStore } from 'pinia'
import logger from '../logger'
import Vue from 'vue'
import IconCancel from '@mdi/svg/svg/cancel.svg?raw'
import IconCheck from '@mdi/svg/svg/check.svg?raw'
import Vue, { defineAsyncComponent, ref } from 'vue'
import { useUserConfigStore } from './userconfig'
let isDialogVisible = false
export const useRenamingStore = defineStore('renaming', () => {
/**
* The currently renamed node
*/
const renamingNode = ref<Node>()
/**
* The new name of the currently renamed node
*/
const newNodeName = ref('')
const showWarningDialog = (oldExtension: string, newExtension: string): Promise<boolean> => {
if (isDialogVisible) {
return Promise.resolve(false)
}
/**
* Internal flag to only allow calling `rename` once.
*/
const isRenaming = ref(false)
isDialogVisible = true
/**
* Execute the renaming.
* This will rename the node set as `renamingNode` to the configured new name `newName`.
*
* @return true if success, false if skipped (e.g. new and old name are the same)
* @throws Error if renaming fails, details are set in the error message
*/
async function rename(): Promise<boolean> {
if (renamingNode.value === undefined) {
throw new Error('No node is currently being renamed')
}
let message
// Only rename once so we use this as some kind of mutex
if (isRenaming.value) {
return false
}
isRenaming.value = true
if (!oldExtension && newExtension) {
message = t(
'files',
'Adding the file extension "{new}" may render the file unreadable.',
{ new: newExtension },
)
} else if (!newExtension) {
message = t(
'files',
'Removing the file extension "{old}" may render the file unreadable.',
{ old: oldExtension },
)
} else {
message = t(
'files',
'Changing the file extension from "{old}" to "{new}" may render the file unreadable.',
{ old: oldExtension, new: newExtension },
)
}
const node = renamingNode.value
Vue.set(node, 'status', NodeStatus.LOADING)
return new Promise((resolve) => {
const dialog = new DialogBuilder()
.setName(t('files', 'Change file extension'))
.setText(message)
.setButtons([
{
label: t('files', 'Keep {oldextension}', { oldextension: oldExtension }),
icon: IconCancel,
type: 'secondary',
callback: () => {
isDialogVisible = false
resolve(false)
},
const userConfig = useUserConfigStore()
let newName = newNodeName.value.trim()
const oldName = node.basename
const oldExtension = extname(oldName)
const newExtension = extname(newName)
// Check for extension change for files
if (node.type === FileType.File
&& oldExtension !== newExtension
&& userConfig.userConfig.show_dialog_file_extension
&& !(await showFileExtensionDialog(oldExtension, newExtension))
) {
// user selected to use the old extension
newName = basename(newName, newExtension) + oldExtension
}
const oldEncodedSource = node.encodedSource
try {
if (oldName === newName) {
return false
}
// rename the node
node.rename(newName)
logger.debug('Moving file to', { destination: node.encodedSource, oldEncodedSource })
// create MOVE request
await axios({
method: 'MOVE',
url: oldEncodedSource,
headers: {
Destination: node.encodedSource,
Overwrite: 'F',
},
{
label: newExtension.length ? t('files', 'Use {newextension}', { newextension: newExtension }) : t('files', 'Remove extension'),
icon: IconCheck,
type: 'primary',
callback: () => {
isDialogVisible = false
resolve(true)
},
},
])
.build()
})
dialog.show().then(() => {
dialog.hide()
})
})
}
// Success 🎉
emit('files:node:updated', node)
emit('files:node:renamed', node)
emit('files:node:moved', {
node,
oldSource: `${dirname(node.source)}/${oldName}`,
})
export const useRenamingStore = function(...args) {
const store = defineStore('renaming', {
state: () => ({
renamingNode: undefined,
newName: '',
} as RenamingStore),
// Reset the state not changed
if (renamingNode.value === node) {
$reset()
}
actions: {
/**
* Execute the renaming.
* This will rename the node set as `renamingNode` to the configured new name `newName`.
* @return true if success, false if skipped (e.g. new and old name are the same)
* @throws Error if renaming fails, details are set in the error message
*/
async rename(): Promise<boolean> {
if (this.renamingNode === undefined) {
throw new Error('No node is currently being renamed')
}
const newName = this.newName.trim?.() || ''
const oldName = this.renamingNode.basename
const oldEncodedSource = this.renamingNode.encodedSource
// Check for extension change for files
const oldExtension = extname(oldName)
const newExtension = extname(newName)
if (oldExtension !== newExtension && this.renamingNode.type === FileType.File) {
const proceed = await showWarningDialog(oldExtension, newExtension)
if (!proceed) {
return false
}
}
if (oldName === newName) {
return false
}
const node = this.renamingNode
Vue.set(node, 'status', NodeStatus.LOADING)
try {
// rename the node
this.renamingNode.rename(newName)
logger.debug('Moving file to', { destination: this.renamingNode.encodedSource, oldEncodedSource })
// create MOVE request
await axios({
method: 'MOVE',
url: oldEncodedSource,
headers: {
Destination: this.renamingNode.encodedSource,
Overwrite: 'F',
return true
} catch (error) {
logger.error('Error while renaming file', { error })
// Rename back as it failed
node.rename(oldName)
if (isAxiosError(error)) {
// TODO: 409 means current folder does not exist, redirect ?
if (error?.response?.status === 404) {
throw new Error(t('files', 'Could not rename "{oldName}", it does not exist any more', { oldName }))
} else if (error?.response?.status === 412) {
throw new Error(t(
'files',
'The name "{newName}" is already used in the folder "{dir}". Please choose a different name.',
{
newName,
dir: basename(renamingNode.value!.dirname),
},
})
// Success 🎉
emit('files:node:updated', this.renamingNode as Node)
emit('files:node:renamed', this.renamingNode as Node)
emit('files:node:moved', {
node: this.renamingNode as Node,
oldSource: `${dirname(this.renamingNode.source)}/${oldName}`,
})
this.$reset()
return true
} catch (error) {
logger.error('Error while renaming file', { error })
// Rename back as it failed
this.renamingNode.rename(oldName)
if (isAxiosError(error)) {
// TODO: 409 means current folder does not exist, redirect ?
if (error?.response?.status === 404) {
throw new Error(t('files', 'Could not rename "{oldName}", it does not exist any more', { oldName }))
} else if (error?.response?.status === 412) {
throw new Error(t(
'files',
'The name "{newName}" is already used in the folder "{dir}". Please choose a different name.',
{
newName,
dir: basename(this.renamingNode.dirname),
},
))
}
}
// Unknown error
throw new Error(t('files', 'Could not rename "{oldName}"', { oldName }))
} finally {
Vue.set(node, 'status', undefined)
))
}
},
},
})
}
// Unknown error
throw new Error(t('files', 'Could not rename "{oldName}"', { oldName }))
} finally {
Vue.set(node, 'status', undefined)
isRenaming.value = false
}
}
const renamingStore = store(...args)
/**
* Reset the store state
*/
function $reset(): void {
newNodeName.value = ''
renamingNode.value = undefined
}
// Make sure we only register the listeners once
if (!renamingStore._initialized) {
subscribe('files:node:rename', function(node: Node) {
renamingStore.renamingNode = node
renamingStore.newName = node.basename
})
renamingStore._initialized = true
}
subscribe('files:node:rename', (node: Node) => {
renamingNode.value = node
newNodeName.value = node.basename
})
return renamingStore
return {
$reset,
newNodeName,
rename,
renamingNode,
}
})
/**
* Show a dialog asking user for confirmation about changing the file extension.
*
* @param oldExtension the old file name extension
* @param newExtension the new file name extension
*/
async function showFileExtensionDialog(oldExtension: string, newExtension: string): Promise<boolean> {
const { promise, resolve } = Promise.withResolvers<boolean>()
spawnDialog(
defineAsyncComponent(() => import('../views/DialogConfirmFileExtension.vue')),
{ oldExtension, newExtension },
(useNewExtension: unknown) => resolve(Boolean(useNewExtension)),
)
return await promise
}

View file

@ -17,6 +17,8 @@ const initialUserConfig = loadState<UserConfig>('files', 'config', {
sort_favorites_first: true,
sort_folders_first: true,
grid_view: false,
show_dialog_file_extension: true,
})
export const useUserConfigStore = defineStore('userconfig', () => {

View file

@ -52,6 +52,7 @@ export interface PathOptions {
export interface UserConfig {
[key: string]: boolean|undefined
show_dialog_file_extension: boolean,
show_hidden: boolean
crop_image_previews: boolean
sort_favorites_first: boolean

View file

@ -0,0 +1,161 @@
/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { createTestingPinia } from '@pinia/testing'
import DialogConfirmFileExtension from './DialogConfirmFileExtension.vue'
import { useUserConfigStore } from '../store/userconfig'
describe('DialogConfirmFileExtension', () => {
it('renders with both extensions', () => {
cy.mount(DialogConfirmFileExtension, {
propsData: {
oldExtension: '.old',
newExtension: '.new',
},
global: {
plugins: [createTestingPinia({
createSpy: cy.spy,
})],
},
})
cy.findByRole('dialog')
.as('dialog')
.should('be.visible')
cy.get('@dialog')
.findByRole('heading')
.should('contain.text', 'Change file extension')
cy.get('@dialog')
.findByRole('checkbox', { name: /Do not show this dialog again/i })
.should('exist')
.and('not.be.checked')
cy.get('@dialog')
.findByRole('button', { name: 'Keep .old' })
.should('be.visible')
cy.get('@dialog')
.findByRole('button', { name: 'Use .new' })
.should('be.visible')
})
it('renders without old extension', () => {
cy.mount(DialogConfirmFileExtension, {
propsData: {
newExtension: '.new',
},
global: {
plugins: [createTestingPinia({
createSpy: cy.spy,
})],
},
})
cy.findByRole('dialog')
.as('dialog')
.should('be.visible')
cy.get('@dialog')
.findByRole('button', { name: 'Keep without extension' })
.should('be.visible')
cy.get('@dialog')
.findByRole('button', { name: 'Use .new' })
.should('be.visible')
})
it('renders without new extension', () => {
cy.mount(DialogConfirmFileExtension, {
propsData: {
oldExtension: '.old',
},
global: {
plugins: [createTestingPinia({
createSpy: cy.spy,
})],
},
})
cy.findByRole('dialog')
.as('dialog')
.should('be.visible')
cy.get('@dialog')
.findByRole('button', { name: 'Keep .old' })
.should('be.visible')
cy.get('@dialog')
.findByRole('button', { name: 'Remove extension' })
.should('be.visible')
})
it('emits correct value on keep old', () => {
cy.mount(DialogConfirmFileExtension, {
propsData: {
oldExtension: '.old',
newExtension: '.new',
},
global: {
plugins: [createTestingPinia({
createSpy: cy.spy,
})],
},
}).as('component')
cy.findByRole('dialog')
.as('dialog')
.should('be.visible')
cy.get('@dialog')
.findByRole('button', { name: 'Keep .old' })
.click()
cy.get('@component')
.its('wrapper')
.should((wrapper) => expect(wrapper.emitted('close')).to.eql([[false]]))
})
it('emits correct value on use new', () => {
cy.mount(DialogConfirmFileExtension, {
propsData: {
oldExtension: '.old',
newExtension: '.new',
},
global: {
plugins: [createTestingPinia({
createSpy: cy.spy,
})],
},
}).as('component')
cy.findByRole('dialog')
.as('dialog')
.should('be.visible')
cy.get('@dialog')
.findByRole('button', { name: 'Use .new' })
.click()
cy.get('@component')
.its('wrapper')
.should((wrapper) => expect(wrapper.emitted('close')).to.eql([[true]]))
})
it('updates user config when checking the checkbox', () => {
const pinia = createTestingPinia({
createSpy: cy.spy,
})
cy.mount(DialogConfirmFileExtension, {
propsData: {
oldExtension: '.old',
newExtension: '.new',
},
global: {
plugins: [pinia],
},
}).as('component')
cy.findByRole('dialog')
.as('dialog')
.should('be.visible')
cy.get('@dialog')
.findByRole('checkbox', { name: /Do not show this dialog again/i })
.check({ force: true })
cy.wrap(useUserConfigStore())
.its('update')
.should('have.been.calledWith', 'show_dialog_file_extension', false)
})
})

View file

@ -0,0 +1,92 @@
<!--
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<script setup lang="ts">
import type { IDialogButton } from '@nextcloud/dialogs'
import { t } from '@nextcloud/l10n'
import { computed, ref } from 'vue'
import { useUserConfigStore } from '../store/userconfig.ts'
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js'
import svgIconCancel from '@mdi/svg/svg/cancel.svg?raw'
import svgIconCheck from '@mdi/svg/svg/check.svg?raw'
const props = defineProps<{
oldExtension?: string
newExtension?: string
}>()
const emit = defineEmits<{
(e: 'close', v: boolean): void
}>()
const userConfigStore = useUserConfigStore()
const dontShowAgain = computed({
get: () => !userConfigStore.userConfig.show_dialog_file_extension,
set: (value: boolean) => userConfigStore.update('show_dialog_file_extension', !value),
})
const buttons = computed<IDialogButton[]>(() => [
{
label: props.oldExtension
? t('files', 'Keep {old}', { old: props.oldExtension })
: t('files', 'Keep without extension'),
icon: svgIconCancel,
type: 'secondary',
callback: () => closeDialog(false),
},
{
label: props.newExtension
? t('files', 'Use {new}', { new: props.newExtension })
: t('files', 'Remove extension'),
icon: svgIconCheck,
type: 'primary',
callback: () => closeDialog(true),
},
])
/** Open state of the dialog */
const open = ref(true)
/**
* Close the dialog and emit the response
* @param value User selected response
*/
function closeDialog(value: boolean) {
emit('close', value)
open.value = false
}
</script>
<template>
<NcDialog :buttons="buttons"
:open="open"
:can-close="false"
:name="t('files', 'Change file extension')"
size="small">
<p v-if="newExtension && oldExtension">
{{ t('files', 'Changing the file extension from "{old}" to "{new}" may render the file unreadable.', { old: oldExtension, new: newExtension }) }}
</p>
<p v-else-if="oldExtension">
{{ t('files', 'Removing the file extension "{old}" may render the file unreadable.', { old: oldExtension }) }}
</p>
<p v-else-if="newExtension">
{{ t('files', 'Adding the file extension "{new}" may render the file unreadable.', { new: newExtension }) }}
</p>
<NcCheckboxRadioSwitch v-model="dontShowAgain"
class="dialog-confirm-file-extension__checkbox"
type="checkbox">
{{ t('files', 'Do not show this dialog again.') }}
</NcCheckboxRadioSwitch>
</NcDialog>
</template>
<style scoped>
.dialog-confirm-file-extension__checkbox {
margin-top: 1rem;
}
</style>

View file

@ -83,6 +83,15 @@
</em>
</NcAppSettingsSection>
<NcAppSettingsSection id="warning" :name="t('files', 'Warnings')">
<em>{{ t('files', 'Prevent warning dialogs from open or reenable them.') }}</em>
<NcCheckboxRadioSwitch type="switch"
:checked="userConfig.show_dialog_file_extension"
@update:checked="setConfig('show_dialog_file_extension', $event)">
{{ t('files', 'Show a warning dialog when changing a file extension.') }}
</NcCheckboxRadioSwitch>
</NcAppSettingsSection>
<NcAppSettingsSection id="shortcuts"
:name="t('files', 'Keyboard shortcuts')">
<em>{{ t('files', 'Speed up your Files experience with these quick shortcuts.') }}</em>

View file

@ -74,7 +74,7 @@ describe('files: Rename nodes', { testIsolation: true }, () => {
})
it('shows accessible loading information', () => {
const { resolve, promise } = Promise.withResolvers()
const { resolve, promise } = Promise.withResolvers<void>()
getRowForFile('file.txt').should('be.visible')
@ -106,7 +106,7 @@ describe('files: Rename nodes', { testIsolation: true }, () => {
.should('not.exist')
cy.log('Resolve promise to preoceed with MOVE request')
.then(() => resolve(null))
.then(() => resolve())
// Ensure the request is done (file renamed)
cy.wait('@moveFile')
@ -194,7 +194,7 @@ describe('files: Rename nodes', { testIsolation: true }, () => {
.should('not.exist')
})
it('shows warning on extension change', () => {
it('shows warning on extension change - select new extension', () => {
getRowForFile('file.txt').should('be.visible')
triggerActionForFile('file.txt', 'rename')
@ -202,39 +202,60 @@ describe('files: Rename nodes', { testIsolation: true }, () => {
.findByRole('textbox', { name: 'Filename' })
.should('be.visible')
.type('{selectAll}file.md')
.should(haveValidity(''))
.type('{enter}')
// See warning dialog
cy.findByRole('dialog', { name: 'Change file extension' })
.should('be.visible')
.findByRole('button', { name: /use/i })
.findByRole('button', { name: 'Use .md' })
.click()
// See it is renamed
getRowForFile('file.md').should('be.visible')
})
it('shows warning on extension change and allow cancellation', () => {
it('shows warning on extension change - select old extension', () => {
getRowForFile('file.txt').should('be.visible')
triggerActionForFile('file.txt', 'rename')
getRowForFile('file.txt')
.findByRole('textbox', { name: 'Filename' })
.should('be.visible')
.type('{selectAll}file.md')
.should(haveValidity(''))
.type('{selectAll}document.md')
.type('{enter}')
// See warning dialog
cy.findByRole('dialog', { name: 'Change file extension' })
.should('be.visible')
.findByRole('button', { name: /keep/i })
.findByRole('button', { name: 'Keep .txt' })
.click()
// See it is not renamed
// See it is renamed
getRowForFile('document.txt').should('be.visible')
})
it('shows warning on extension removal', () => {
getRowForFile('file.txt').should('be.visible')
getRowForFile('file.md').should('not.exist')
triggerActionForFile('file.txt', 'rename')
getRowForFile('file.txt')
.findByRole('textbox', { name: 'Filename' })
.should('be.visible')
.type('{selectAll}file')
.type('{enter}')
cy.findByRole('dialog', { name: 'Change file extension' })
.should('be.visible')
.findByRole('button', { name: 'Keep .txt' })
.should('be.visible')
cy.findByRole('dialog', { name: 'Change file extension' })
.findByRole('button', { name: 'Remove extension' })
.should('be.visible')
.click()
// See it is renamed
getRowForFile('file').should('be.visible')
getRowForFile('file.txt').should('not.exist')
})
it('does not show warning on folder renaming with a dot', () => {

2
dist/7425-7425.js vendored Normal file

File diff suppressed because one or more lines are too long

123
dist/7425-7425.js.license vendored Normal file
View file

@ -0,0 +1,123 @@
SPDX-License-Identifier: MIT
SPDX-License-Identifier: ISC
SPDX-License-Identifier: GPL-3.0-or-later
SPDX-License-Identifier: BSD-3-Clause
SPDX-License-Identifier: Apache-2.0
SPDX-License-Identifier: AGPL-3.0-or-later
SPDX-License-Identifier: (MPL-2.0 OR Apache-2.0)
SPDX-FileCopyrightText: escape-html developers
SPDX-FileCopyrightText: Tobias Koppers @sokra
SPDX-FileCopyrightText: T. Jameson Little <t.jameson.little@gmail.com>
SPDX-FileCopyrightText: Roman Shtylman <shtylman@gmail.com>
SPDX-FileCopyrightText: Nextcloud GmbH and Nextcloud contributors
SPDX-FileCopyrightText: Matt Zabriskie
SPDX-FileCopyrightText: John-David Dalton <john.david.dalton@gmail.com> (http://allyoucanleet.com/)
SPDX-FileCopyrightText: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
SPDX-FileCopyrightText: Guillaume Chau <guillaume.b.chau@gmail.com>
SPDX-FileCopyrightText: Guillaume Chau
SPDX-FileCopyrightText: GitHub Inc.
SPDX-FileCopyrightText: Feross Aboukhadijeh
SPDX-FileCopyrightText: Evan You
SPDX-FileCopyrightText: Eduardo San Martin Morote
SPDX-FileCopyrightText: Dr.-Ing. Mario Heiderich, Cure53 <mario@cure53.de> (https://cure53.de/)
SPDX-FileCopyrightText: David Clark
SPDX-FileCopyrightText: Christoph Wurst
SPDX-FileCopyrightText: Austin Andrews
SPDX-FileCopyrightText: Anthony Fu <https://github.com/antfu>
SPDX-FileCopyrightText: Andris Reinman
This file is generated from multiple sources. Included packages:
- @mdi/svg
- version: 7.4.47
- license: Apache-2.0
- @nextcloud/auth
- version: 2.4.0
- license: GPL-3.0-or-later
- @nextcloud/axios
- version: 2.5.1
- license: GPL-3.0-or-later
- @nextcloud/browser-storage
- version: 0.4.0
- license: GPL-3.0-or-later
- semver
- version: 7.6.3
- license: ISC
- @nextcloud/event-bus
- version: 3.3.1
- license: GPL-3.0-or-later
- @nextcloud/initial-state
- version: 2.2.0
- license: GPL-3.0-or-later
- @nextcloud/l10n
- version: 3.1.0
- license: GPL-3.0-or-later
- @nextcloud/router
- version: 3.0.1
- license: GPL-3.0-or-later
- @nextcloud/vue
- version: 8.22.0
- license: AGPL-3.0-or-later
- @vue/devtools-api
- version: 6.6.3
- license: MIT
- @vueuse/core
- version: 11.1.0
- license: MIT
- @vueuse/shared
- version: 11.1.0
- license: MIT
- axios
- version: 1.7.9
- license: MIT
- base64-js
- version: 1.5.1
- license: MIT
- css-loader
- version: 7.1.2
- license: MIT
- dompurify
- version: 3.1.7
- license: (MPL-2.0 OR Apache-2.0)
- escape-html
- version: 1.0.3
- license: MIT
- floating-vue
- version: 1.0.0-beta.19
- license: MIT
- focus-trap
- version: 7.6.4
- license: MIT
- ieee754
- version: 1.2.1
- license: BSD-3-Clause
- lodash.get
- version: 4.4.2
- license: MIT
- node-gettext
- version: 3.0.0
- license: MIT
- buffer
- version: 6.0.3
- license: MIT
- pinia
- version: 2.3.1
- license: MIT
- process
- version: 0.11.10
- license: MIT
- style-loader
- version: 4.0.0
- license: MIT
- tabbable
- version: 6.2.0
- license: MIT
- vue-loader
- version: 15.11.1
- license: MIT
- vue
- version: 2.7.16
- license: MIT
- nextcloud
- version: 1.0.0
- license: AGPL-3.0-or-later

1
dist/7425-7425.js.map vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/7425-7425.js.map.license vendored Symbolic link
View file

@ -0,0 +1 @@
7425-7425.js.license

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

4
dist/files-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