aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--apps/files/js/app.js5
-rw-r--r--apps/files/src/components/BreadCrumbs.vue58
-rw-r--r--apps/files/src/components/FileEntry.vue134
-rw-r--r--apps/files/src/components/FilesListHeader.vue122
-rw-r--r--apps/files/src/components/FilesListVirtual.vue124
-rw-r--r--apps/files/src/main.js15
-rw-r--r--apps/files/src/mixins/fileslist-row.scss63
-rw-r--r--apps/files/src/services/Navigation.ts31
-rw-r--r--apps/files/src/store/files.ts97
-rw-r--r--apps/files/src/store/index.ts16
-rw-r--r--apps/files/src/store/paths.ts71
-rw-r--r--apps/files/src/store/selection.ts51
-rw-r--r--apps/files/src/types.ts56
-rw-r--r--apps/files/src/views/FilesList.vue318
-rw-r--r--apps/files/src/views/Navigation.vue33
-rw-r--r--apps/files/templates/appnavigation.php10
-rw-r--r--apps/files/templates/index.php6
-rw-r--r--apps/files_trashbin/composer/composer/autoload_classmap.php1
-rw-r--r--apps/files_trashbin/composer/composer/autoload_static.php1
-rw-r--r--apps/files_trashbin/lib/AppInfo/Application.php19
-rw-r--r--apps/files_trashbin/lib/Listeners/LoadAdditionalScripts.php41
-rw-r--r--apps/files_trashbin/src/main.ts39
-rw-r--r--apps/files_trashbin/src/services/client.ts33
-rw-r--r--apps/files_trashbin/src/services/trashbin.ts95
-rw-r--r--apps/files_trashbin/src/trash.scss22
-rw-r--r--apps/files_trashbin/tests/js/appSpec.js70
-rw-r--r--apps/files_trashbin/tests/js/filelistSpec.js397
-rw-r--r--babel.config.js1
-rw-r--r--core/src/OC/apps.js135
-rw-r--r--core/src/OC/index.js30
-rw-r--r--core/src/OC/util-history.js2
-rw-r--r--core/src/main.js2
-rw-r--r--custom.d.ts (renamed from apps/files_trashbin/src/files_trashbin.js)16
-rw-r--r--cypress.d.ts34
-rw-r--r--cypress/support/component.ts16
-rw-r--r--package.json7
-rw-r--r--tsconfig.json4
-rw-r--r--webpack.common.js1
-rw-r--r--webpack.modules.js2
39 files changed, 1475 insertions, 703 deletions
diff --git a/apps/files/js/app.js b/apps/files/js/app.js
index f0c3ac5c212..75967ef5753 100644
--- a/apps/files/js/app.js
+++ b/apps/files/js/app.js
@@ -51,7 +51,6 @@
* Initializes the files app
*/
initialize: function() {
- this.navigation = OCP.Files.Navigation;
this.$showHiddenFiles = $('input#showhiddenfilesToggle');
var showHidden = $('#showHiddenFiles').val() === "1";
this.$showHiddenFiles.prop('checked', showHidden);
@@ -135,8 +134,6 @@
OC.Plugins.attach('OCA.Files.App', this);
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._debouncedPersistCropImagePreviewsState = _.debounce(this._persistCropImagePreviewsState, 1200);
@@ -145,6 +142,8 @@
OCP.WhatsNew.query(); // for Nextcloud server
sessionStorage.setItem('WhatsNewServerCheck', Date.now());
}
+
+ window._nc_event_bus.emit('files:legacy-view:initialized', this);
},
/**
diff --git a/apps/files/src/components/BreadCrumbs.vue b/apps/files/src/components/BreadCrumbs.vue
new file mode 100644
index 00000000000..15fd35667ec
--- /dev/null
+++ b/apps/files/src/components/BreadCrumbs.vue
@@ -0,0 +1,58 @@
+<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>
diff --git a/apps/files/src/components/FileEntry.vue b/apps/files/src/components/FileEntry.vue
new file mode 100644
index 00000000000..de340917b69
--- /dev/null
+++ b/apps/files/src/components/FileEntry.vue
@@ -0,0 +1,134 @@
+<!--
+ - @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>
diff --git a/apps/files/src/components/FilesListHeader.vue b/apps/files/src/components/FilesListHeader.vue
new file mode 100644
index 00000000000..588d86709da
--- /dev/null
+++ b/apps/files/src/components/FilesListHeader.vue
@@ -0,0 +1,122 @@
+<!--
+ - @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>
diff --git a/apps/files/src/components/FilesListVirtual.vue b/apps/files/src/components/FilesListVirtual.vue
new file mode 100644
index 00000000000..9228179a96c
--- /dev/null
+++ b/apps/files/src/components/FilesListVirtual.vue
@@ -0,0 +1,124 @@
+<!--
+ - @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>
diff --git a/apps/files/src/main.js b/apps/files/src/main.js
index 3099a4c619c..3d1c88755f0 100644
--- a/apps/files/src/main.js
+++ b/apps/files/src/main.js
@@ -4,12 +4,15 @@ import processLegacyFilesViews from './legacy/navigationMapper.js'
import Vue from 'vue'
import NavigationService from './services/Navigation.ts'
+
import NavigationView from './views/Navigation.vue'
+import FilesListView from './views/FilesList.vue'
import SettingsService from './services/Settings.js'
import SettingsModel from './models/Setting.js'
import router from './router/router.js'
+import store from './store/index.ts'
// Init private and public Files namespace
window.OCA.Files = window.OCA.Files ?? {}
@@ -35,5 +38,17 @@ const FilesNavigationRoot = new View({
})
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
processLegacyFilesViews()
diff --git a/apps/files/src/mixins/fileslist-row.scss b/apps/files/src/mixins/fileslist-row.scss
new file mode 100644
index 00000000000..9b0c3197b76
--- /dev/null
+++ b/apps/files/src/mixins/fileslist-row.scss
@@ -0,0 +1,63 @@
+/**
+ * @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);
+ }
+}
diff --git a/apps/files/src/services/Navigation.ts b/apps/files/src/services/Navigation.ts
index 9efed538825..01b6e701c72 100644
--- a/apps/files/src/services/Navigation.ts
+++ b/apps/files/src/services/Navigation.ts
@@ -19,19 +19,27 @@
* 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 logger from '../logger.js'
+export type ContentsWithRoot = {
+ folder: Folder,
+ contents: Node[]
+}
+
export interface Column {
/** Unique column ID */
id: string
/** Translated column title */
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
/** Special function used to sort Nodes between them */
sortFunction?: (nodeA: Node, nodeB: Node) => number;
@@ -45,8 +53,15 @@ export interface Navigation {
id: string
/** Translated view name */
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 */
icon: string
/** The view order */
@@ -150,8 +165,8 @@ const isValidNavigation = function(view: Navigation): boolean {
* TODO: remove when support for legacy views is removed
*/
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)) {
diff --git a/apps/files/src/store/files.ts b/apps/files/src/store/files.ts
new file mode 100644
index 00000000000..e9760e2bc85
--- /dev/null
+++ b/apps/files/src/store/files.ts
@@ -0,0 +1,97 @@
+/**
+ * @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,
+}
diff --git a/apps/files/src/store/index.ts b/apps/files/src/store/index.ts
new file mode 100644
index 00000000000..52007fef892
--- /dev/null
+++ b/apps/files/src/store/index.ts
@@ -0,0 +1,16 @@
+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,
+ },
+})
diff --git a/apps/files/src/store/paths.ts b/apps/files/src/store/paths.ts
new file mode 100644
index 00000000000..d6b23578da7
--- /dev/null
+++ b/apps/files/src/store/paths.ts
@@ -0,0 +1,71 @@
+/**
+ * @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,
+}
diff --git a/apps/files/src/store/selection.ts b/apps/files/src/store/selection.ts
new file mode 100644
index 00000000000..3ec61848c98
--- /dev/null
+++ b/apps/files/src/store/selection.ts
@@ -0,0 +1,51 @@
+/**
+ * @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,
+}
diff --git a/apps/files/src/types.ts b/apps/files/src/types.ts
new file mode 100644
index 00000000000..1c7068985d8
--- /dev/null
+++ b/apps/files/src/types.ts
@@ -0,0 +1,56 @@
+/**
+ * @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
+}
diff --git a/apps/files/src/views/FilesList.vue b/apps/files/src/views/FilesList.vue
new file mode 100644
index 00000000000..adc8a3bcb0f
--- /dev/null
+++ b/apps/files/src/views/FilesList.vue
@@ -0,0 +1,318 @@
+<!--
+ - @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>
diff --git a/apps/files/src/views/Navigation.vue b/apps/files/src/views/Navigation.vue
index d9fdfa7fe02..9a2e82d1bc6 100644
--- a/apps/files/src/views/Navigation.vue
+++ b/apps/files/src/views/Navigation.vue
@@ -32,13 +32,20 @@
:title="view.name"
:to="generateToNavigation(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]"
:key="child.id"
:data-cy-files-navigation-item="child.id"
:exact="true"
:icon="child.iconClass"
: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>
</template>
@@ -74,6 +81,7 @@ import axios from '@nextcloud/axios'
import Cog from 'vue-material-design-icons/Cog.vue'
import NcAppNavigation from '@nextcloud/vue/dist/Components/NcAppNavigation.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 Navigation from '../services/Navigation.ts'
@@ -86,10 +94,11 @@ export default {
components: {
Cog,
+ NavigationQuota,
NcAppNavigation,
NcAppNavigationItem,
+ NcIconSvgWrapper,
SettingsModal,
- NavigationQuota,
},
props: {
@@ -151,7 +160,16 @@ export default {
watch: {
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)
},
},
@@ -163,6 +181,12 @@ export default {
}
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: {
@@ -174,7 +198,7 @@ export default {
// Closing any opened sidebar
window?.OCA?.Files?.Sidebar?.close?.()
- if (view.legacy) {
+ if (view?.legacy) {
const newAppContent = document.querySelector('#app-content #app-content-' + this.currentView.id + '.viewcontainer')
document.querySelectorAll('#app-content .viewcontainer').forEach(el => {
el.classList.add('hidden')
@@ -188,7 +212,6 @@ export default {
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('urlChanged', params))
-
}
this.Navigation.setActive(view)
diff --git a/apps/files/templates/appnavigation.php b/apps/files/templates/appnavigation.php
index f316ccbf773..96df2b91a84 100644
--- a/apps/files/templates/appnavigation.php
+++ b/apps/files/templates/appnavigation.php
@@ -1,13 +1,11 @@
<div id="app-navigation-files" role="navigation"></div>
<div class="hidden">
<ul class="with-icon" tabindex="0">
-
<?php
-
- $pinned = 0;
- foreach ($_['navigationItems'] as $item) {
- $pinned = NavigationListElements($item, $l, $pinned);
- }
+ $pinned = 0;
+ foreach ($_['navigationItems'] as $item) {
+ $pinned = NavigationListElements($item, $l, $pinned);
+ }
?>
</ul>
</div>
diff --git a/apps/files/templates/index.php b/apps/files/templates/index.php
index 80eca84ed65..c6f145bfe40 100644
--- a/apps/files/templates/index.php
+++ b/apps/files/templates/index.php
@@ -1,5 +1,9 @@
<?php /** @var \OCP\IL10N $l */ ?>
<?php $_['appNavigation']->printPage(); ?>
+
+<!-- New files vue container -->
+<div id="app-content-vue" class="hidden"></div>
+
<div id="app-content" tabindex="0">
<input type="checkbox" class="hidden-visually" id="showgridview"
@@ -8,8 +12,6 @@
<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>
- <!-- New files vue container -->
- <div id="app-content-vue" class="hidden"></div>
<!-- Legacy views -->
<?php foreach ($_['appContents'] as $content) { ?>
diff --git a/apps/files_trashbin/composer/composer/autoload_classmap.php b/apps/files_trashbin/composer/composer/autoload_classmap.php
index 760044d4f87..01f602448d4 100644
--- a/apps/files_trashbin/composer/composer/autoload_classmap.php
+++ b/apps/files_trashbin/composer/composer/autoload_classmap.php
@@ -21,6 +21,7 @@ return array(
'OCA\\Files_Trashbin\\Expiration' => $baseDir . '/../lib/Expiration.php',
'OCA\\Files_Trashbin\\Helper' => $baseDir . '/../lib/Helper.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\\Sabre\\AbstractTrash' => $baseDir . '/../lib/Sabre/AbstractTrash.php',
'OCA\\Files_Trashbin\\Sabre\\AbstractTrashFile' => $baseDir . '/../lib/Sabre/AbstractTrashFile.php',
diff --git a/apps/files_trashbin/composer/composer/autoload_static.php b/apps/files_trashbin/composer/composer/autoload_static.php
index ef52ac0e1e7..40f3310c663 100644
--- a/apps/files_trashbin/composer/composer/autoload_static.php
+++ b/apps/files_trashbin/composer/composer/autoload_static.php
@@ -36,6 +36,7 @@ class ComposerStaticInitFiles_Trashbin
'OCA\\Files_Trashbin\\Expiration' => __DIR__ . '/..' . '/../lib/Expiration.php',
'OCA\\Files_Trashbin\\Helper' => __DIR__ . '/..' . '/../lib/Helper.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\\Sabre\\AbstractTrash' => __DIR__ . '/..' . '/../lib/Sabre/AbstractTrash.php',
'OCA\\Files_Trashbin\\Sabre\\AbstractTrashFile' => __DIR__ . '/..' . '/../lib/Sabre/AbstractTrashFile.php',
diff --git a/apps/files_trashbin/lib/AppInfo/Application.php b/apps/files_trashbin/lib/AppInfo/Application.php
index 41466a865ac..461eade6802 100644
--- a/apps/files_trashbin/lib/AppInfo/Application.php
+++ b/apps/files_trashbin/lib/AppInfo/Application.php
@@ -26,8 +26,10 @@
namespace OCA\Files_Trashbin\AppInfo;
use OCA\DAV\Connector\Sabre\Principal;
+use OCA\Files\Event\LoadAdditionalScriptsEvent;
use OCA\Files_Trashbin\Capabilities;
use OCA\Files_Trashbin\Expiration;
+use OCA\Files_Trashbin\Listeners\LoadAdditionalScripts;
use OCA\Files_Trashbin\Trash\ITrashManager;
use OCA\Files_Trashbin\Trash\TrashManager;
use OCA\Files_Trashbin\UserMigration\TrashbinMigrator;
@@ -55,6 +57,11 @@ class Application extends App implements IBootstrap {
$context->registerServiceAlias('principalBackend', Principal::class);
$context->registerUserMigrator(TrashbinMigrator::class);
+
+ $context->registerEventListener(
+ LoadAdditionalScriptsEvent::class,
+ LoadAdditionalScripts::class
+ );
}
public function boot(IBootContext $context): void {
@@ -68,18 +75,6 @@ class Application extends App implements IBootstrap {
\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
\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) {
diff --git a/apps/files_trashbin/lib/Listeners/LoadAdditionalScripts.php b/apps/files_trashbin/lib/Listeners/LoadAdditionalScripts.php
new file mode 100644
index 00000000000..33b1b2de1cc
--- /dev/null
+++ b/apps/files_trashbin/lib/Listeners/LoadAdditionalScripts.php
@@ -0,0 +1,41 @@
+<?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');
+ }
+}
diff --git a/apps/files_trashbin/src/main.ts b/apps/files_trashbin/src/main.ts
new file mode 100644
index 00000000000..626b9ef813d
--- /dev/null
+++ b/apps/files_trashbin/src/main.ts
@@ -0,0 +1,39 @@
+/**
+ * @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,
+})
diff --git a/apps/files_trashbin/src/services/client.ts b/apps/files_trashbin/src/services/client.ts
new file mode 100644
index 00000000000..9fb3361839a
--- /dev/null
+++ b/apps/files_trashbin/src/services/client.ts
@@ -0,0 +1,33 @@
+/**
+ * @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
diff --git a/apps/files_trashbin/src/services/trashbin.ts b/apps/files_trashbin/src/services/trashbin.ts
new file mode 100644
index 00000000000..2070cfc92b0
--- /dev/null
+++ b/apps/files_trashbin/src/services/trashbin.ts
@@ -0,0 +1,95 @@
+/**
+ * @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),
+ }
+}
diff --git a/apps/files_trashbin/src/trash.scss b/apps/files_trashbin/src/trash.scss
deleted file mode 100644
index 633107c9d6d..00000000000
--- a/apps/files_trashbin/src/trash.scss
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * 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;
-}
-
diff --git a/apps/files_trashbin/tests/js/appSpec.js b/apps/files_trashbin/tests/js/appSpec.js
deleted file mode 100644
index 281e7bbc2ba..00000000000
--- a/apps/files_trashbin/tests/js/appSpec.js
+++ /dev/null
@@ -1,70 +0,0 @@
-/**
-* @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');
- });
- });
-});
diff --git a/apps/files_trashbin/tests/js/filelistSpec.js b/apps/files_trashbin/tests/js/filelistSpec.js
deleted file mode 100644
index 9e27188efb8..00000000000
--- a/apps/files_trashbin/tests/js/filelistSpec.js
+++ /dev/null
@@ -1,397 +0,0 @@
-/**
- * @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);
- });
- });
- });
-});
diff --git a/babel.config.js b/babel.config.js
index 1d5dc3b6de0..3f523a8d2af 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -10,6 +10,7 @@ module.exports = {
'@babel/preset-env',
{
useBuiltIns: false,
+ modules: 'auto',
},
],
],
diff --git a/core/src/OC/apps.js b/core/src/OC/apps.js
deleted file mode 100644
index bbda177409e..00000000000
--- a/core/src/OC/apps.js
+++ /dev/null
@@ -1,135 +0,0 @@
-/**
- * @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
diff --git a/core/src/OC/index.js b/core/src/OC/index.js
index cc70bb550a7..e8f4b199103 100644
--- a/core/src/OC/index.js
+++ b/core/src/OC/index.js
@@ -30,7 +30,6 @@ import {
processAjaxError,
registerXHRForErrorProcessing,
} from './xhr-error.js'
-import Apps from './apps.js'
import { AppConfig, appConfig } from './appconfig.js'
import { appSettings } from './appsettings.js'
import appswebroots from './appswebroots.js'
@@ -45,8 +44,8 @@ import {
import {
build as buildQueryString,
parse as parseQueryString,
-} from './query-string.js'
-import Config from './config.js'
+} from './query-string'
+import Config from './config'
import {
coreApps,
menuSpeed,
@@ -58,30 +57,30 @@ import {
PERMISSION_SHARE,
PERMISSION_UPDATE,
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 {
getHost,
getHostName,
getPort,
getProtocol,
-} from './host.js'
+} from './host'
import {
getToken as getRequestToken,
-} from './requesttoken.js'
+} from './requesttoken'
import {
hideMenus,
registerMenu,
showMenu,
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 {
getCanonicalLocale,
getLanguage,
@@ -141,7 +140,6 @@ export default {
addScript,
addStyle,
- Apps,
AppConfig,
appConfig,
appSettings,
diff --git a/core/src/OC/util-history.js b/core/src/OC/util-history.js
index d18b8743936..e5f9ff9447b 100644
--- a/core/src/OC/util-history.js
+++ b/core/src/OC/util-history.js
@@ -165,6 +165,8 @@ export default {
},
_onPopState(e) {
+ debugger
+
if (this._cancelPop) {
this._cancelPop = false
return
diff --git a/core/src/main.js b/core/src/main.js
index f76d4f0b8e1..11a7ece6114 100644
--- a/core/src/main.js
+++ b/core/src/main.js
@@ -35,11 +35,9 @@ import OC from './OC/index.js'
import './globals.js'
import './jquery/index.js'
import { initCore } from './init.js'
-import { registerAppsSlideToggle } from './OC/apps.js'
window.addEventListener('DOMContentLoaded', function() {
initCore()
- registerAppsSlideToggle()
// fallback to hashchange when no history support
if (window.history.pushState) {
diff --git a/apps/files_trashbin/src/files_trashbin.js b/custom.d.ts
index f66e78905f6..80fc7ccf9e1 100644
--- a/apps/files_trashbin/src/files_trashbin.js
+++ b/custom.d.ts
@@ -1,7 +1,7 @@
/**
- * @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
*
@@ -19,9 +19,13 @@
* 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
diff --git a/cypress.d.ts b/cypress.d.ts
new file mode 100644
index 00000000000..b19af267631
--- /dev/null
+++ b/cypress.d.ts
@@ -0,0 +1,34 @@
+/**
+ * @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;
+ }
+ }
+}
diff --git a/cypress/support/component.ts b/cypress/support/component.ts
index be4b8c94b1b..b56c3dc3604 100644
--- a/cypress/support/component.ts
+++ b/cypress/support/component.ts
@@ -19,21 +19,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
+/* eslint-disable */
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:
// cy.mount(MyComponent)
Cypress.Commands.add('mount', (component, optionsOrProps) => {
diff --git a/package.json b/package.json
index a40dc0daa97..69c54664300 100644
--- a/package.json
+++ b/package.json
@@ -44,7 +44,7 @@
"@nextcloud/capabilities": "^1.0.4",
"@nextcloud/dialogs": "^4.0.0-beta.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/l10n": "^2.1.0",
"@nextcloud/logger": "^2.5.0",
@@ -99,15 +99,17 @@
"vue": "^2.7.14",
"vue-click-outside": "^1.1.0",
"vue-cropperjs": "^4.2.0",
+ "vue-fragment": "^1.6.0",
"vue-infinite-loading": "^2.4.5",
"vue-localstorage": "^0.6.2",
"vue-material-design-icons": "^5.0.0",
"vue-multiselect": "^2.1.6",
"vue-router": "^3.6.5",
+ "vue-virtual-scroll-list": "github:skjnldsv/vue-virtual-scroll-list#feat/table",
"vuedraggable": "^2.24.3",
"vuex": "^3.6.2",
"vuex-router-sync": "^5.0.0",
- "webdav": "^4.11.0"
+ "webdav": "^5.0.0-r1"
},
"devDependencies": {
"@babel/node": "^7.20.7",
@@ -160,6 +162,7 @@
"sass-loader": "^13.2.0",
"sinon": "<= 5.0.7",
"style-loader": "^3.3.1",
+ "ts-loader": "^9.4.2",
"ts-node": "^10.9.1",
"tslib": "^2.4.1",
"typescript": "^4.9.3",
diff --git a/tsconfig.json b/tsconfig.json
index 8a0ceb144a9..d8f4257afe4 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,8 +1,8 @@
{
"extends": "@vue/tsconfig/tsconfig.json",
- "include": ["./apps/**/*.ts", "./core/**/*.ts"],
+ "include": ["./apps/**/*.ts", "./core/**/*.ts", "./*.d.ts"],
"compilerOptions": {
- "types": ["node"],
+ "types": ["cypress", "node", "vue"],
"outDir": "./dist/",
"target": "ESNext",
"module": "esnext",
diff --git a/webpack.common.js b/webpack.common.js
index b76763a136e..1c589a6ce8d 100644
--- a/webpack.common.js
+++ b/webpack.common.js
@@ -171,6 +171,7 @@ module.exports = {
alias: {
// make sure to use the handlebar runtime when importing
handlebars: 'handlebars/runtime',
+ vue$: path.resolve('./node_modules/vue'),
},
extensions: ['*', '.ts', '.js', '.vue'],
symlinks: true,
diff --git a/webpack.modules.js b/webpack.modules.js
index 8bc42d81e3a..045bcaacc82 100644
--- a/webpack.modules.js
+++ b/webpack.modules.js
@@ -63,7 +63,7 @@ module.exports = {
'personal-settings': path.join(__dirname, 'apps/files_sharing/src', 'personal-settings.js'),
},
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: path.join(__dirname, 'apps/files_versions/src', 'files_versions_tab.js'),