From 29a7f7f6efd2a9791fdcfb9f9f7e862bafd8da82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Molakvo=C3=A6?= Date: Fri, 13 Jan 2023 17:32:57 +0100 Subject: [PATCH] feat(files_trashbin): migrate to vue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: John Molakvoæ --- apps/files/js/app.js | 5 +- apps/files/src/components/BreadCrumbs.vue | 58 +++ apps/files/src/components/FileEntry.vue | 134 ++++++ apps/files/src/components/FilesListHeader.vue | 122 ++++++ .../files/src/components/FilesListVirtual.vue | 124 ++++++ apps/files/src/main.js | 15 + apps/files/src/mixins/fileslist-row.scss | 63 +++ apps/files/src/services/Navigation.ts | 31 +- apps/files/src/store/files.ts | 97 +++++ apps/files/src/store/index.ts | 16 + apps/files/src/store/paths.ts | 71 ++++ apps/files/src/store/selection.ts | 51 +++ apps/files/src/types.ts | 56 +++ apps/files/src/views/FilesList.vue | 318 ++++++++++++++ apps/files/src/views/Navigation.vue | 33 +- apps/files/templates/appnavigation.php | 10 +- apps/files/templates/index.php | 6 +- .../composer/composer/autoload_classmap.php | 1 + .../composer/composer/autoload_static.php | 1 + .../lib/AppInfo/Application.php | 19 +- .../lib/Listeners/LoadAdditionalScripts.php | 41 ++ apps/files_trashbin/src/main.ts | 39 ++ apps/files_trashbin/src/services/client.ts | 33 ++ apps/files_trashbin/src/services/trashbin.ts | 95 +++++ apps/files_trashbin/src/trash.scss | 22 - apps/files_trashbin/tests/js/appSpec.js | 70 --- apps/files_trashbin/tests/js/filelistSpec.js | 397 ------------------ babel.config.js | 1 + core/src/OC/apps.js | 135 ------ core/src/OC/index.js | 30 +- core/src/OC/util-history.js | 2 + core/src/main.js | 2 - .../src/files_trashbin.js => custom.d.ts | 16 +- cypress.d.ts | 34 ++ cypress/support/component.ts | 16 +- package.json | 7 +- tsconfig.json | 4 +- webpack.common.js | 1 + webpack.modules.js | 2 +- 39 files changed, 1475 insertions(+), 703 deletions(-) create mode 100644 apps/files/src/components/BreadCrumbs.vue create mode 100644 apps/files/src/components/FileEntry.vue create mode 100644 apps/files/src/components/FilesListHeader.vue create mode 100644 apps/files/src/components/FilesListVirtual.vue create mode 100644 apps/files/src/mixins/fileslist-row.scss create mode 100644 apps/files/src/store/files.ts create mode 100644 apps/files/src/store/index.ts create mode 100644 apps/files/src/store/paths.ts create mode 100644 apps/files/src/store/selection.ts create mode 100644 apps/files/src/types.ts create mode 100644 apps/files/src/views/FilesList.vue create mode 100644 apps/files_trashbin/lib/Listeners/LoadAdditionalScripts.php create mode 100644 apps/files_trashbin/src/main.ts create mode 100644 apps/files_trashbin/src/services/client.ts create mode 100644 apps/files_trashbin/src/services/trashbin.ts delete mode 100644 apps/files_trashbin/src/trash.scss delete mode 100644 apps/files_trashbin/tests/js/appSpec.js delete mode 100644 apps/files_trashbin/tests/js/filelistSpec.js delete mode 100644 core/src/OC/apps.js rename apps/files_trashbin/src/files_trashbin.js => custom.d.ts (72%) create mode 100644 cypress.d.ts 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 @@ + + + + + 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 @@ + + + + + + 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 @@ + + + + + + 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 @@ + + + + + + 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æ + * + * @author John Molakvoæ + * + * @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 . + * + */ +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 . * */ -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 /** 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æ + * + * @author John Molakvoæ + * + * @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 . + * + */ +/* 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æ + * + * @author John Molakvoæ + * + * @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 . + * + */ +/* 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æ + * + * @author John Molakvoæ + * + * @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 . + * + */ +/* 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æ + * + * @author John Molakvoæ + * + * @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 . + * + */ +/* 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 @@ + + + + + + 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)"> + + + + + :to="generateToNavigation(child)"> + + + @@ -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 @@ 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 @@ printPage(); ?> + + + +
- - 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 @@ + + * + * @author John Molakvoæ + * + * @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 . + * + */ +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æ + * + * @author John Molakvoæ + * + * @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 . + * + */ +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æ + * + * @author John Molakvoæ + * + * @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 . + * + */ +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æ + * + * @author John Molakvoæ + * + * @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 . + * + */ +/* 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 = ` + + + + + + + + + + + + + + + +` + +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 => { + // 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 + + const contentsResponse = await client.getDirectoryContents(path, { + details: true, + data, + }) as ResponseDataDetailed + + 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 - * - * @author Vincent Petry - * - * @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 . - * - */ - -describe('OCA.Trashbin.App tests', function() { - var App = OCA.Trashbin.App; - - beforeEach(function() { - $('#testArea').append( - '
' + - '
' + - '
' + - '' + - '' + - '
' + - '
' - ); - 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 - * - * @author Abijeet - * @author Christoph Wurst - * @author Jan C. Borchardt - * @author Jan-Christoph Borchardt - * @author John Molakvoæ - * @author Robin Appelman - * @author Vincent Petry - * - * @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 . - * - */ - -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( - '
' + - // set this but it shouldn't be used (could be the one from the - // files app) - '' + - // dummy controls - '
' + - '
' + - '
' + - '
' + - // dummy table - // TODO: at some point this will be rendered by the fileList class itself! - '' + - '' + - '' + - '' + - '
' + - '
Empty content message
' + - '
' - ); - - 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 - * @author John Molakvoæ - * - * @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 . - * - */ - -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: - * - *
I'm sliding up
- */ -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 similarity index 72% rename from apps/files_trashbin/src/files_trashbin.js rename to 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 + * @copyright Copyright (c) 2023 John Molakvoæ * - * @author Roeland Jago Douma + * @author John Molakvoæ * * @license AGPL-3.0-or-later * @@ -19,9 +19,13 @@ * along with this program. If not, see . * */ +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æ + * + * @author John Molakvoæ + * + * @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 . + * + */ +/* eslint-disable */ +import { mount } from 'cypress/vue2' + +type MountParams = Parameters; +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 . * */ +/* 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 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'),