Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>tags/v27.0.0beta1
* Initializes the files app | * Initializes the files app | ||||
*/ | */ | ||||
initialize: function() { | initialize: function() { | ||||
this.navigation = OCP.Files.Navigation; | |||||
this.$showHiddenFiles = $('input#showhiddenfilesToggle'); | this.$showHiddenFiles = $('input#showhiddenfilesToggle'); | ||||
var showHidden = $('#showHiddenFiles').val() === "1"; | var showHidden = $('#showHiddenFiles').val() === "1"; | ||||
this.$showHiddenFiles.prop('checked', showHidden); | this.$showHiddenFiles.prop('checked', showHidden); | ||||
OC.Plugins.attach('OCA.Files.App', this); | OC.Plugins.attach('OCA.Files.App', this); | ||||
this._setupEvents(); | this._setupEvents(); | ||||
// trigger URL change event handlers | |||||
this._onPopState({ ...OC.Util.History.parseUrlQuery(), view: this.navigation?.active?.id }); | |||||
this._debouncedPersistShowHiddenFilesState = _.debounce(this._persistShowHiddenFilesState, 1200); | this._debouncedPersistShowHiddenFilesState = _.debounce(this._persistShowHiddenFilesState, 1200); | ||||
this._debouncedPersistCropImagePreviewsState = _.debounce(this._persistCropImagePreviewsState, 1200); | this._debouncedPersistCropImagePreviewsState = _.debounce(this._persistCropImagePreviewsState, 1200); | ||||
OCP.WhatsNew.query(); // for Nextcloud server | OCP.WhatsNew.query(); // for Nextcloud server | ||||
sessionStorage.setItem('WhatsNewServerCheck', Date.now()); | sessionStorage.setItem('WhatsNewServerCheck', Date.now()); | ||||
} | } | ||||
window._nc_event_bus.emit('files:legacy-view:initialized', this); | |||||
}, | }, | ||||
/** | /** |
<template> | |||||
<NcBreadcrumbs data-cy-files-content-breadcrumbs> | |||||
<!-- Current path sections --> | |||||
<NcBreadcrumb v-for="section in sections" | |||||
:key="section.dir" | |||||
:aria-label="t('files', `Go to the '{dir}' directory`, section)" | |||||
v-bind="section" /> | |||||
</NcBreadcrumbs> | |||||
</template> | |||||
<script> | |||||
import NcBreadcrumbs from '@nextcloud/vue/dist/Components/NcBreadcrumbs.js' | |||||
import NcBreadcrumb from '@nextcloud/vue/dist/Components/NcBreadcrumb.js' | |||||
import { basename } from 'path' | |||||
export default { | |||||
name: 'BreadCrumbs', | |||||
components: { | |||||
NcBreadcrumbs, | |||||
NcBreadcrumb, | |||||
}, | |||||
props: { | |||||
path: { | |||||
type: String, | |||||
default: '/', | |||||
}, | |||||
}, | |||||
computed: { | |||||
dirs() { | |||||
const cumulativePath = (acc) => (value) => (acc += `${value}/`) | |||||
return ['/', ...this.path.split('/').filter(Boolean).map(cumulativePath('/'))] | |||||
}, | |||||
sections() { | |||||
return this.dirs.map(dir => { | |||||
const to = { ...this.$route, query: { dir } } | |||||
return { | |||||
dir, | |||||
to, | |||||
title: basename(dir), | |||||
} | |||||
}) | |||||
}, | |||||
}, | |||||
} | |||||
</script> | |||||
<style lang="scss" scoped> | |||||
.breadcrumb { | |||||
// Take as much space as possible | |||||
flex: 1 1 100% !important; | |||||
width: 100%; | |||||
} | |||||
</style> |
<!-- | |||||
- @copyright Copyright (c) 2019 Gary Kim <gary@garykim.dev> | |||||
- | |||||
- @author Gary Kim <gary@garykim.dev> | |||||
- | |||||
- @license GNU AGPL version 3 or any later version | |||||
- | |||||
- 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/>. | |||||
- | |||||
--> | |||||
<template> | |||||
<Fragment> | |||||
<td class="files-list__row-checkbox"> | |||||
<NcCheckboxRadioSwitch :aria-label="t('files', 'Select the row for {displayName}', { displayName })" | |||||
:checked.sync="selectedFiles" | |||||
:value="fileid.toString()" | |||||
name="selectedFiles" /> | |||||
</td> | |||||
<!-- Icon or preview --> | |||||
<td class="files-list__row-icon"> | |||||
<FolderIcon v-if="source.type === 'folder'" /> | |||||
</td> | |||||
<!-- Link to file and --> | |||||
<td class="files-list__row-name"> | |||||
<a v-bind="linkTo"> | |||||
{{ displayName }} | |||||
</a> | |||||
</td> | |||||
</Fragment> | |||||
</template> | |||||
<script lang="ts"> | |||||
import { Folder, File } from '@nextcloud/files' | |||||
import { Fragment } from 'vue-fragment' | |||||
import { join } from 'path' | |||||
import { translate } from '@nextcloud/l10n' | |||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js' | |||||
import FolderIcon from 'vue-material-design-icons/Folder.vue' | |||||
import logger from '../logger' | |||||
export default { | |||||
name: 'FileEntry', | |||||
components: { | |||||
FolderIcon, | |||||
Fragment, | |||||
NcCheckboxRadioSwitch, | |||||
}, | |||||
props: { | |||||
index: { | |||||
type: Number, | |||||
required: true, | |||||
}, | |||||
source: { | |||||
type: [File, Folder], | |||||
required: true, | |||||
}, | |||||
}, | |||||
computed: { | |||||
dir() { | |||||
// Remove any trailing slash but leave root slash | |||||
return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1') | |||||
}, | |||||
fileid() { | |||||
return this.source.attributes.fileid | |||||
}, | |||||
displayName() { | |||||
return this.source.attributes.displayName | |||||
|| this.source.basename | |||||
}, | |||||
linkTo() { | |||||
if (this.source.type === 'folder') { | |||||
const to = { ...this.$route, query: { dir: join(this.dir, this.source.basename) } } | |||||
return { | |||||
is: 'router-link', | |||||
title: this.t('files', 'Open folder {name}', { name: this.displayName }), | |||||
to, | |||||
} | |||||
} | |||||
return { | |||||
href: this.source.source, | |||||
// TODO: Use first action title ? | |||||
title: this.t('files', 'Download file {name}', { name: this.displayName }), | |||||
} | |||||
}, | |||||
selectedFiles: { | |||||
get() { | |||||
return this.$store.state.selection.selected | |||||
}, | |||||
set(selection) { | |||||
logger.debug('Added node to selection', { selection }) | |||||
this.$store.dispatch('selection/set', selection) | |||||
}, | |||||
}, | |||||
}, | |||||
methods: { | |||||
/** | |||||
* Get a cached note from the store | |||||
* | |||||
* @param {number} fileId the file id to get | |||||
* @return {Folder|File} | |||||
*/ | |||||
getNode(fileId) { | |||||
return this.$store.getters['files/getNode'](fileId) | |||||
}, | |||||
t: translate, | |||||
}, | |||||
} | |||||
</script> | |||||
<style scoped lang="scss"> | |||||
@import '../mixins/fileslist-row.scss' | |||||
</style> |
<!-- | |||||
- @copyright Copyright (c) 2019 Gary Kim <gary@garykim.dev> | |||||
- | |||||
- @author Gary Kim <gary@garykim.dev> | |||||
- | |||||
- @license GNU AGPL version 3 or any later version | |||||
- | |||||
- 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/>. | |||||
- | |||||
--> | |||||
<template> | |||||
<tr> | |||||
<th class="files-list__row-checkbox"> | |||||
<NcCheckboxRadioSwitch v-bind="selectAllBind" @update:checked="onToggleAll" /> | |||||
</th> | |||||
<!-- Icon or preview --> | |||||
<th class="files-list__row-icon" /> | |||||
<!-- Link to file and --> | |||||
<th class="files-list__row-name"> | |||||
{{ t('files', 'Name') }} | |||||
</th> | |||||
</tr> | |||||
</template> | |||||
<script lang="ts"> | |||||
import { translate } from '@nextcloud/l10n' | |||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js' | |||||
import logger from '../logger' | |||||
import { File, Folder } from '@nextcloud/files' | |||||
export default { | |||||
name: 'FilesListHeader', | |||||
components: { | |||||
NcCheckboxRadioSwitch, | |||||
}, | |||||
props: { | |||||
nodes: { | |||||
type: [File, Folder], | |||||
required: true, | |||||
}, | |||||
}, | |||||
computed: { | |||||
dir() { | |||||
// Remove any trailing slash but leave root slash | |||||
return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1') | |||||
}, | |||||
selectAllBind() { | |||||
return { | |||||
ariaLabel: this.isNoneSelected || this.isSomeSelected | |||||
? this.t('files', 'Select all') | |||||
: this.t('files', 'Unselect all'), | |||||
checked: this.isAllSelected, | |||||
indeterminate: this.isSomeSelected, | |||||
} | |||||
}, | |||||
isAllSelected() { | |||||
return this.selectedFiles.length === this.nodes.length | |||||
}, | |||||
isNoneSelected() { | |||||
return this.selectedFiles.length === 0 | |||||
}, | |||||
isSomeSelected() { | |||||
return !this.isAllSelected && !this.isNoneSelected | |||||
}, | |||||
selectedFiles() { | |||||
return this.$store.state.selection.selected | |||||
}, | |||||
}, | |||||
methods: { | |||||
/** | |||||
* Get a cached note from the store | |||||
* | |||||
* @param {number} fileId the file id to get | |||||
* @return {Folder|File} | |||||
*/ | |||||
getNode(fileId) { | |||||
return this.$store.getters['files/getNode'](fileId) | |||||
}, | |||||
onToggleAll(selected) { | |||||
if (selected) { | |||||
const selection = this.nodes.map(node => node.attributes.fileid.toString()) | |||||
logger.debug('Added all nodes to selection', { selection }) | |||||
this.$store.dispatch('selection/set', selection) | |||||
} else { | |||||
logger.debug('Cleared selection') | |||||
this.$store.dispatch('selection/reset') | |||||
} | |||||
}, | |||||
t: translate, | |||||
}, | |||||
} | |||||
</script> | |||||
<style scoped lang="scss"> | |||||
@import '../mixins/fileslist-row.scss' | |||||
</style> |
<!-- | |||||
- @copyright Copyright (c) 2019 Gary Kim <gary@garykim.dev> | |||||
- | |||||
- @author Gary Kim <gary@garykim.dev> | |||||
- | |||||
- @license GNU AGPL version 3 or any later version | |||||
- | |||||
- 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/>. | |||||
- | |||||
--> | |||||
<template> | |||||
<VirtualList class="files-list" | |||||
:data-component="FileEntry" | |||||
:data-key="getFileId" | |||||
:data-sources="nodes" | |||||
:estimate-size="55" | |||||
:table-mode="true" | |||||
item-class="files-list__row" | |||||
wrap-class="files-list__body"> | |||||
<template #before> | |||||
<caption v-show="false" class="files-list__caption"> | |||||
{{ summary }} | |||||
</caption> | |||||
</template> | |||||
<template #header> | |||||
<FilesListHeader :nodes="nodes" /> | |||||
</template> | |||||
</VirtualList> | |||||
</template> | |||||
<script lang="ts"> | |||||
import { Folder, File } from '@nextcloud/files' | |||||
import { translate, translatePlural } from '@nextcloud/l10n' | |||||
import VirtualList from 'vue-virtual-scroll-list' | |||||
import FileEntry from './FileEntry.vue' | |||||
import FilesListHeader from './FilesListHeader.vue' | |||||
export default { | |||||
name: 'FilesListVirtual', | |||||
components: { | |||||
VirtualList, | |||||
FilesListHeader, | |||||
}, | |||||
props: { | |||||
nodes: { | |||||
type: [File, Folder], | |||||
required: true, | |||||
}, | |||||
}, | |||||
data() { | |||||
return { | |||||
FileEntry, | |||||
} | |||||
}, | |||||
computed: { | |||||
files() { | |||||
return this.nodes.filter(node => node.type === 'file') | |||||
}, | |||||
summaryFile() { | |||||
const count = this.files.length | |||||
return translatePlural('files', '{count} file', '{count} files', count, { count }) | |||||
}, | |||||
summaryFolder() { | |||||
const count = this.nodes.length - this.files.length | |||||
return translatePlural('files', '{count} folder', '{count} folders', count, { count }) | |||||
}, | |||||
summary() { | |||||
return translate('files', '{summaryFile} and {summaryFolder}', this) | |||||
}, | |||||
}, | |||||
methods: { | |||||
getFileId(node) { | |||||
return node.attributes.fileid | |||||
}, | |||||
t: translate, | |||||
}, | |||||
} | |||||
</script> | |||||
<style scoped lang="scss"> | |||||
.files-list { | |||||
--row-height: 55px; | |||||
--checkbox-padding: calc((var(--row-height) - var(--checkbox-size)) / 2); | |||||
--checkbox-size: 24px; | |||||
--clickable-area: 44px; | |||||
--icon-preview-size: 32px; | |||||
display: block; | |||||
overflow: auto; | |||||
height: 100%; | |||||
&::v-deep { | |||||
tbody, thead, tfoot { | |||||
display: flex; | |||||
flex-direction: column; | |||||
width: 100%; | |||||
} | |||||
thead, .files-list__row { | |||||
border-bottom: 1px solid var(--color-border); | |||||
} | |||||
} | |||||
} | |||||
</style> |
import Vue from 'vue' | import Vue from 'vue' | ||||
import NavigationService from './services/Navigation.ts' | import NavigationService from './services/Navigation.ts' | ||||
import NavigationView from './views/Navigation.vue' | import NavigationView from './views/Navigation.vue' | ||||
import FilesListView from './views/FilesList.vue' | |||||
import SettingsService from './services/Settings.js' | import SettingsService from './services/Settings.js' | ||||
import SettingsModel from './models/Setting.js' | import SettingsModel from './models/Setting.js' | ||||
import router from './router/router.js' | import router from './router/router.js' | ||||
import store from './store/index.ts' | |||||
// Init private and public Files namespace | // Init private and public Files namespace | ||||
window.OCA.Files = window.OCA.Files ?? {} | window.OCA.Files = window.OCA.Files ?? {} | ||||
}) | }) | ||||
FilesNavigationRoot.$mount('#app-navigation-files') | FilesNavigationRoot.$mount('#app-navigation-files') | ||||
// Init content list view | |||||
const ListView = Vue.extend(FilesListView) | |||||
const FilesList = new ListView({ | |||||
name: 'FilesListRoot', | |||||
propsData: { | |||||
Navigation, | |||||
}, | |||||
router, | |||||
store, | |||||
}) | |||||
FilesList.$mount('#app-content-vue') | |||||
// Init legacy files views | // Init legacy files views | ||||
processLegacyFilesViews() | processLegacyFilesViews() |
/** | |||||
* @copyright Copyright (c) 2023 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/>. | |||||
* | |||||
*/ | |||||
td, th { | |||||
height: var(--row-height); | |||||
vertical-align: middle; | |||||
padding: 0px; | |||||
border: none; | |||||
} | |||||
.files-list__row-checkbox { | |||||
width: var(--row-height); | |||||
&::v-deep .checkbox-radio-switch { | |||||
--icon-size: var(--checkbox-size); | |||||
display: flex; | |||||
justify-content: center; | |||||
label.checkbox-radio-switch__label { | |||||
margin: 0; | |||||
height: var(--clickable-area); | |||||
width: var(--clickable-area); | |||||
padding: calc((var(--clickable-area) - var(--checkbox-size)) / 2) | |||||
} | |||||
.checkbox-radio-switch__icon { | |||||
margin: 0 !important; | |||||
} | |||||
} | |||||
} | |||||
.files-list__row-icon { | |||||
// Remove left padding to look nicer with the checkbox | |||||
// => ico preview size + one checkbox td padding | |||||
width: calc(var(--icon-preview-size) + var(--checkbox-padding)); | |||||
padding-right: var(--checkbox-padding); | |||||
color: var(--color-primary-element); | |||||
& > span { | |||||
justify-content: flex-start; | |||||
} | |||||
&::v-deep svg { | |||||
width: var(--icon-preview-size); | |||||
height: var(--icon-preview-size); | |||||
} | |||||
} |
* along with this program. If not, see <http://www.gnu.org/licenses/>. | * along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
* | * | ||||
*/ | */ | ||||
import type Node from '@nextcloud/files/dist/files/node' | |||||
/* eslint-disable */ | |||||
import type { Folder, Node } from '@nextcloud/files' | |||||
import isSvg from 'is-svg' | import isSvg from 'is-svg' | ||||
import logger from '../logger.js' | import logger from '../logger.js' | ||||
export type ContentsWithRoot = { | |||||
folder: Folder, | |||||
contents: Node[] | |||||
} | |||||
export interface Column { | export interface Column { | ||||
/** Unique column ID */ | /** Unique column ID */ | ||||
id: string | id: string | ||||
/** Translated column title */ | /** Translated column title */ | ||||
title: string | title: string | ||||
/** Property key from Node main or additional attributes. | |||||
Will be used if no custom sort function is provided. | |||||
Sorting will be done by localCompare */ | |||||
/** | |||||
* Property key from Node main or additional attributes. | |||||
* Will be used if no custom sort function is provided. | |||||
* Sorting will be done by localCompare | |||||
*/ | |||||
property: string | property: string | ||||
/** Special function used to sort Nodes between them */ | /** Special function used to sort Nodes between them */ | ||||
sortFunction?: (nodeA: Node, nodeB: Node) => number; | sortFunction?: (nodeA: Node, nodeB: Node) => number; | ||||
id: string | id: string | ||||
/** Translated view name */ | /** Translated view name */ | ||||
name: string | name: string | ||||
/** Method return the content of the provided path */ | |||||
getFiles: (path: string) => Node[] | |||||
/** | |||||
* Method return the content of the provided path | |||||
* This ideally should be a cancellable promise. | |||||
* promise.cancel(reason) will be called when the directory | |||||
* change and the promise is not resolved yet. | |||||
* You _must_ also return the current directory | |||||
* information alongside with its content. | |||||
*/ | |||||
getContents: (path: string) => Promise<ContentsWithRoot[]> | |||||
/** The view icon as an inline svg */ | /** The view icon as an inline svg */ | ||||
icon: string | icon: string | ||||
/** The view order */ | /** The view order */ | ||||
* TODO: remove when support for legacy views is removed | * TODO: remove when support for legacy views is removed | ||||
*/ | */ | ||||
if (!view.legacy) { | if (!view.legacy) { | ||||
if (!view.getFiles || typeof view.getFiles !== 'function') { | |||||
throw new Error('Navigation getFiles is required and must be a function') | |||||
if (!view.getContents || typeof view.getContents !== 'function') { | |||||
throw new Error('Navigation getContents is required and must be a function') | |||||
} | } | ||||
if (!view.icon || typeof view.icon !== 'string' || !isSvg(view.icon)) { | if (!view.icon || typeof view.icon !== 'string' || !isSvg(view.icon)) { |
/** | |||||
* @copyright Copyright (c) 2023 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/>. | |||||
* | |||||
*/ | |||||
/* eslint-disable */ | |||||
import type { Folder, Node } from '@nextcloud/files' | |||||
import Vue from 'vue' | |||||
import type { FileStore, RootStore, RootOptions, Service } from '../types' | |||||
const state = { | |||||
files: {} as FileStore, | |||||
roots: {} as RootStore, | |||||
} | |||||
const getters = { | |||||
/** | |||||
* Get a file or folder by id | |||||
*/ | |||||
getNode: (state) => (id: number): Node|undefined => state.files[id], | |||||
/** | |||||
* Get a list of files or folders by their IDs | |||||
* Does not return undefined values | |||||
*/ | |||||
getNodes: (state) => (ids: number[]): Node[] => ids | |||||
.map(id => state.files[id]) | |||||
.filter(Boolean), | |||||
/** | |||||
* Get a file or folder by id | |||||
*/ | |||||
getRoot: (state) => (service: Service): Folder|undefined => state.roots[service], | |||||
} | |||||
const mutations = { | |||||
updateNodes: (state, nodes: Node[]) => { | |||||
nodes.forEach(node => { | |||||
if (!node.attributes.fileid) { | |||||
return | |||||
} | |||||
Vue.set(state.files, node.attributes.fileid, node) | |||||
// state.files = { | |||||
// ...state.files, | |||||
// [node.attributes.fileid]: node, | |||||
// } | |||||
}) | |||||
}, | |||||
setRoot: (state, { service, root }: RootOptions) => { | |||||
state.roots = { | |||||
...state.roots, | |||||
[service]: root, | |||||
} | |||||
} | |||||
} | |||||
const actions = { | |||||
/** | |||||
* Insert valid nodes into the store. | |||||
* Roots (that does _not_ have a fileid) should | |||||
* be defined in the roots store | |||||
*/ | |||||
addNodes: (context, nodes: Node[]) => { | |||||
context.commit('updateNodes', nodes) | |||||
}, | |||||
/** | |||||
* Set the root of a service | |||||
*/ | |||||
setRoot(context, { service, root }: RootOptions) { | |||||
context.commit('setRoot', { service, root }) | |||||
} | |||||
} | |||||
export default { | |||||
namespaced: true, | |||||
state, | |||||
getters, | |||||
mutations, | |||||
actions, | |||||
} |
import Vue from 'vue' | |||||
import Vuex, { Store } from 'vuex' | |||||
import files from './files' | |||||
import paths from './paths' | |||||
import selection from './selection' | |||||
Vue.use(Vuex) | |||||
export default new Store({ | |||||
modules: { | |||||
files, | |||||
paths, | |||||
selection, | |||||
}, | |||||
}) |
/** | |||||
* @copyright Copyright (c) 2023 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/>. | |||||
* | |||||
*/ | |||||
/* eslint-disable */ | |||||
import type { Folder } from '@nextcloud/files' | |||||
import Vue from 'vue' | |||||
import type { PathOptions, ServicePaths, ServiceStore } from '../types' | |||||
const module = { | |||||
state: { | |||||
services: { | |||||
files: {} as ServicePaths, | |||||
} as ServiceStore, | |||||
}, | |||||
getters: { | |||||
getPath(state: { services: ServiceStore }) { | |||||
return (service: string, path: string): number|undefined => { | |||||
if (!state.services[service]) { | |||||
return undefined | |||||
} | |||||
return state.services[service][path] | |||||
} | |||||
}, | |||||
}, | |||||
mutations: { | |||||
addPath: (state, opts: PathOptions) => { | |||||
// If it doesn't exists, init the service state | |||||
if (!state.services[opts.service]) { | |||||
// TODO: investigate why Vue.set is not working | |||||
state.services = { | |||||
[opts.service]: {} as ServicePaths, | |||||
...state.services | |||||
} | |||||
} | |||||
// Now we can set the path | |||||
Vue.set(state.services[opts.service], opts.path, opts.fileid) | |||||
} | |||||
}, | |||||
actions: { | |||||
addPath: (context, opts: PathOptions) => { | |||||
context.commit('addPath', opts) | |||||
}, | |||||
} | |||||
} | |||||
export default { | |||||
namespaced: true, | |||||
...module, | |||||
} |
/** | |||||
* @copyright Copyright (c) 2023 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/>. | |||||
* | |||||
*/ | |||||
/* eslint-disable */ | |||||
import type { Folder } from '@nextcloud/files' | |||||
import Vue from 'vue' | |||||
import type { PathOptions, ServicePaths, ServiceStore } from '../types' | |||||
const module = { | |||||
state: { | |||||
selected: [] as number[] | |||||
}, | |||||
mutations: { | |||||
set: (state, selection: number[]) => { | |||||
Vue.set(state, 'selected', selection) | |||||
} | |||||
}, | |||||
actions: { | |||||
set: (context, selection = [] as number[]) => { | |||||
context.commit('set', selection) | |||||
}, | |||||
reset(context) { | |||||
context.commit('set', []) | |||||
} | |||||
} | |||||
} | |||||
export default { | |||||
namespaced: true, | |||||
...module, | |||||
} |
/** | |||||
* @copyright Copyright (c) 2023 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/>. | |||||
* | |||||
*/ | |||||
/* eslint-disable */ | |||||
import type { Folder } from '@nextcloud/files' | |||||
import type { Node } from '@nextcloud/files' | |||||
// Global definitions | |||||
export type Service = string | |||||
// Files store | |||||
export type FileStore = { | |||||
[id: number]: Node | |||||
} | |||||
export type RootStore = { | |||||
[service: Service]: Folder | |||||
} | |||||
export interface RootOptions { | |||||
root: Folder | |||||
service: Service | |||||
} | |||||
// Paths store | |||||
export type ServicePaths = { | |||||
[path: string]: number | |||||
} | |||||
export type ServiceStore = { | |||||
[service: Service]: ServicePaths | |||||
} | |||||
export interface PathOptions { | |||||
service: Service | |||||
path: string | |||||
fileid: number | |||||
} |
<!-- | |||||
- @copyright Copyright (c) 2019 Gary Kim <gary@garykim.dev> | |||||
- | |||||
- @author Gary Kim <gary@garykim.dev> | |||||
- | |||||
- @license GNU AGPL version 3 or any later version | |||||
- | |||||
- 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/>. | |||||
- | |||||
--> | |||||
<template> | |||||
<NcAppContent v-show="!currentView?.legacy" | |||||
:class="{'app-content--hidden': currentView?.legacy}" | |||||
data-cy-files-content> | |||||
<div class="files-list__header"> | |||||
<!-- Current folder breadcrumbs --> | |||||
<BreadCrumbs :path="dir" /> | |||||
<!-- Secondary loading indicator --> | |||||
<NcLoadingIcon v-if="isRefreshing" class="files-list__refresh-icon" /> | |||||
</div> | |||||
<!-- Initial loading --> | |||||
<NcLoadingIcon v-if="loading && !isRefreshing" | |||||
class="files-list__loading-icon" | |||||
:size="38" | |||||
:title="t('files', 'Loading current folder')" /> | |||||
<!-- Empty content placeholder --> | |||||
<NcEmptyContent v-else-if="!loading && isEmptyDir" | |||||
:title="t('files', 'No files in here')" | |||||
:description="t('files', 'No files or folders have been deleted yet')" | |||||
data-cy-files-content-empty> | |||||
<template #action> | |||||
<NcButton v-if="dir !== '/'" | |||||
aria-label="t('files', 'Go to the previous folder')" | |||||
type="primary" | |||||
:to="toPreviousDir"> | |||||
{{ t('files', 'Go back') }} | |||||
</NcButton> | |||||
</template> | |||||
<template #icon> | |||||
<TrashCan /> | |||||
</template> | |||||
</NcEmptyContent> | |||||
<!-- File list --> | |||||
<FilesListVirtual v-else :nodes="dirContents" /> | |||||
</NcAppContent> | |||||
</template> | |||||
<script lang="ts"> | |||||
import { Folder } from '@nextcloud/files' | |||||
import { translate } from '@nextcloud/l10n' | |||||
import NcAppContent from '@nextcloud/vue/dist/Components/NcAppContent.js' | |||||
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' | |||||
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js' | |||||
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js' | |||||
import TrashCan from 'vue-material-design-icons/TrashCan.vue' | |||||
import BreadCrumbs from '../components/BreadCrumbs.vue' | |||||
import logger from '../logger.js' | |||||
import Navigation from '../services/Navigation' | |||||
import FilesListVirtual from '../components/FilesListVirtual.vue' | |||||
import { ContentsWithRoot } from '../services/Navigation' | |||||
import { join } from 'path' | |||||
export default { | |||||
name: 'FilesList', | |||||
components: { | |||||
BreadCrumbs, | |||||
FilesListVirtual, | |||||
NcAppContent, | |||||
NcButton, | |||||
NcEmptyContent, | |||||
NcLoadingIcon, | |||||
TrashCan, | |||||
}, | |||||
props: { | |||||
// eslint-disable-next-line vue/prop-name-casing | |||||
Navigation: { | |||||
type: Navigation, | |||||
required: true, | |||||
}, | |||||
}, | |||||
data() { | |||||
return { | |||||
loading: true, | |||||
promise: null, | |||||
} | |||||
}, | |||||
computed: { | |||||
currentViewId() { | |||||
return this.$route.params.view || 'files' | |||||
}, | |||||
/** @return {Navigation} */ | |||||
currentView() { | |||||
return this.views.find(view => view.id === this.currentViewId) | |||||
}, | |||||
/** @return {Navigation[]} */ | |||||
views() { | |||||
return this.Navigation.views | |||||
}, | |||||
/** | |||||
* The current directory query. | |||||
* @return {string} | |||||
*/ | |||||
dir() { | |||||
// Remove any trailing slash but leave root slash | |||||
return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1') | |||||
}, | |||||
/** | |||||
* The current folder. | |||||
* @return {Folder|undefined} | |||||
*/ | |||||
currentFolder() { | |||||
if (this.dir === '/') { | |||||
return this.$store.getters['files/getRoot'](this.currentViewId) | |||||
} | |||||
const fileId = this.$store.getters['paths/getPath'](this.currentViewId, this.dir) | |||||
return this.$store.getters['files/getNode'](fileId) | |||||
}, | |||||
/** | |||||
* The current directory contents. | |||||
* @return {Node[]} | |||||
*/ | |||||
dirContents() { | |||||
return (this.currentFolder?.children || []).map(this.getNode) | |||||
}, | |||||
/** | |||||
* The current directory is empty. | |||||
*/ | |||||
isEmptyDir() { | |||||
return this.dirContents.length === 0 | |||||
}, | |||||
/** | |||||
* We are refreshing the current directory. | |||||
* But we already have a cached version of it | |||||
* that is not empty. | |||||
*/ | |||||
isRefreshing() { | |||||
return this.currentFolder !== undefined | |||||
&& !this.isEmptyDir | |||||
&& this.loading | |||||
}, | |||||
/** | |||||
* Route to the previous directory. | |||||
*/ | |||||
toPreviousDir() { | |||||
const dir = this.dir.split('/').slice(0, -1).join('/') || '/' | |||||
return { ...this.$route, query: { dir } } | |||||
}, | |||||
}, | |||||
watch: { | |||||
currentView(newView, oldView) { | |||||
if (newView?.id === oldView?.id) { | |||||
return | |||||
} | |||||
logger.debug('View changed', { newView, oldView }) | |||||
this.$store.dispatch('selection/reset') | |||||
this.fetchContent() | |||||
}, | |||||
dir(newDir, oldDir) { | |||||
logger.debug('Directory changed', { newDir, oldDir }) | |||||
// TODO: preserve selection on browsing? | |||||
this.$store.dispatch('selection/reset') | |||||
this.fetchContent() | |||||
}, | |||||
paths(paths) { | |||||
logger.debug('Paths changed', { paths }) | |||||
}, | |||||
currentFolder(currentFolder) { | |||||
logger.debug('currentFolder changed', { currentFolder }) | |||||
}, | |||||
}, | |||||
methods: { | |||||
async fetchContent() { | |||||
if (this.currentView?.legacy) { | |||||
return | |||||
} | |||||
this.loading = true | |||||
const dir = this.dir | |||||
const currentView = this.currentView | |||||
// If we have a cancellable promise ongoing, cancel it | |||||
if (typeof this.promise?.cancel === 'function') { | |||||
this.promise.cancel() | |||||
logger.debug('Cancelled previous ongoing fetch') | |||||
} | |||||
// Fetch the current dir contents | |||||
/** @type {Promise<ContentsWithRoot>} */ | |||||
this.promise = currentView.getContents(dir) | |||||
try { | |||||
const { folder, contents } = await this.promise | |||||
logger.debug('Fetched contents', { dir, folder, contents }) | |||||
// Update store | |||||
this.$store.dispatch('files/addNodes', contents) | |||||
// Define current directory children | |||||
folder.children = contents.map(node => node.attributes.fileid) | |||||
// If we're in the root dir, define the root | |||||
if (dir === '/') { | |||||
console.debug('files', 'Setting root', { service: currentView.id, folder }) | |||||
this.$store.dispatch('files/setRoot', { service: currentView.id, root: folder }) | |||||
} else | |||||
// Otherwise, add the folder to the store | |||||
if (folder.attributes.fileid) { | |||||
this.$store.dispatch('files/addNodes', [folder]) | |||||
this.$store.dispatch('paths/addPath', { service: currentView.id, fileid: folder.attributes.fileid, path: dir }) | |||||
} else { | |||||
// If we're here, the view API messed up | |||||
logger.error('Invalid root folder returned', { dir, folder, currentView }) | |||||
} | |||||
// Update paths store | |||||
const folders = contents.filter(node => node.type === 'folder') | |||||
folders.forEach(node => { | |||||
this.$store.dispatch('paths/addPath', { service: currentView.id, fileid: node.attributes.fileid, path: join(dir, node.basename) }) | |||||
}) | |||||
} catch (error) { | |||||
logger.error('Error while fetching content', { error }) | |||||
} finally { | |||||
this.loading = false | |||||
} | |||||
}, | |||||
/** | |||||
* Get a cached note from the store | |||||
* | |||||
* @param {number} fileId the file id to get | |||||
* @return {Folder|File} | |||||
*/ | |||||
getNode(fileId) { | |||||
return this.$store.getters['files/getNode'](fileId) | |||||
}, | |||||
t: translate, | |||||
}, | |||||
} | |||||
</script> | |||||
<style scoped lang="scss"> | |||||
.app-content { | |||||
// Virtual list needs to be full height and is scrollable | |||||
display: flex; | |||||
overflow: hidden; | |||||
flex-direction: column; | |||||
max-height: 100%; | |||||
// TODO: remove after all legacy views are migrated | |||||
// Hides the legacy app-content if shown view is not legacy | |||||
&:not(&--hidden)::v-deep + #app-content { | |||||
display: none; | |||||
} | |||||
} | |||||
$margin: 4px; | |||||
$navigationToggleSize: 50px; | |||||
.files-list { | |||||
&__header { | |||||
display: flex; | |||||
align-content: center; | |||||
// Do not grow or shrink (vertically) | |||||
flex: 0 0; | |||||
// Align with the navigation toggle icon | |||||
margin: $margin $margin $margin $navigationToggleSize; | |||||
> * { | |||||
// Do not grow or shrink (horizontally) | |||||
// Only the breadcrumbs shrinks | |||||
flex: 0 0; | |||||
} | |||||
} | |||||
&__refresh-icon { | |||||
flex: 0 0 44px; | |||||
width: 44px; | |||||
height: 44px; | |||||
} | |||||
&__loading-icon { | |||||
margin: auto; | |||||
} | |||||
} | |||||
</style> |
:title="view.name" | :title="view.name" | ||||
:to="generateToNavigation(view)" | :to="generateToNavigation(view)" | ||||
@update:open="onToggleExpand(view)"> | @update:open="onToggleExpand(view)"> | ||||
<!-- Sanitized icon as svg if provided --> | |||||
<NcIconSvgWrapper v-if="view.icon" slot="icon" :svg="view.icon" /> | |||||
<!-- Child views if any --> | |||||
<NcAppNavigationItem v-for="child in childViews[view.id]" | <NcAppNavigationItem v-for="child in childViews[view.id]" | ||||
:key="child.id" | :key="child.id" | ||||
:data-cy-files-navigation-item="child.id" | :data-cy-files-navigation-item="child.id" | ||||
:exact="true" | :exact="true" | ||||
:icon="child.iconClass" | :icon="child.iconClass" | ||||
:title="child.name" | :title="child.name" | ||||
:to="generateToNavigation(child)" /> | |||||
:to="generateToNavigation(child)"> | |||||
<!-- Sanitized icon as svg if provided --> | |||||
<NcIconSvgWrapper v-if="view.icon" slot="icon" :svg="view.icon" /> | |||||
</NcAppNavigationItem> | |||||
</NcAppNavigationItem> | </NcAppNavigationItem> | ||||
</template> | </template> | ||||
import Cog from 'vue-material-design-icons/Cog.vue' | import Cog from 'vue-material-design-icons/Cog.vue' | ||||
import NcAppNavigation from '@nextcloud/vue/dist/Components/NcAppNavigation.js' | import NcAppNavigation from '@nextcloud/vue/dist/Components/NcAppNavigation.js' | ||||
import NcAppNavigationItem from '@nextcloud/vue/dist/Components/NcAppNavigationItem.js' | import NcAppNavigationItem from '@nextcloud/vue/dist/Components/NcAppNavigationItem.js' | ||||
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js' | |||||
import logger from '../logger.js' | import logger from '../logger.js' | ||||
import Navigation from '../services/Navigation.ts' | import Navigation from '../services/Navigation.ts' | ||||
components: { | components: { | ||||
Cog, | Cog, | ||||
NavigationQuota, | |||||
NcAppNavigation, | NcAppNavigation, | ||||
NcAppNavigationItem, | NcAppNavigationItem, | ||||
NcIconSvgWrapper, | |||||
SettingsModal, | SettingsModal, | ||||
NavigationQuota, | |||||
}, | }, | ||||
props: { | props: { | ||||
watch: { | watch: { | ||||
currentView(view, oldView) { | currentView(view, oldView) { | ||||
logger.debug('View changed', { id: view.id, view }) | |||||
// If undefined, it means we're initializing the view | |||||
// This is handled by the legacy-view:initialized event | |||||
if (view?.id === oldView?.id) { | |||||
return | |||||
} | |||||
this.Navigation.setActive(view.id) | |||||
logger.debug('Navigation changed', { id: view.id, view }) | |||||
// debugger | |||||
this.showView(view, oldView) | this.showView(view, oldView) | ||||
}, | }, | ||||
}, | }, | ||||
} | } | ||||
subscribe('files:legacy-navigation:changed', this.onLegacyNavigationChanged) | subscribe('files:legacy-navigation:changed', this.onLegacyNavigationChanged) | ||||
// TODO: remove this once the legacy navigation is gone | |||||
subscribe('files:legacy-view:initialized', () => { | |||||
logger.debug('Legacy view initialized', { ...this.currentView }) | |||||
this.showView(this.currentView) | |||||
}) | |||||
}, | }, | ||||
methods: { | methods: { | ||||
// Closing any opened sidebar | // Closing any opened sidebar | ||||
window?.OCA?.Files?.Sidebar?.close?.() | window?.OCA?.Files?.Sidebar?.close?.() | ||||
if (view.legacy) { | |||||
if (view?.legacy) { | |||||
const newAppContent = document.querySelector('#app-content #app-content-' + this.currentView.id + '.viewcontainer') | const newAppContent = document.querySelector('#app-content #app-content-' + this.currentView.id + '.viewcontainer') | ||||
document.querySelectorAll('#app-content .viewcontainer').forEach(el => { | document.querySelectorAll('#app-content .viewcontainer').forEach(el => { | ||||
el.classList.add('hidden') | el.classList.add('hidden') | ||||
logger.debug('Triggering legacy navigation event', params) | logger.debug('Triggering legacy navigation event', params) | ||||
window.jQuery(newAppContent).trigger(new window.jQuery.Event('show', params)) | window.jQuery(newAppContent).trigger(new window.jQuery.Event('show', params)) | ||||
window.jQuery(newAppContent).trigger(new window.jQuery.Event('urlChanged', params)) | window.jQuery(newAppContent).trigger(new window.jQuery.Event('urlChanged', params)) | ||||
} | } | ||||
this.Navigation.setActive(view) | this.Navigation.setActive(view) |
<div id="app-navigation-files" role="navigation"></div> | <div id="app-navigation-files" role="navigation"></div> | ||||
<div class="hidden"> | <div class="hidden"> | ||||
<ul class="with-icon" tabindex="0"> | <ul class="with-icon" tabindex="0"> | ||||
<?php | <?php | ||||
$pinned = 0; | |||||
foreach ($_['navigationItems'] as $item) { | |||||
$pinned = NavigationListElements($item, $l, $pinned); | |||||
} | |||||
$pinned = 0; | |||||
foreach ($_['navigationItems'] as $item) { | |||||
$pinned = NavigationListElements($item, $l, $pinned); | |||||
} | |||||
?> | ?> | ||||
</ul> | </ul> | ||||
</div> | </div> |
<?php /** @var \OCP\IL10N $l */ ?> | <?php /** @var \OCP\IL10N $l */ ?> | ||||
<?php $_['appNavigation']->printPage(); ?> | <?php $_['appNavigation']->printPage(); ?> | ||||
<!-- New files vue container --> | |||||
<div id="app-content-vue" class="hidden"></div> | |||||
<div id="app-content" tabindex="0"> | <div id="app-content" tabindex="0"> | ||||
<input type="checkbox" class="hidden-visually" id="showgridview" | <input type="checkbox" class="hidden-visually" id="showgridview" | ||||
<label id="view-toggle" for="showgridview" tabindex="0" class="button <?php p($_['showgridview'] ? 'icon-toggle-filelist' : 'icon-toggle-pictures') ?>" | <label id="view-toggle" for="showgridview" tabindex="0" class="button <?php p($_['showgridview'] ? 'icon-toggle-filelist' : 'icon-toggle-pictures') ?>" | ||||
title="<?php p($_['showgridview'] ? $l->t('Show list view') : $l->t('Show grid view'))?>"></label> | title="<?php p($_['showgridview'] ? $l->t('Show list view') : $l->t('Show grid view'))?>"></label> | ||||
<!-- New files vue container --> | |||||
<div id="app-content-vue" class="hidden"></div> | |||||
<!-- Legacy views --> | <!-- Legacy views --> | ||||
<?php foreach ($_['appContents'] as $content) { ?> | <?php foreach ($_['appContents'] as $content) { ?> |
'OCA\\Files_Trashbin\\Expiration' => $baseDir . '/../lib/Expiration.php', | 'OCA\\Files_Trashbin\\Expiration' => $baseDir . '/../lib/Expiration.php', | ||||
'OCA\\Files_Trashbin\\Helper' => $baseDir . '/../lib/Helper.php', | 'OCA\\Files_Trashbin\\Helper' => $baseDir . '/../lib/Helper.php', | ||||
'OCA\\Files_Trashbin\\Hooks' => $baseDir . '/../lib/Hooks.php', | 'OCA\\Files_Trashbin\\Hooks' => $baseDir . '/../lib/Hooks.php', | ||||
'OCA\\Files_Trashbin\\Listeners\\LoadAdditionalScripts' => $baseDir . '/../lib/Listeners/LoadAdditionalScripts.php', | |||||
'OCA\\Files_Trashbin\\Migration\\Version1010Date20200630192639' => $baseDir . '/../lib/Migration/Version1010Date20200630192639.php', | 'OCA\\Files_Trashbin\\Migration\\Version1010Date20200630192639' => $baseDir . '/../lib/Migration/Version1010Date20200630192639.php', | ||||
'OCA\\Files_Trashbin\\Sabre\\AbstractTrash' => $baseDir . '/../lib/Sabre/AbstractTrash.php', | 'OCA\\Files_Trashbin\\Sabre\\AbstractTrash' => $baseDir . '/../lib/Sabre/AbstractTrash.php', | ||||
'OCA\\Files_Trashbin\\Sabre\\AbstractTrashFile' => $baseDir . '/../lib/Sabre/AbstractTrashFile.php', | 'OCA\\Files_Trashbin\\Sabre\\AbstractTrashFile' => $baseDir . '/../lib/Sabre/AbstractTrashFile.php', |
'OCA\\Files_Trashbin\\Expiration' => __DIR__ . '/..' . '/../lib/Expiration.php', | 'OCA\\Files_Trashbin\\Expiration' => __DIR__ . '/..' . '/../lib/Expiration.php', | ||||
'OCA\\Files_Trashbin\\Helper' => __DIR__ . '/..' . '/../lib/Helper.php', | 'OCA\\Files_Trashbin\\Helper' => __DIR__ . '/..' . '/../lib/Helper.php', | ||||
'OCA\\Files_Trashbin\\Hooks' => __DIR__ . '/..' . '/../lib/Hooks.php', | 'OCA\\Files_Trashbin\\Hooks' => __DIR__ . '/..' . '/../lib/Hooks.php', | ||||
'OCA\\Files_Trashbin\\Listeners\\LoadAdditionalScripts' => __DIR__ . '/..' . '/../lib/Listeners/LoadAdditionalScripts.php', | |||||
'OCA\\Files_Trashbin\\Migration\\Version1010Date20200630192639' => __DIR__ . '/..' . '/../lib/Migration/Version1010Date20200630192639.php', | 'OCA\\Files_Trashbin\\Migration\\Version1010Date20200630192639' => __DIR__ . '/..' . '/../lib/Migration/Version1010Date20200630192639.php', | ||||
'OCA\\Files_Trashbin\\Sabre\\AbstractTrash' => __DIR__ . '/..' . '/../lib/Sabre/AbstractTrash.php', | 'OCA\\Files_Trashbin\\Sabre\\AbstractTrash' => __DIR__ . '/..' . '/../lib/Sabre/AbstractTrash.php', | ||||
'OCA\\Files_Trashbin\\Sabre\\AbstractTrashFile' => __DIR__ . '/..' . '/../lib/Sabre/AbstractTrashFile.php', | 'OCA\\Files_Trashbin\\Sabre\\AbstractTrashFile' => __DIR__ . '/..' . '/../lib/Sabre/AbstractTrashFile.php', |
namespace OCA\Files_Trashbin\AppInfo; | namespace OCA\Files_Trashbin\AppInfo; | ||||
use OCA\DAV\Connector\Sabre\Principal; | use OCA\DAV\Connector\Sabre\Principal; | ||||
use OCA\Files\Event\LoadAdditionalScriptsEvent; | |||||
use OCA\Files_Trashbin\Capabilities; | use OCA\Files_Trashbin\Capabilities; | ||||
use OCA\Files_Trashbin\Expiration; | use OCA\Files_Trashbin\Expiration; | ||||
use OCA\Files_Trashbin\Listeners\LoadAdditionalScripts; | |||||
use OCA\Files_Trashbin\Trash\ITrashManager; | use OCA\Files_Trashbin\Trash\ITrashManager; | ||||
use OCA\Files_Trashbin\Trash\TrashManager; | use OCA\Files_Trashbin\Trash\TrashManager; | ||||
use OCA\Files_Trashbin\UserMigration\TrashbinMigrator; | use OCA\Files_Trashbin\UserMigration\TrashbinMigrator; | ||||
$context->registerServiceAlias('principalBackend', Principal::class); | $context->registerServiceAlias('principalBackend', Principal::class); | ||||
$context->registerUserMigrator(TrashbinMigrator::class); | $context->registerUserMigrator(TrashbinMigrator::class); | ||||
$context->registerEventListener( | |||||
LoadAdditionalScriptsEvent::class, | |||||
LoadAdditionalScripts::class | |||||
); | |||||
} | } | ||||
public function boot(IBootContext $context): void { | public function boot(IBootContext $context): void { | ||||
\OCP\Util::connectHook('OC_Filesystem', 'post_write', 'OCA\Files_Trashbin\Hooks', 'post_write_hook'); | \OCP\Util::connectHook('OC_Filesystem', 'post_write', 'OCA\Files_Trashbin\Hooks', 'post_write_hook'); | ||||
// pre and post-rename, disable trash logic for the copy+unlink case | // pre and post-rename, disable trash logic for the copy+unlink case | ||||
\OCP\Util::connectHook('OC_Filesystem', 'delete', 'OCA\Files_Trashbin\Trashbin', 'ensureFileScannedHook'); | \OCP\Util::connectHook('OC_Filesystem', 'delete', 'OCA\Files_Trashbin\Trashbin', 'ensureFileScannedHook'); | ||||
\OCA\Files\App::getNavigationManager()->add(function () { | |||||
$l = \OC::$server->getL10N(self::APP_ID); | |||||
return [ | |||||
'id' => 'trashbin', | |||||
'appname' => self::APP_ID, | |||||
'script' => 'list.php', | |||||
'order' => 50, | |||||
'name' => $l->t('Deleted files'), | |||||
'classes' => 'pinned', | |||||
]; | |||||
}); | |||||
} | } | ||||
public function registerTrashBackends(IServerContainer $serverContainer, ILogger $logger, IAppManager $appManager, ITrashManager $trashManager) { | public function registerTrashBackends(IServerContainer $serverContainer, ILogger $logger, IAppManager $appManager, ITrashManager $trashManager) { |
<?php | |||||
declare(strict_types=1); | |||||
/** | |||||
* @copyright Copyright (c) 2022, John Molakvoæ <skjnldsv@protonmail.com> | |||||
* | |||||
* @author John Molakvoæ <skjnldsv@protonmail.com> | |||||
* | |||||
* @license GNU AGPL version 3 or any later version | |||||
* | |||||
* 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/>. | |||||
* | |||||
*/ | |||||
namespace OCA\Files_Trashbin\Listeners; | |||||
use OCA\Files_Trashbin\AppInfo\Application; | |||||
use OCA\Files\Event\LoadAdditionalScriptsEvent; | |||||
use OCP\EventDispatcher\Event; | |||||
use OCP\EventDispatcher\IEventListener; | |||||
use OCP\Util; | |||||
class LoadAdditionalScripts implements IEventListener { | |||||
public function handle(Event $event): void { | |||||
if (!($event instanceof LoadAdditionalScriptsEvent)) { | |||||
return; | |||||
} | |||||
Util::addScript(Application::APP_ID, 'main'); | |||||
} | |||||
} |
/** | |||||
* @copyright Copyright (c) 2023 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 NavigationService from '../../files/src/services/Navigation' | |||||
import { translate as t } from '@nextcloud/l10n' | |||||
import DeleteSvg from '@mdi/svg/svg/delete.svg?raw' | |||||
import getContents from './services/trashbin' | |||||
const Navigation = window.OCP.Files.Navigation as NavigationService | |||||
Navigation.register({ | |||||
id: 'trashbin', | |||||
name: t('files_trashbin', 'Deleted files'), | |||||
icon: DeleteSvg, | |||||
order: 50, | |||||
sticky: true, | |||||
getContents, | |||||
}) |
/** | |||||
* @copyright Copyright (c) 2023 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 { createClient } from 'webdav' | |||||
import { generateRemoteUrl } from '@nextcloud/router' | |||||
import { getCurrentUser, getRequestToken } from '@nextcloud/auth' | |||||
export const rootPath = `/trashbin/${getCurrentUser()?.uid}/trash` | |||||
export const rootUrl = generateRemoteUrl('dav' + rootPath) | |||||
const client = createClient(rootUrl, { | |||||
headers: { | |||||
requesttoken: getRequestToken(), | |||||
}, | |||||
}) | |||||
export default client |
/** | |||||
* @copyright Copyright (c) 2023 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/>. | |||||
* | |||||
*/ | |||||
/* eslint-disable */ | |||||
import { getCurrentUser } from '@nextcloud/auth' | |||||
import { File, Folder, parseWebdavPermissions } from '@nextcloud/files' | |||||
import { generateRemoteUrl } from '@nextcloud/router' | |||||
import type { FileStat, ResponseDataDetailed } from 'webdav' | |||||
import type { ContentsWithRoot } from '../../../files/src/services/Navigation' | |||||
import client, { rootPath } from './client' | |||||
const data = `<?xml version="1.0"?> | |||||
<d:propfind xmlns:d="DAV:" | |||||
xmlns:oc="http://owncloud.org/ns" | |||||
xmlns:nc="http://nextcloud.org/ns"> | |||||
<d:prop> | |||||
<nc:trashbin-filename /> | |||||
<nc:trashbin-deletion-time /> | |||||
<nc:trashbin-original-location /> | |||||
<nc:trashbin-title /> | |||||
<d:getlastmodified /> | |||||
<d:getetag /> | |||||
<d:getcontenttype /> | |||||
<d:resourcetype /> | |||||
<oc:fileid /> | |||||
<oc:permissions /> | |||||
<oc:size /> | |||||
<d:getcontentlength /> | |||||
</d:prop> | |||||
</d:propfind>` | |||||
const resultToNode = function(node: FileStat): File | Folder { | |||||
const permissions = parseWebdavPermissions(node.props?.permissions) | |||||
const owner = getCurrentUser()?.uid as string | |||||
const nodeData = { | |||||
id: node.props?.fileid as number || 0, | |||||
source: generateRemoteUrl('dav' + rootPath + node.filename), | |||||
mtime: new Date(node.lastmod), | |||||
mime: node.mime as string, | |||||
size: node.props?.size as number || 0, | |||||
permissions, | |||||
owner, | |||||
root: rootPath, | |||||
attributes: { | |||||
...node, | |||||
...node.props, | |||||
// Override displayed name on the list | |||||
displayName: node.props?.['trashbin-filename'], | |||||
}, | |||||
} | |||||
return node.type === 'file' | |||||
? new File(nodeData) | |||||
: new Folder(nodeData) | |||||
} | |||||
export default async (path: string = '/'): Promise<ContentsWithRoot> => { | |||||
// TODO: use only one request when webdav-client supports it | |||||
// @see https://github.com/perry-mitchell/webdav-client/pull/334 | |||||
const rootResponse = await client.stat(path, { | |||||
details: true, | |||||
data, | |||||
}) as ResponseDataDetailed<FileStat> | |||||
const contentsResponse = await client.getDirectoryContents(path, { | |||||
details: true, | |||||
data, | |||||
}) as ResponseDataDetailed<FileStat[]> | |||||
return { | |||||
folder: resultToNode(rootResponse.data) as Folder, | |||||
contents: contentsResponse.data.map(resultToNode), | |||||
} | |||||
} |
/* | |||||
* Copyright (c) 2014 | |||||
* | |||||
* This file is licensed under the Affero General Public License version 3 | |||||
* or later. | |||||
* | |||||
* See the COPYING-README file. | |||||
* | |||||
*/ | |||||
#app-content-trashbin tbody tr[data-type="file"] td a.name, | |||||
#app-content-trashbin tbody tr[data-type="file"] td a.name span.nametext, | |||||
#app-content-trashbin tbody tr[data-type="file"] td a.name span.nametext span { | |||||
cursor: default; | |||||
} | |||||
#app-content-trashbin .summary :last-child { | |||||
padding: 0; | |||||
} | |||||
#app-content-trashbin .files-filestable .summary .filesize { | |||||
display: none; | |||||
} | |||||
/** | |||||
* @copyright 2014 Vincent Petry <pvince81@owncloud.com> | |||||
* | |||||
* @author Vincent Petry <vincent@nextcloud.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/>. | |||||
* | |||||
*/ | |||||
describe('OCA.Trashbin.App tests', function() { | |||||
var App = OCA.Trashbin.App; | |||||
beforeEach(function() { | |||||
$('#testArea').append( | |||||
'<div id="app-navigation">' + | |||||
'<ul><li data-id="files"><a>Files</a></li>' + | |||||
'<li data-id="trashbin"><a>Trashbin</a></li>' + | |||||
'</div>' + | |||||
'<div id="app-content">' + | |||||
'<div id="app-content-files" class="hidden">' + | |||||
'</div>' + | |||||
'<div id="app-content-trashbin" class="hidden">' + | |||||
'</div>' + | |||||
'</div>' + | |||||
'</div>' | |||||
); | |||||
App.initialize($('#app-content-trashbin')); | |||||
}); | |||||
afterEach(function() { | |||||
App._initialized = false; | |||||
App.fileList = null; | |||||
}); | |||||
describe('initialization', function() { | |||||
it('creates a custom filelist instance', function() { | |||||
App.initialize(); | |||||
expect(App.fileList).toBeDefined(); | |||||
expect(App.fileList.$el.is('#app-content-trashbin')).toEqual(true); | |||||
}); | |||||
it('registers custom file actions', function() { | |||||
var fileActions; | |||||
App.initialize(); | |||||
fileActions = App.fileList.fileActions; | |||||
expect(fileActions.actions.all).toBeDefined(); | |||||
expect(fileActions.actions.all.Restore).toBeDefined(); | |||||
expect(fileActions.actions.all.Delete).toBeDefined(); | |||||
expect(fileActions.actions.all.Rename).not.toBeDefined(); | |||||
expect(fileActions.actions.all.Download).not.toBeDefined(); | |||||
expect(fileActions.defaults.dir).toEqual('Open'); | |||||
}); | |||||
}); | |||||
}); |
/** | |||||
* @copyright 2014 Vincent Petry <pvince81@owncloud.com> | |||||
* | |||||
* @author Abijeet <abijeetpatro@gmail.com> | |||||
* @author Christoph Wurst <christoph@winzerhof-wurst.at> | |||||
* @author Jan C. Borchardt <hey@jancborchardt.net> | |||||
* @author Jan-Christoph Borchardt <hey@jancborchardt.net> | |||||
* @author John Molakvoæ <skjnldsv@protonmail.com> | |||||
* @author Robin Appelman <robin@icewind.nl> | |||||
* @author Vincent Petry <vincent@nextcloud.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/>. | |||||
* | |||||
*/ | |||||
describe('OCA.Trashbin.FileList tests', function () { | |||||
var testFiles, alertStub, notificationStub, fileList, client; | |||||
beforeEach(function () { | |||||
alertStub = sinon.stub(OC.dialogs, 'alert'); | |||||
notificationStub = sinon.stub(OC.Notification, 'show'); | |||||
client = new OC.Files.Client({ | |||||
host: 'localhost', | |||||
port: 80, | |||||
root: '/remote.php/dav/trashbin/user', | |||||
useHTTPS: OC.getProtocol() === 'https' | |||||
}); | |||||
// init parameters and test table elements | |||||
$('#testArea').append( | |||||
'<div id="app-content">' + | |||||
// set this but it shouldn't be used (could be the one from the | |||||
// files app) | |||||
'<input type="hidden" id="permissions" value="31"></input>' + | |||||
// dummy controls | |||||
'<div class="files-controls">' + | |||||
' <div class="actions creatable"></div>' + | |||||
' <div class="notCreatable"></div>' + | |||||
'</div>' + | |||||
// dummy table | |||||
// TODO: at some point this will be rendered by the fileList class itself! | |||||
'<table class="files-filestable list-container view-grid">' + | |||||
'<thead><tr><th class="hidden column-name">' + | |||||
'<input type="checkbox" id="select_all_trash" class="select-all">' + | |||||
'<span class="name">Name</span>' + | |||||
'<span class="selectedActions hidden">' + | |||||
'<a href="" class="actions-selected"><span class="icon icon-more"></span><span>Actions</span>' + | |||||
'</span>' + | |||||
'</th></tr></thead>' + | |||||
'<tbody class="files-fileList"></tbody>' + | |||||
'<tfoot></tfoot>' + | |||||
'</table>' + | |||||
'<div class="emptyfilelist emptycontent">Empty content message</div>' + | |||||
'</div>' | |||||
); | |||||
testFiles = [{ | |||||
id: 1, | |||||
type: 'file', | |||||
name: 'One.txt.d11111', | |||||
displayName: 'One.txt', | |||||
mtime: 11111000, | |||||
mimetype: 'text/plain', | |||||
etag: 'abc' | |||||
}, { | |||||
id: 2, | |||||
type: 'file', | |||||
name: 'Two.jpg.d22222', | |||||
displayName: 'Two.jpg', | |||||
mtime: 22222000, | |||||
mimetype: 'image/jpeg', | |||||
etag: 'def', | |||||
}, { | |||||
id: 3, | |||||
type: 'file', | |||||
name: 'Three.pdf.d33333', | |||||
displayName: 'Three.pdf', | |||||
mtime: 33333000, | |||||
mimetype: 'application/pdf', | |||||
etag: '123', | |||||
}, { | |||||
id: 4, | |||||
type: 'dir', | |||||
mtime: 99999000, | |||||
name: 'somedir.d99999', | |||||
displayName: 'somedir', | |||||
mimetype: 'httpd/unix-directory', | |||||
etag: '456' | |||||
}]; | |||||
// register file actions like the trashbin App does | |||||
var fileActions = OCA.Trashbin.App._createFileActions(fileList); | |||||
fileList = new OCA.Trashbin.FileList( | |||||
$('#app-content'), { | |||||
fileActions: fileActions, | |||||
multiSelectMenu: [{ | |||||
name: 'restore', | |||||
displayName: t('files', 'Restore'), | |||||
iconClass: 'icon-history', | |||||
}, | |||||
{ | |||||
name: 'delete', | |||||
displayName: t('files', 'Delete'), | |||||
iconClass: 'icon-delete', | |||||
} | |||||
], | |||||
client: client | |||||
} | |||||
); | |||||
}); | |||||
afterEach(function () { | |||||
testFiles = undefined; | |||||
fileList.destroy(); | |||||
fileList = undefined; | |||||
notificationStub.restore(); | |||||
alertStub.restore(); | |||||
}); | |||||
describe('Initialization', function () { | |||||
it('Sorts by mtime by default', function () { | |||||
expect(fileList._sort).toEqual('mtime'); | |||||
expect(fileList._sortDirection).toEqual('desc'); | |||||
}); | |||||
it('Always returns read and delete permission', function () { | |||||
expect(fileList.getDirectoryPermissions()).toEqual(OC.PERMISSION_READ | OC.PERMISSION_DELETE); | |||||
}); | |||||
}); | |||||
describe('Breadcrumbs', function () { | |||||
beforeEach(function () { | |||||
var data = { | |||||
status: 'success', | |||||
data: { | |||||
files: testFiles, | |||||
permissions: 1 | |||||
} | |||||
}; | |||||
fakeServer.respondWith(/\/index\.php\/apps\/files_trashbin\/ajax\/list.php\?dir=%2Fsubdir/, [ | |||||
200, { | |||||
"Content-Type": "application/json" | |||||
}, | |||||
JSON.stringify(data) | |||||
]); | |||||
}); | |||||
it('links the breadcrumb to the trashbin view', function () { | |||||
fileList.changeDirectory('/subdir', false, true); | |||||
fakeServer.respond(); | |||||
var $crumbs = fileList.$el.find('.files-controls .crumb'); | |||||
expect($crumbs.length).toEqual(3); | |||||
expect($crumbs.eq(1).find('a').text()).toEqual('Home'); | |||||
expect($crumbs.eq(1).find('a').attr('href')) | |||||
.toEqual(OC.getRootPath() + '/index.php/apps/files?view=trashbin&dir=/'); | |||||
expect($crumbs.eq(2).find('a').text()).toEqual('subdir'); | |||||
expect($crumbs.eq(2).find('a').attr('href')) | |||||
.toEqual(OC.getRootPath() + '/index.php/apps/files?view=trashbin&dir=/subdir'); | |||||
}); | |||||
}); | |||||
describe('Rendering rows', function () { | |||||
it('renders rows with the correct data when in root', function () { | |||||
// dir listing is false when in root | |||||
fileList.setFiles(testFiles); | |||||
var $rows = fileList.$el.find('tbody tr'); | |||||
var $tr = $rows.eq(0); | |||||
expect($rows.length).toEqual(4); | |||||
expect($tr.attr('data-id')).toEqual('1'); | |||||
expect($tr.attr('data-type')).toEqual('file'); | |||||
expect($tr.attr('data-file')).toEqual('One.txt.d11111'); | |||||
expect($tr.attr('data-size')).not.toBeDefined(); | |||||
expect($tr.attr('data-etag')).toEqual('abc'); | |||||
expect($tr.attr('data-permissions')).toEqual('9'); // read and delete | |||||
expect($tr.attr('data-mime')).toEqual('text/plain'); | |||||
expect($tr.attr('data-mtime')).toEqual('11111000'); | |||||
expect($tr.find('a.name').attr('href')).toEqual('#'); | |||||
expect($tr.find('.nametext').text().trim()).toEqual('One.txt'); | |||||
expect(fileList.findFileEl('One.txt.d11111')[0]).toEqual($tr[0]); | |||||
}); | |||||
it('renders rows with the correct data when in root after calling setFiles with the same data set', function () { | |||||
// dir listing is false when in root | |||||
fileList.setFiles(testFiles); | |||||
fileList.setFiles(fileList.files); | |||||
var $rows = fileList.$el.find('tbody tr'); | |||||
var $tr = $rows.eq(0); | |||||
expect($rows.length).toEqual(4); | |||||
expect($tr.attr('data-id')).toEqual('1'); | |||||
expect($tr.attr('data-type')).toEqual('file'); | |||||
expect($tr.attr('data-file')).toEqual('One.txt.d11111'); | |||||
expect($tr.attr('data-size')).not.toBeDefined(); | |||||
expect($tr.attr('data-etag')).toEqual('abc'); | |||||
expect($tr.attr('data-permissions')).toEqual('9'); // read and delete | |||||
expect($tr.attr('data-mime')).toEqual('text/plain'); | |||||
expect($tr.attr('data-mtime')).toEqual('11111000'); | |||||
expect($tr.find('a.name').attr('href')).toEqual('#'); | |||||
expect($tr.find('.nametext').text().trim()).toEqual('One.txt'); | |||||
expect(fileList.findFileEl('One.txt.d11111')[0]).toEqual($tr[0]); | |||||
}); | |||||
it('renders rows with the correct data when in subdirectory', function () { | |||||
fileList.setFiles(testFiles.map(function (file) { | |||||
file.name = file.displayName; | |||||
return file; | |||||
})); | |||||
var $rows = fileList.$el.find('tbody tr'); | |||||
var $tr = $rows.eq(0); | |||||
expect($rows.length).toEqual(4); | |||||
expect($tr.attr('data-id')).toEqual('1'); | |||||
expect($tr.attr('data-type')).toEqual('file'); | |||||
expect($tr.attr('data-file')).toEqual('One.txt'); | |||||
expect($tr.attr('data-size')).not.toBeDefined(); | |||||
expect($tr.attr('data-etag')).toEqual('abc'); | |||||
expect($tr.attr('data-permissions')).toEqual('9'); // read and delete | |||||
expect($tr.attr('data-mime')).toEqual('text/plain'); | |||||
expect($tr.attr('data-mtime')).toEqual('11111000'); | |||||
expect($tr.find('a.name').attr('href')).toEqual('#'); | |||||
expect($tr.find('.nametext').text().trim()).toEqual('One.txt'); | |||||
expect(fileList.findFileEl('One.txt')[0]).toEqual($tr[0]); | |||||
}); | |||||
it('does not render a size column', function () { | |||||
expect(fileList.$el.find('tbody tr .filesize').length).toEqual(0); | |||||
}); | |||||
}); | |||||
describe('File actions', function () { | |||||
describe('Deleting single files', function () { | |||||
// TODO: checks ajax call | |||||
// TODO: checks spinner | |||||
// TODO: remove item after delete | |||||
// TODO: bring back item if delete failed | |||||
}); | |||||
describe('Restoring single files', function () { | |||||
// TODO: checks ajax call | |||||
// TODO: checks spinner | |||||
// TODO: remove item after restore | |||||
// TODO: bring back item if restore failed | |||||
}); | |||||
}); | |||||
describe('file previews', function () { | |||||
// TODO: check that preview URL is going through files_trashbin | |||||
}); | |||||
describe('loading file list', function () { | |||||
// TODO: check that ajax URL is going through files_trashbin | |||||
}); | |||||
describe('breadcrumbs', function () { | |||||
// TODO: test label + URL | |||||
}); | |||||
describe('elementToFile', function () { | |||||
var $tr; | |||||
beforeEach(function () { | |||||
fileList.setFiles(testFiles); | |||||
$tr = fileList.findFileEl('One.txt.d11111'); | |||||
}); | |||||
it('converts data attributes to file info structure', function () { | |||||
var fileInfo = fileList.elementToFile($tr); | |||||
expect(fileInfo.id).toEqual(1); | |||||
expect(fileInfo.name).toEqual('One.txt.d11111'); | |||||
expect(fileInfo.displayName).toEqual('One.txt'); | |||||
expect(fileInfo.mtime).toEqual(11111000); | |||||
expect(fileInfo.etag).toEqual('abc'); | |||||
expect(fileInfo.permissions).toEqual(OC.PERMISSION_READ | OC.PERMISSION_DELETE); | |||||
expect(fileInfo.mimetype).toEqual('text/plain'); | |||||
expect(fileInfo.type).toEqual('file'); | |||||
}); | |||||
}); | |||||
describe('Global Actions', function () { | |||||
beforeEach(function () { | |||||
fileList.setFiles(testFiles); | |||||
fileList.findFileEl('One.txt.d11111').find('input:checkbox').click(); | |||||
fileList.findFileEl('Three.pdf.d33333').find('input:checkbox').click(); | |||||
fileList.findFileEl('somedir.d99999').find('input:checkbox').click(); | |||||
fileList.$el.find('.actions-selected').click(); | |||||
}); | |||||
afterEach(function () { | |||||
fileList.$el.find('.actions-selected').click(); | |||||
}); | |||||
describe('Delete', function () { | |||||
it('Shows trashbin actions', function () { | |||||
// visible because a few files were selected | |||||
expect($('.selectedActions').is(':visible')).toEqual(true); | |||||
expect($('.selectedActions .item-delete').is(':visible')).toEqual(true); | |||||
expect($('.selectedActions .item-restore').is(':visible')).toEqual(true); | |||||
// check | |||||
fileList.$el.find('.select-all').click(); | |||||
// stays visible | |||||
expect($('.selectedActions').is(':visible')).toEqual(true); | |||||
expect($('.selectedActions .item-delete').is(':visible')).toEqual(true); | |||||
expect($('.selectedActions .item-restore').is(':visible')).toEqual(true); | |||||
// uncheck | |||||
fileList.$el.find('.select-all').click(); | |||||
// becomes hidden now | |||||
expect($('.selectedActions').is(':visible')).toEqual(false); | |||||
expect($('.selectedActions .item-delete').is(':visible')).toEqual(false); | |||||
expect($('.selectedActions .item-restore').is(':visible')).toEqual(false); | |||||
}); | |||||
it('Deletes selected files when "Delete" clicked', function (done) { | |||||
var request; | |||||
var promise = fileList._onClickDeleteSelected({ | |||||
preventDefault: function () { | |||||
} | |||||
}); | |||||
var files = ["One.txt.d11111", "Three.pdf.d33333", "somedir.d99999"]; | |||||
expect(fakeServer.requests.length).toEqual(files.length); | |||||
for (var i = 0; i < files.length; i++) { | |||||
request = fakeServer.requests[i]; | |||||
expect(request.url).toEqual(OC.getRootPath() + '/remote.php/dav/trashbin/user/trash/' + files[i]); | |||||
request.respond(200); | |||||
} | |||||
return promise.then(function () { | |||||
expect(fileList.findFileEl('One.txt.d11111').length).toEqual(0); | |||||
expect(fileList.findFileEl('Three.pdf.d33333').length).toEqual(0); | |||||
expect(fileList.findFileEl('somedir.d99999').length).toEqual(0); | |||||
expect(fileList.findFileEl('Two.jpg.d22222').length).toEqual(1); | |||||
}).then(done, done); | |||||
}); | |||||
it('Deletes all files when all selected when "Delete" clicked', function (done) { | |||||
var request; | |||||
$('.select-all').click(); | |||||
var promise = fileList._onClickDeleteSelected({ | |||||
preventDefault: function () { | |||||
} | |||||
}); | |||||
expect(fakeServer.requests.length).toEqual(1); | |||||
request = fakeServer.requests[0]; | |||||
expect(request.url).toEqual(OC.getRootPath() + '/remote.php/dav/trashbin/user/trash'); | |||||
request.respond(200); | |||||
return promise.then(function () { | |||||
expect(fileList.isEmpty).toEqual(true); | |||||
}).then(done, done); | |||||
}); | |||||
}); | |||||
describe('Restore', function () { | |||||
it('Restores selected files when "Restore" clicked', function (done) { | |||||
var request; | |||||
var promise = fileList._onClickRestoreSelected({ | |||||
preventDefault: function () { | |||||
} | |||||
}); | |||||
var files = ["One.txt.d11111", "Three.pdf.d33333", "somedir.d99999"]; | |||||
expect(fakeServer.requests.length).toEqual(files.length); | |||||
for (var i = 0; i < files.length; i++) { | |||||
request = fakeServer.requests[i]; | |||||
expect(request.url).toEqual(OC.getRootPath() + '/remote.php/dav/trashbin/user/trash/' + files[i]); | |||||
expect(request.requestHeaders.Destination).toEqual(OC.getRootPath() + '/remote.php/dav/trashbin/user/restore/' + files[i]); | |||||
request.respond(200); | |||||
} | |||||
return promise.then(function() { | |||||
expect(fileList.findFileEl('One.txt.d11111').length).toEqual(0); | |||||
expect(fileList.findFileEl('Three.pdf.d33333').length).toEqual(0); | |||||
expect(fileList.findFileEl('somedir.d99999').length).toEqual(0); | |||||
expect(fileList.findFileEl('Two.jpg.d22222').length).toEqual(1); | |||||
}).then(done, done); | |||||
}); | |||||
it('Restores all files when all selected when "Restore" clicked', function (done) { | |||||
var request; | |||||
$('.select-all').click(); | |||||
var promise = fileList._onClickRestoreSelected({ | |||||
preventDefault: function () { | |||||
} | |||||
}); | |||||
var files = ["One.txt.d11111", "Two.jpg.d22222", "Three.pdf.d33333", "somedir.d99999"]; | |||||
expect(fakeServer.requests.length).toEqual(files.length); | |||||
for (var i = 0; i < files.length; i++) { | |||||
request = fakeServer.requests[i]; | |||||
expect(request.url).toEqual(OC.getRootPath() + '/remote.php/dav/trashbin/user/trash/' + files[i]); | |||||
expect(request.requestHeaders.Destination).toEqual(OC.getRootPath() + '/remote.php/dav/trashbin/user/restore/' + files[i]); | |||||
request.respond(200); | |||||
} | |||||
return promise.then(function() { | |||||
expect(fileList.isEmpty).toEqual(true); | |||||
}).then(done, done); | |||||
}); | |||||
}); | |||||
}); | |||||
}); |
'@babel/preset-env', | '@babel/preset-env', | ||||
{ | { | ||||
useBuiltIns: false, | useBuiltIns: false, | ||||
modules: 'auto', | |||||
}, | }, | ||||
], | ], | ||||
], | ], |
/** | |||||
* @copyright Bernhard Posselt 2014 | |||||
* | |||||
* @author Christoph Wurst <christoph@winzerhof-wurst.at> | |||||
* @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 $ from 'jquery' | |||||
let dynamicSlideToggleEnabled = false | |||||
const Apps = { | |||||
enableDynamicSlideToggle() { | |||||
dynamicSlideToggleEnabled = true | |||||
}, | |||||
} | |||||
/** | |||||
* Shows the #app-sidebar and add .with-app-sidebar to subsequent siblings | |||||
* | |||||
* @param {object} [$el] sidebar element to show, defaults to $('#app-sidebar') | |||||
*/ | |||||
Apps.showAppSidebar = function($el) { | |||||
const $appSidebar = $el || $('#app-sidebar') | |||||
$appSidebar.removeClass('disappear').show() | |||||
$('#app-content').trigger(new $.Event('appresized')) | |||||
} | |||||
/** | |||||
* Shows the #app-sidebar and removes .with-app-sidebar from subsequent | |||||
* siblings | |||||
* | |||||
* @param {object} [$el] sidebar element to hide, defaults to $('#app-sidebar') | |||||
*/ | |||||
Apps.hideAppSidebar = function($el) { | |||||
const $appSidebar = $el || $('#app-sidebar') | |||||
$appSidebar.hide().addClass('disappear') | |||||
$('#app-content').trigger(new $.Event('appresized')) | |||||
} | |||||
/** | |||||
* Provides a way to slide down a target area through a button and slide it | |||||
* up if the user clicks somewhere else. Used for the news app settings and | |||||
* add new field. | |||||
* | |||||
* Usage: | |||||
* <button data-apps-slide-toggle=".slide-area">slide</button> | |||||
* <div class=".slide-area" class="hidden">I'm sliding up</div> | |||||
*/ | |||||
export const registerAppsSlideToggle = () => { | |||||
let buttons = $('[data-apps-slide-toggle]') | |||||
if (buttons.length === 0) { | |||||
$('#app-navigation').addClass('without-app-settings') | |||||
} | |||||
$(document).click(function(event) { | |||||
if (dynamicSlideToggleEnabled) { | |||||
buttons = $('[data-apps-slide-toggle]') | |||||
} | |||||
buttons.each(function(index, button) { | |||||
const areaSelector = $(button).data('apps-slide-toggle') | |||||
const area = $(areaSelector) | |||||
/** | |||||
* | |||||
*/ | |||||
function hideArea() { | |||||
area.slideUp(OC.menuSpeed * 4, function() { | |||||
area.trigger(new $.Event('hide')) | |||||
}) | |||||
area.removeClass('opened') | |||||
$(button).removeClass('opened') | |||||
} | |||||
/** | |||||
* | |||||
*/ | |||||
function showArea() { | |||||
area.slideDown(OC.menuSpeed * 4, function() { | |||||
area.trigger(new $.Event('show')) | |||||
}) | |||||
area.addClass('opened') | |||||
$(button).addClass('opened') | |||||
const input = $(areaSelector + ' [autofocus]') | |||||
if (input.length === 1) { | |||||
input.focus() | |||||
} | |||||
} | |||||
// do nothing if the area is animated | |||||
if (!area.is(':animated')) { | |||||
// button toggles the area | |||||
if ($(button).is($(event.target).closest('[data-apps-slide-toggle]'))) { | |||||
if (area.is(':visible')) { | |||||
hideArea() | |||||
} else { | |||||
showArea() | |||||
} | |||||
// all other areas that have not been clicked but are open | |||||
// should be slid up | |||||
} else { | |||||
const closest = $(event.target).closest(areaSelector) | |||||
if (area.is(':visible') && closest[0] !== area[0]) { | |||||
hideArea() | |||||
} | |||||
} | |||||
} | |||||
}) | |||||
}) | |||||
} | |||||
export default Apps |
processAjaxError, | processAjaxError, | ||||
registerXHRForErrorProcessing, | registerXHRForErrorProcessing, | ||||
} from './xhr-error.js' | } from './xhr-error.js' | ||||
import Apps from './apps.js' | |||||
import { AppConfig, appConfig } from './appconfig.js' | import { AppConfig, appConfig } from './appconfig.js' | ||||
import { appSettings } from './appsettings.js' | import { appSettings } from './appsettings.js' | ||||
import appswebroots from './appswebroots.js' | import appswebroots from './appswebroots.js' | ||||
import { | import { | ||||
build as buildQueryString, | build as buildQueryString, | ||||
parse as parseQueryString, | parse as parseQueryString, | ||||
} from './query-string.js' | |||||
import Config from './config.js' | |||||
} from './query-string' | |||||
import Config from './config' | |||||
import { | import { | ||||
coreApps, | coreApps, | ||||
menuSpeed, | menuSpeed, | ||||
PERMISSION_SHARE, | PERMISSION_SHARE, | ||||
PERMISSION_UPDATE, | PERMISSION_UPDATE, | ||||
TAG_FAVORITE, | TAG_FAVORITE, | ||||
} from './constants.js' | |||||
import ContactsMenu from './contactsmenu.js' | |||||
import { currentUser, getCurrentUser } from './currentuser.js' | |||||
import Dialogs from './dialogs.js' | |||||
import EventSource from './eventsource.js' | |||||
import { get, set } from './get_set.js' | |||||
import { getCapabilities } from './capabilities.js' | |||||
} from './constants' | |||||
import ContactsMenu from './contactsmenu' | |||||
import { currentUser, getCurrentUser } from './currentuser' | |||||
import Dialogs from './dialogs' | |||||
import EventSource from './eventsource' | |||||
import { get, set } from './get_set' | |||||
import { getCapabilities } from './capabilities' | |||||
import { | import { | ||||
getHost, | getHost, | ||||
getHostName, | getHostName, | ||||
getPort, | getPort, | ||||
getProtocol, | getProtocol, | ||||
} from './host.js' | |||||
} from './host' | |||||
import { | import { | ||||
getToken as getRequestToken, | getToken as getRequestToken, | ||||
} from './requesttoken.js' | |||||
} from './requesttoken' | |||||
import { | import { | ||||
hideMenus, | hideMenus, | ||||
registerMenu, | registerMenu, | ||||
showMenu, | showMenu, | ||||
unregisterMenu, | unregisterMenu, | ||||
} from './menu.js' | |||||
import { isUserAdmin } from './admin.js' | |||||
import L10N from './l10n.js' | |||||
} from './menu' | |||||
import { isUserAdmin } from './admin' | |||||
import L10N from './l10n' | |||||
import { | import { | ||||
getCanonicalLocale, | getCanonicalLocale, | ||||
getLanguage, | getLanguage, | ||||
addScript, | addScript, | ||||
addStyle, | addStyle, | ||||
Apps, | |||||
AppConfig, | AppConfig, | ||||
appConfig, | appConfig, | ||||
appSettings, | appSettings, |
}, | }, | ||||
_onPopState(e) { | _onPopState(e) { | ||||
debugger | |||||
if (this._cancelPop) { | if (this._cancelPop) { | ||||
this._cancelPop = false | this._cancelPop = false | ||||
return | return |
import './globals.js' | import './globals.js' | ||||
import './jquery/index.js' | import './jquery/index.js' | ||||
import { initCore } from './init.js' | import { initCore } from './init.js' | ||||
import { registerAppsSlideToggle } from './OC/apps.js' | |||||
window.addEventListener('DOMContentLoaded', function() { | window.addEventListener('DOMContentLoaded', function() { | ||||
initCore() | initCore() | ||||
registerAppsSlideToggle() | |||||
// fallback to hashchange when no history support | // fallback to hashchange when no history support | ||||
if (window.history.pushState) { | if (window.history.pushState) { |
/** | /** | ||||
* @copyright Copyright (c) 2016 Roeland Jago Douma <roeland@famdouma.nl> | |||||
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com> | |||||
* | * | ||||
* @author Roeland Jago Douma <roeland@famdouma.nl> | |||||
* @author John Molakvoæ <skjnldsv@protonmail.com> | |||||
* | * | ||||
* @license AGPL-3.0-or-later | * @license AGPL-3.0-or-later | ||||
* | * | ||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | * along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
* | * | ||||
*/ | */ | ||||
declare module '*.svg' { | |||||
const content: any | |||||
export default content | |||||
} | |||||
import './app.js' | |||||
import './filelist.js' | |||||
import './trash.scss' | |||||
declare module '*.vue' { | |||||
import Vue from 'vue' | |||||
export default Vue | |||||
} | |||||
window.OCA.Trashbin = OCA.Trashbin |
/** | |||||
* @copyright Copyright (c) 2023 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/>. | |||||
* | |||||
*/ | |||||
/* eslint-disable */ | |||||
import { mount } from 'cypress/vue2' | |||||
type MountParams = Parameters<typeof mount>; | |||||
type OptionsParam = MountParams[1]; | |||||
declare global { | |||||
namespace Cypress { | |||||
interface Chainable { | |||||
mount: typeof mount; | |||||
} | |||||
} | |||||
} |
* along with this program. If not, see <http://www.gnu.org/licenses/>. | * along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
* | * | ||||
*/ | */ | ||||
/* eslint-disable */ | |||||
import { mount } from 'cypress/vue2' | import { mount } from 'cypress/vue2' | ||||
// Augment the Cypress namespace to include type definitions for | |||||
// your custom command. | |||||
// Alternatively, can be defined in cypress/support/component.d.ts | |||||
// with a <reference path="./component" /> at the top of your spec. | |||||
declare global { | |||||
// eslint-disable-next-line @typescript-eslint/no-namespace | |||||
namespace Cypress { | |||||
interface Chainable { | |||||
mount: typeof mount | |||||
} | |||||
} | |||||
} | |||||
// Example use: | // Example use: | ||||
// cy.mount(MyComponent) | // cy.mount(MyComponent) | ||||
Cypress.Commands.add('mount', (component, optionsOrProps) => { | Cypress.Commands.add('mount', (component, optionsOrProps) => { |
"@nextcloud/capabilities": "^1.0.4", | "@nextcloud/capabilities": "^1.0.4", | ||||
"@nextcloud/dialogs": "^4.0.0-beta.2", | "@nextcloud/dialogs": "^4.0.0-beta.2", | ||||
"@nextcloud/event-bus": "^3.0.2", | "@nextcloud/event-bus": "^3.0.2", | ||||
"@nextcloud/files": "^3.0.0-beta.5", | |||||
"@nextcloud/files": "^3.0.0-beta.7", | |||||
"@nextcloud/initial-state": "^2.0.0", | "@nextcloud/initial-state": "^2.0.0", | ||||
"@nextcloud/l10n": "^2.1.0", | "@nextcloud/l10n": "^2.1.0", | ||||
"@nextcloud/logger": "^2.5.0", | "@nextcloud/logger": "^2.5.0", | ||||
"vue": "^2.7.14", | "vue": "^2.7.14", | ||||
"vue-click-outside": "^1.1.0", | "vue-click-outside": "^1.1.0", | ||||
"vue-cropperjs": "^4.2.0", | "vue-cropperjs": "^4.2.0", | ||||
"vue-fragment": "^1.6.0", | |||||
"vue-infinite-loading": "^2.4.5", | "vue-infinite-loading": "^2.4.5", | ||||
"vue-localstorage": "^0.6.2", | "vue-localstorage": "^0.6.2", | ||||
"vue-material-design-icons": "^5.0.0", | "vue-material-design-icons": "^5.0.0", | ||||
"vue-multiselect": "^2.1.6", | "vue-multiselect": "^2.1.6", | ||||
"vue-router": "^3.6.5", | "vue-router": "^3.6.5", | ||||
"vue-virtual-scroll-list": "github:skjnldsv/vue-virtual-scroll-list#feat/table", | |||||
"vuedraggable": "^2.24.3", | "vuedraggable": "^2.24.3", | ||||
"vuex": "^3.6.2", | "vuex": "^3.6.2", | ||||
"vuex-router-sync": "^5.0.0", | "vuex-router-sync": "^5.0.0", | ||||
"webdav": "^4.11.0" | |||||
"webdav": "^5.0.0-r1" | |||||
}, | }, | ||||
"devDependencies": { | "devDependencies": { | ||||
"@babel/node": "^7.20.7", | "@babel/node": "^7.20.7", | ||||
"sass-loader": "^13.2.0", | "sass-loader": "^13.2.0", | ||||
"sinon": "<= 5.0.7", | "sinon": "<= 5.0.7", | ||||
"style-loader": "^3.3.1", | "style-loader": "^3.3.1", | ||||
"ts-loader": "^9.4.2", | |||||
"ts-node": "^10.9.1", | "ts-node": "^10.9.1", | ||||
"tslib": "^2.4.1", | "tslib": "^2.4.1", | ||||
"typescript": "^4.9.3", | "typescript": "^4.9.3", |
{ | { | ||||
"extends": "@vue/tsconfig/tsconfig.json", | "extends": "@vue/tsconfig/tsconfig.json", | ||||
"include": ["./apps/**/*.ts", "./core/**/*.ts"], | |||||
"include": ["./apps/**/*.ts", "./core/**/*.ts", "./*.d.ts"], | |||||
"compilerOptions": { | "compilerOptions": { | ||||
"types": ["node"], | |||||
"types": ["cypress", "node", "vue"], | |||||
"outDir": "./dist/", | "outDir": "./dist/", | ||||
"target": "ESNext", | "target": "ESNext", | ||||
"module": "esnext", | "module": "esnext", |
alias: { | alias: { | ||||
// make sure to use the handlebar runtime when importing | // make sure to use the handlebar runtime when importing | ||||
handlebars: 'handlebars/runtime', | handlebars: 'handlebars/runtime', | ||||
vue$: path.resolve('./node_modules/vue'), | |||||
}, | }, | ||||
extensions: ['*', '.ts', '.js', '.vue'], | extensions: ['*', '.ts', '.js', '.vue'], | ||||
symlinks: true, | symlinks: true, |
'personal-settings': path.join(__dirname, 'apps/files_sharing/src', 'personal-settings.js'), | 'personal-settings': path.join(__dirname, 'apps/files_sharing/src', 'personal-settings.js'), | ||||
}, | }, | ||||
files_trashbin: { | files_trashbin: { | ||||
files_trashbin: path.join(__dirname, 'apps/files_trashbin/src', 'files_trashbin.js'), | |||||
main: path.join(__dirname, 'apps/files_trashbin/src', 'main.ts'), | |||||
}, | }, | ||||
files_versions: { | files_versions: { | ||||
files_versions: path.join(__dirname, 'apps/files_versions/src', 'files_versions_tab.js'), | files_versions: path.join(__dirname, 'apps/files_versions/src', 'files_versions_tab.js'), |