diff --git a/__mocks__/svg.js b/__mocks__/svg.js index 52b1041f754a9..1afe4357c0d81 100644 --- a/__mocks__/svg.js +++ b/__mocks__/svg.js @@ -19,4 +19,4 @@ * along with this program. If not, see . * */ -export default 'SvgMock' +export default 'SvgMock' diff --git a/__mocks__/webdav.ts b/__mocks__/webdav.ts new file mode 100644 index 0000000000000..5a1498c0e2db9 --- /dev/null +++ b/__mocks__/webdav.ts @@ -0,0 +1,27 @@ +/** + * @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 . + * + */ +export const createClient = () => {} +export const getPatcher = () => { + return { + patch: () => {} + } +} diff --git a/apps/files/appinfo/routes.php b/apps/files/appinfo/routes.php index a97c631d89659..05d0a37fd706d 100644 --- a/apps/files/appinfo/routes.php +++ b/apps/files/appinfo/routes.php @@ -128,11 +128,6 @@ 'url' => '/api/v1/showgridview', 'verb' => 'GET' ], - [ - 'name' => 'Api#getNodeType', - 'url' => '/api/v1/quickaccess/get/NodeType', - 'verb' => 'GET', - ], [ 'name' => 'DirectEditingView#edit', 'url' => '/directEditing/{token}', diff --git a/apps/files/js/favoritesfilelist.js b/apps/files/js/favoritesfilelist.js deleted file mode 100644 index 2c6b3c63e1559..0000000000000 --- a/apps/files/js/favoritesfilelist.js +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (c) 2014 Vincent Petry - * - * This file is licensed under the Affero General Public License version 3 - * or later. - * - * See the COPYING-README file. - * - */ - -// HACK: this piece needs to be loaded AFTER the files app (for unit tests) -window.addEventListener('DOMContentLoaded', function() { - (function(OCA) { - /** - * @class OCA.Files.FavoritesFileList - * @augments OCA.Files.FavoritesFileList - * - * @classdesc Favorites file list. - * Displays the list of files marked as favorites - * - * @param $el container element with existing markup for the .files-controls - * and a table - * @param [options] map of options, see other parameters - */ - var FavoritesFileList = function($el, options) { - this.initialize($el, options); - }; - FavoritesFileList.prototype = _.extend({}, OCA.Files.FileList.prototype, - /** @lends OCA.Files.FavoritesFileList.prototype */ { - id: 'favorites', - appName: t('files','Favorites'), - - _clientSideSort: true, - _allowSelection: false, - - /** - * @private - */ - initialize: function($el, options) { - OCA.Files.FileList.prototype.initialize.apply(this, arguments); - if (this.initialized) { - return; - } - OC.Plugins.attach('OCA.Files.FavoritesFileList', this); - }, - - updateEmptyContent: function() { - var dir = this.getCurrentDirectory(); - if (dir === '/') { - // root has special permissions - this.$el.find('.emptyfilelist.emptycontent').toggleClass('hidden', !this.isEmpty); - this.$el.find('.files-filestable thead th').toggleClass('hidden', this.isEmpty); - } - else { - OCA.Files.FileList.prototype.updateEmptyContent.apply(this, arguments); - } - }, - - getDirectoryPermissions: function() { - return OC.PERMISSION_READ | OC.PERMISSION_DELETE; - }, - - updateStorageStatistics: function() { - // no op because it doesn't have - // storage info like free space / used space - }, - - reload: function() { - this.showMask(); - if (this._reloadCall?.abort) { - this._reloadCall.abort(); - } - - // there is only root - this._setCurrentDir('/', false); - - this._reloadCall = this.filesClient.getFilteredFiles( - { - favorite: true - }, - { - properties: this._getWebdavProperties() - } - ); - var callBack = this.reloadCallback.bind(this); - return this._reloadCall.then(callBack, callBack); - }, - - reloadCallback: function(status, result) { - if (result) { - // prepend empty dir info because original handler - result.unshift({}); - } - - return OCA.Files.FileList.prototype.reloadCallback.call(this, status, result); - }, - }); - - OCA.Files.FavoritesFileList = FavoritesFileList; - })(OCA); -}); - diff --git a/apps/files/js/favoritesplugin.js b/apps/files/js/favoritesplugin.js deleted file mode 100644 index 5964d71a46940..0000000000000 --- a/apps/files/js/favoritesplugin.js +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (c) 2014 Vincent Petry - * - * This file is licensed under the Affero General Public License version 3 - * or later. - * - * See the COPYING-README file. - * - */ - -(function(OCA) { - /** - * Registers the favorites file list from the files app sidebar. - * - * @namespace OCA.Files.FavoritesPlugin - */ - OCA.Files.FavoritesPlugin = { - name: 'Favorites', - - /** - * @type OCA.Files.FavoritesFileList - */ - favoritesFileList: null, - - attach: function() { - var self = this; - $('#app-content-favorites').on('show.plugin-favorites', function(e) { - self.showFileList($(e.target)); - }); - $('#app-content-favorites').on('hide.plugin-favorites', function() { - self.hideFileList(); - }); - }, - - detach: function() { - if (this.favoritesFileList) { - this.favoritesFileList.destroy(); - OCA.Files.fileActions.off('setDefault.plugin-favorites', this._onActionsUpdated); - OCA.Files.fileActions.off('registerAction.plugin-favorites', this._onActionsUpdated); - $('#app-content-favorites').off('.plugin-favorites'); - this.favoritesFileList = null; - } - }, - - showFileList: function($el) { - if (!this.favoritesFileList) { - this.favoritesFileList = this._createFavoritesFileList($el); - } - return this.favoritesFileList; - }, - - hideFileList: function() { - if (this.favoritesFileList) { - this.favoritesFileList.$fileList.empty(); - } - }, - - /** - * Creates the favorites file list. - * - * @param $el container for the file list - * @return {OCA.Files.FavoritesFileList} file list - */ - _createFavoritesFileList: function($el) { - var fileActions = this._createFileActions(); - // register favorite list for sidebar section - return new OCA.Files.FavoritesFileList( - $el, { - fileActions: fileActions, - // The file list is created when a "show" event is handled, - // so it should be marked as "shown" like it would have been - // done if handling the event with the file list already - // created. - shown: true - } - ); - }, - - _createFileActions: function() { - // inherit file actions from the files app - var fileActions = new OCA.Files.FileActions(); - // note: not merging the legacy actions because legacy apps are not - // compatible with the sharing overview and need to be adapted first - fileActions.registerDefaultActions(); - fileActions.merge(OCA.Files.fileActions); - - if (!this._globalActionsInitialized) { - // in case actions are registered later - this._onActionsUpdated = _.bind(this._onActionsUpdated, this); - OCA.Files.fileActions.on('setDefault.plugin-favorites', this._onActionsUpdated); - OCA.Files.fileActions.on('registerAction.plugin-favorites', this._onActionsUpdated); - this._globalActionsInitialized = true; - } - - // when the user clicks on a folder, redirect to the corresponding - // folder in the files app instead of opening it directly - fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) { - OCA.Files.App.setActiveView('files', {silent: true}); - OCA.Files.App.fileList.changeDirectory(OC.joinPaths(context.$file.attr('data-path'), filename), true, true); - }); - fileActions.setDefault('dir', 'Open'); - return fileActions; - }, - - _onActionsUpdated: function(ev) { - if (ev.action) { - this.favoritesFileList.fileActions.registerAction(ev.action); - } else if (ev.defaultAction) { - this.favoritesFileList.fileActions.setDefault( - ev.defaultAction.mime, - ev.defaultAction.name - ); - } - } - }; - -})(OCA); - -OC.Plugins.register('OCA.Files.App', OCA.Files.FavoritesPlugin); - diff --git a/apps/files/js/merged-index.json b/apps/files/js/merged-index.json index 2b7d6ec7d6d42..38b36c1689601 100644 --- a/apps/files/js/merged-index.json +++ b/apps/files/js/merged-index.json @@ -4,8 +4,6 @@ "detailfileinfoview.js", "detailsview.js", "detailtabview.js", - "favoritesfilelist.js", - "favoritesplugin.js", "file-upload.js", "fileactions.js", "fileactionsmenu.js", diff --git a/apps/files/js/navigation.js b/apps/files/js/navigation.js deleted file mode 100644 index d7ae7dd7fee87..0000000000000 --- a/apps/files/js/navigation.js +++ /dev/null @@ -1,347 +0,0 @@ -/* - * @Copyright 2014 Vincent Petry - * - * @author Vincent Petry - * @author Felix Nüsse - * - * - * This file is licensed under the Affero General Public License version 3 - * or later. - * - * See the COPYING-README file. - * - */ - -(function () { - - /** - * @class OCA.Files.Navigation - * @classdesc Navigation control for the files app sidebar. - * - * @param $el element containing the navigation - */ - var Navigation = function ($el) { - this.initialize($el); - }; - - /** - * @memberof OCA.Files - */ - Navigation.prototype = { - - /** - * Currently selected item in the list - */ - _activeItem: null, - - /** - * Currently selected container - */ - $currentContent: null, - - /** - * Key for the quick-acces-list - */ - $quickAccessListKey: 'sublist-favorites', - /** - * Initializes the navigation from the given container - * - * @private - * @param $el element containing the navigation - */ - initialize: function ($el) { - this.$el = $el; - this._activeItem = null; - this.$currentContent = null; - this._setupEvents(); - - this.setInitialQuickaccessSettings(); - }, - - /** - * Setup UI events - */ - _setupEvents: function () { - this.$el.on('click', 'li a', _.bind(this._onClickItem, this)); - this.$el.on('click', 'li button', _.bind(this._onClickMenuButton, this)); - - var trashBinElement = $('.nav-trashbin'); - trashBinElement.droppable({ - over: function (event, ui) { - trashBinElement.addClass('dropzone-background'); - }, - out: function (event, ui) { - trashBinElement.removeClass('dropzone-background'); - }, - activate: function (event, ui) { - var element = trashBinElement.find('a').first(); - element.addClass('nav-icon-trashbin-starred').removeClass('nav-icon-trashbin'); - }, - deactivate: function (event, ui) { - var element = trashBinElement.find('a').first(); - element.addClass('nav-icon-trashbin').removeClass('nav-icon-trashbin-starred'); - }, - drop: function (event, ui) { - trashBinElement.removeClass('dropzone-background'); - - var $selectedFiles = $(ui.draggable); - - // FIXME: when there are a lot of selected files the helper - // contains only a subset of them; the list of selected - // files should be gotten from the file list instead to - // ensure that all of them are removed. - var item = ui.helper.find('tr'); - for (var i = 0; i < item.length; i++) { - $selectedFiles.trigger('droppedOnTrash', item[i].getAttribute('data-file'), item[i].getAttribute('data-dir')); - } - } - }); - }, - - /** - * Returns the container of the currently active app. - * - * @return app container - */ - getActiveContainer: function () { - return this.$currentContent; - }, - - /** - * Returns the currently active item - * - * @return item ID - */ - getActiveItem: function () { - return this._activeItem; - }, - - /** - * Switch the currently selected item, mark it as selected and - * make the content container visible, if any. - * - * @param string itemId id of the navigation item to select - * @param array options "silent" to not trigger event - */ - setActiveItem: function (itemId, options) { - var currentItem = this.$el.find('li[data-id="' + itemId + '"]'); - var itemDir = currentItem.data('dir'); - var itemView = currentItem.data('view'); - var oldItemId = this._activeItem; - if (itemId === this._activeItem) { - if (!options || !options.silent) { - this.$el.trigger( - new $.Event('itemChanged', { - itemId: itemId, - previousItemId: oldItemId, - dir: itemDir, - view: itemView - }) - ); - } - return; - } - this.$el.find('li a').removeClass('active').removeAttr('aria-current'); - if (this.$currentContent) { - this.$currentContent.addClass('hidden'); - this.$currentContent.trigger(jQuery.Event('hide')); - } - this._activeItem = itemId; - currentItem.children('a').addClass('active').attr('aria-current', 'page'); - this.$currentContent = $('#app-content-' + (typeof itemView === 'string' && itemView !== '' ? itemView : itemId)); - this.$currentContent.removeClass('hidden'); - if (!options || !options.silent) { - this.$currentContent.trigger(jQuery.Event('show', { - itemId: itemId, - previousItemId: oldItemId, - dir: itemDir, - view: itemView - })); - this.$el.trigger( - new $.Event('itemChanged', { - itemId: itemId, - previousItemId: oldItemId, - dir: itemDir, - view: itemView - }) - ); - } - }, - - /** - * Returns whether a given item exists - */ - itemExists: function (itemId) { - return this.$el.find('li[data-id="' + itemId + '"]').length; - }, - - /** - * Event handler for when clicking on an item. - */ - _onClickItem: function (ev) { - var $target = $(ev.target); - var itemId = $target.closest('li').attr('data-id'); - if (!_.isUndefined(itemId)) { - this.setActiveItem(itemId); - } - ev.preventDefault(); - }, - - /** - * Event handler for clicking a button - */ - _onClickMenuButton: function (ev) { - var $target = $(ev.target); - var $menu = $target.parent('li'); - var itemId = $target.closest('button').attr('id'); - - var collapsibleToggles = []; - var dotmenuToggles = []; - - if ($menu.hasClass('collapsible') && $menu.data('expandedstate')) { - $menu.toggleClass('open'); - var targetAriaExpanded = $target.attr('aria-expanded'); - if (targetAriaExpanded === 'false') { - $target.attr('aria-expanded', 'true'); - } else if (targetAriaExpanded === 'true') { - $target.attr('aria-expanded', 'false'); - } - $menu.toggleAttr('data-expanded', 'true', 'false'); - var show = $menu.hasClass('open') ? 1 : 0; - var key = $menu.data('expandedstate'); - $.post(OC.generateUrl("/apps/files/api/v1/toggleShowFolder/" + key), {show: show}); - } - - dotmenuToggles.forEach(function foundToggle (item) { - if (item[0] === ("#" + itemId)) { - document.getElementById(item[1]).classList.toggle('open'); - } - }); - - ev.preventDefault(); - }, - - /** - * Sort initially as setup of sidebar for QuickAccess - */ - setInitialQuickaccessSettings: function () { - var quickAccessKey = this.$quickAccessListKey; - var quickAccessMenu = document.getElementById(quickAccessKey); - if (quickAccessMenu) { - var list = quickAccessMenu.getElementsByTagName('li'); - this.QuickSort(list, 0, list.length - 1); - } - - var favoritesListElement = $(quickAccessMenu).parent(); - favoritesListElement.droppable({ - over: function (event, ui) { - favoritesListElement.addClass('dropzone-background'); - }, - out: function (event, ui) { - favoritesListElement.removeClass('dropzone-background'); - }, - activate: function (event, ui) { - var element = favoritesListElement.find('a').first(); - element.addClass('nav-icon-favorites-starred').removeClass('nav-icon-favorites'); - }, - deactivate: function (event, ui) { - var element = favoritesListElement.find('a').first(); - element.addClass('nav-icon-favorites').removeClass('nav-icon-favorites-starred'); - }, - drop: function (event, ui) { - favoritesListElement.removeClass('dropzone-background'); - - var $selectedFiles = $(ui.draggable); - - if (ui.helper.find('tr').size() === 1) { - var $tr = $selectedFiles.closest('tr'); - if ($tr.attr("data-favorite")) { - return; - } - $selectedFiles.trigger('droppedOnFavorites', $tr.attr('data-file')); - } else { - // FIXME: besides the issue described for dropping on - // the trash bin, for favoriting it is not possible to - // use the data from the helper; due to some bugs the - // tags are not always added to the selected files, and - // thus that data can not be accessed through the helper - // to prevent triggering the favorite action on an - // already favorited file (which would remove it from - // favorites). - OC.Notification.showTemporary(t('files', 'You can only favorite a single file or folder at a time')); - } - } - }); - }, - - /** - * Sorting-Algorithm for QuickAccess - */ - QuickSort: function (list, start, end) { - var lastMatch; - if (list.length > 1) { - lastMatch = this.quicksort_helper(list, start, end); - if (start < lastMatch - 1) { - this.QuickSort(list, start, lastMatch - 1); - } - if (lastMatch < end) { - this.QuickSort(list, lastMatch, end); - } - } - }, - - /** - * Sorting-Algorithm-Helper for QuickAccess - */ - quicksort_helper: function (list, start, end) { - var pivot = Math.floor((end + start) / 2); - var pivotElement = this.getCompareValue(list, pivot); - var i = start; - var j = end; - - while (i <= j) { - while (this.getCompareValue(list, i) < pivotElement) { - i++; - } - while (this.getCompareValue(list, j) > pivotElement) { - j--; - } - if (i <= j) { - this.swap(list, i, j); - i++; - j--; - } - } - return i; - }, - - /** - * Sorting-Algorithm-Helper for QuickAccess - * This method allows easy access to the element which is sorted by. - */ - getCompareValue: function (nodes, int, strategy) { - return nodes[int].getElementsByTagName('a')[0].innerHTML.toLowerCase(); - }, - - /** - * Sorting-Algorithm-Helper for QuickAccess - * This method allows easy swapping of elements. - */ - swap: function (list, j, i) { - var before = function(node, insertNode) { - node.parentNode.insertBefore(insertNode, node); - } - before(list[i], list[j]); - before(list[j], list[i]); - } - - }; - - OCA.Files.Navigation = Navigation; - -})(); - - - - - diff --git a/apps/files/js/tagsplugin.js b/apps/files/js/tagsplugin.js index 6759aa7002b5c..78bd7eec5577f 100644 --- a/apps/files/js/tagsplugin.js +++ b/apps/files/js/tagsplugin.js @@ -52,82 +52,6 @@ $favoriteMarkEl.toggleClass('permanent', state); } - /** - * Remove Item from Quickaccesslist - * - * @param {String} appfolder folder to be removed - */ - function removeFavoriteFromList (appfolder) { - var quickAccessList = 'sublist-favorites'; - var listULElements = document.getElementById(quickAccessList); - if (!listULElements) { - return; - } - - var apppath=appfolder; - if(appfolder.startsWith("//")){ - apppath=appfolder.substring(1, appfolder.length); - } - - $(listULElements).find('[data-dir="' + _.escape(apppath) + '"]').remove(); - - if (listULElements.childElementCount === 0) { - var collapsibleButton = $(listULElements).parent().find('button.collapse'); - collapsibleButton.hide(); - $("#button-collapse-parent-favorites").removeClass('collapsible'); - } - } - - /** - * Add Item to Quickaccesslist - * - * @param {String} appfolder folder to be added - */ - function addFavoriteToList (appfolder) { - var quickAccessList = 'sublist-favorites'; - var listULElements = document.getElementById(quickAccessList); - if (!listULElements) { - return; - } - var listLIElements = listULElements.getElementsByTagName('li'); - - var appName = appfolder.substring(appfolder.lastIndexOf("/") + 1, appfolder.length); - var apppath = appfolder; - - if(appfolder.startsWith("//")){ - apppath = appfolder.substring(1, appfolder.length); - } - var url = OC.generateUrl('/apps/files/?dir=' + apppath + '&view=files'); - - var innerTagA = document.createElement('A'); - innerTagA.setAttribute("href", url); - innerTagA.setAttribute("class", "nav-icon-files svg"); - innerTagA.innerHTML = _.escape(appName); - - var length = listLIElements.length + 1; - var innerTagLI = document.createElement('li'); - innerTagLI.setAttribute("data-id", apppath.replace('/', '-')); - innerTagLI.setAttribute("data-dir", apppath); - innerTagLI.setAttribute("data-view", 'files'); - innerTagLI.setAttribute("class", "nav-" + appName); - innerTagLI.setAttribute("folderpos", length.toString()); - innerTagLI.appendChild(innerTagA); - - $.get(OC.generateUrl("/apps/files/api/v1/quickaccess/get/NodeType"),{folderpath: apppath}, function (data, status) { - if (data === "dir") { - if (listULElements.childElementCount <= 0) { - listULElements.appendChild(innerTagLI); - var collapsibleButton = $(listULElements).parent().find('button.collapse'); - collapsibleButton.show(); - $(listULElements).parent().addClass('collapsible'); - } else { - listLIElements[listLIElements.length - 1].after(innerTagLI); - } - } - } - ); - } - OCA.Files = OCA.Files || {}; /** @@ -194,13 +118,23 @@ tags = tags.split('|'); tags = _.without(tags, ''); var isFavorite = tags.indexOf(OC.TAG_FAVORITE) >= 0; + + // Fake Node object for vue compatibility + const node = { + type: 'folder', + path: (dir + '/' + fileName).replace(/\/\/+/g, '/'), + root: '/files/' + OC.getCurrentUser().uid + } + if (isFavorite) { // remove tag from list tags = _.without(tags, OC.TAG_FAVORITE); - removeFavoriteFromList(dir + '/' + fileName); + // vue compatibility + window._nc_event_bus.emit('files:favorites:removed', node) } else { tags.push(OC.TAG_FAVORITE); - addFavoriteToList(dir + '/' + fileName); + // vue compatibility + window._nc_event_bus.emit('files:favorites:added', node) } // pre-toggle the star diff --git a/apps/files/lib/AppInfo/Application.php b/apps/files/lib/AppInfo/Application.php index 0d366e66fe8b3..7021769752e55 100644 --- a/apps/files/lib/AppInfo/Application.php +++ b/apps/files/lib/AppInfo/Application.php @@ -172,15 +172,6 @@ private function registerNavigation(IL10N $l10n): void { 'name' => $l10n->t('Recent') ]; }); - \OCA\Files\App::getNavigationManager()->add(function () use ($l10n) { - return [ - 'id' => 'favorites', - 'appname' => 'files', - 'script' => 'simplelist.php', - 'order' => 5, - 'name' => $l10n->t('Favorites'), - ]; - }); } private function registerHooks(): void { diff --git a/apps/files/lib/Controller/ApiController.php b/apps/files/lib/Controller/ApiController.php index fd0f3bdf26166..f8911c4d10425 100644 --- a/apps/files/lib/Controller/ApiController.php +++ b/apps/files/lib/Controller/ApiController.php @@ -383,20 +383,6 @@ public function getGridView() { return new JSONResponse(['gridview' => $status]); } - /** - * Get sorting-order for custom sorting - * - * @NoAdminRequired - * - * @param string $folderpath - * @return string - * @throws \OCP\Files\NotFoundException - */ - public function getNodeType($folderpath) { - $node = $this->userFolder->get($folderpath); - return $node->getType(); - } - /** * @NoAdminRequired * @NoCSRFRequired diff --git a/apps/files/lib/Controller/ViewController.php b/apps/files/lib/Controller/ViewController.php index 70e0fd4845654..43be43aa11647 100644 --- a/apps/files/lib/Controller/ViewController.php +++ b/apps/files/lib/Controller/ViewController.php @@ -202,37 +202,8 @@ public function index($dir = '', $view = '', $fileid = null, $fileNotFound = fal $favElements['folders'] = []; } - $collapseClasses = ''; - if (count($favElements['folders']) > 0) { - $collapseClasses = 'collapsible'; - } - - $favoritesSublistArray = []; - - $navBarPositionPosition = 6; - foreach ($favElements['folders'] as $favElement) { - $element = [ - 'id' => str_replace('/', '-', $favElement), - 'dir' => $favElement, - 'order' => $navBarPositionPosition, - 'name' => basename($favElement), - 'icon' => 'folder', - 'params' => [ - 'view' => 'files', - 'dir' => $favElement, - ], - ]; - - array_push($favoritesSublistArray, $element); - $navBarPositionPosition++; - } - $navItems = \OCA\Files\App::getNavigationManager()->getAll(); - // add the favorites entry in menu - $navItems['favorites']['sublist'] = $favoritesSublistArray; - $navItems['favorites']['classes'] = $collapseClasses; - // parse every menu and add the expanded user value foreach ($navItems as $key => $item) { $navItems[$key]['expanded'] = $this->config->getUserValue($userId, 'files', 'show_' . $item['id'], '0') === '1'; @@ -253,6 +224,7 @@ public function index($dir = '', $view = '', $fileid = null, $fileNotFound = fal $this->initialState->provideInitialState('navigation', $navItems); $this->initialState->provideInitialState('config', $this->userConfig->getConfigs()); $this->initialState->provideInitialState('viewConfigs', $this->viewConfig->getConfigs()); + $this->initialState->provideInitialState('favoriteFolders', $favElements['folders'] ?? []); // File sorting user config $filesSortingConfig = json_decode($this->config->getUserValue($userId, 'files', 'files_sorting_configs', '{}'), true); diff --git a/apps/files/lib/Service/UserConfig.php b/apps/files/lib/Service/UserConfig.php index e405b02c07a91..c39719ae8ed2c 100644 --- a/apps/files/lib/Service/UserConfig.php +++ b/apps/files/lib/Service/UserConfig.php @@ -41,6 +41,12 @@ class UserConfig { 'default' => false, 'allowed' => [true, false], ], + [ + // Whether to sort favorites first in the list or not + 'key' => 'sort_favorites_first', + 'default' => true, + 'allowed' => [true, false], + ], ]; protected IConfig $config; @@ -133,7 +139,7 @@ public function getConfigs(): array { $userConfigs = array_map(function(string $key) use ($userId) { $value = $this->config->getUserValue($userId, Application::APP_ID, $key, $this->getDefaultConfigValue($key)); // If the default is expected to be a boolean, we need to cast the value - if (is_bool($this->getDefaultConfigValue($key))) { + if (is_bool($this->getDefaultConfigValue($key)) && is_string($value)) { return $value === '1'; } return $value; diff --git a/apps/files/src/actions/deleteAction.spec.ts b/apps/files/src/actions/deleteAction.spec.ts index af7722008b619..8d99b195c3d31 100644 --- a/apps/files/src/actions/deleteAction.spec.ts +++ b/apps/files/src/actions/deleteAction.spec.ts @@ -43,8 +43,8 @@ describe('Delete action conditions tests', () => { expect(action).toBeInstanceOf(FileAction) expect(action.id).toBe('delete') expect(action.displayName([], view)).toBe('Delete') - expect(action.iconSvgInline([], view)).toBe('SvgMock') - expect(action.default).toBe(false) + expect(action.iconSvgInline([], view)).toBe('SvgMock') + expect(action.default).toBeUndefined() expect(action.order).toBe(100) }) diff --git a/apps/files/src/actions/deleteAction.ts b/apps/files/src/actions/deleteAction.ts index 20af8573dd986..52dd2f534915c 100644 --- a/apps/files/src/actions/deleteAction.ts +++ b/apps/files/src/actions/deleteAction.ts @@ -23,7 +23,7 @@ import { emit } from '@nextcloud/event-bus' import { Permission, Node } from '@nextcloud/files' import { translate as t } from '@nextcloud/l10n' import axios from '@nextcloud/axios' -import TrashCan from '@mdi/svg/svg/trash-can.svg?raw' +import TrashCanSvg from '@mdi/svg/svg/trash-can.svg?raw' import { registerFileAction, FileAction } from '../services/FileAction' import logger from '../logger.js' @@ -36,7 +36,7 @@ export const action = new FileAction({ ? t('files_trashbin', 'Delete permanently') : t('files', 'Delete') }, - iconSvgInline: () => TrashCan, + iconSvgInline: () => TrashCanSvg, enabled(nodes: Node[]) { return nodes.length > 0 && nodes diff --git a/apps/files/src/actions/downloadAction.spec.ts b/apps/files/src/actions/downloadAction.spec.ts index a9b51b395101e..abe099af3f897 100644 --- a/apps/files/src/actions/downloadAction.spec.ts +++ b/apps/files/src/actions/downloadAction.spec.ts @@ -38,8 +38,8 @@ describe('Download action conditions tests', () => { expect(action).toBeInstanceOf(FileAction) expect(action.id).toBe('download') expect(action.displayName([], view)).toBe('Download') - expect(action.iconSvgInline([], view)).toBe('SvgMock') - expect(action.default).toBe(false) + expect(action.iconSvgInline([], view)).toBe('SvgMock') + expect(action.default).toBeUndefined() expect(action.order).toBe(30) }) }) diff --git a/apps/files/src/actions/downloadAction.ts b/apps/files/src/actions/downloadAction.ts index 3801553aeaa3e..44e9fa4b379ff 100644 --- a/apps/files/src/actions/downloadAction.ts +++ b/apps/files/src/actions/downloadAction.ts @@ -22,7 +22,7 @@ import { emit } from '@nextcloud/event-bus' import { Permission, Node, FileType } from '@nextcloud/files' import { translate as t } from '@nextcloud/l10n' -import ArrowDown from '@mdi/svg/svg/arrow-down.svg?raw' +import ArrowDownSvg from '@mdi/svg/svg/arrow-down.svg?raw' import { registerFileAction, FileAction } from '../services/FileAction' import { generateUrl } from '@nextcloud/router' @@ -48,7 +48,7 @@ const downloadNodes = function(dir: string, nodes: Node[]) { export const action = new FileAction({ id: 'download', displayName: () => t('files', 'Download'), - iconSvgInline: () => ArrowDown, + iconSvgInline: () => ArrowDownSvg, enabled(nodes: Node[]) { return nodes.length > 0 && nodes diff --git a/apps/files/src/actions/editLocallyAction.spec.ts b/apps/files/src/actions/editLocallyAction.spec.ts index 3582c0d913857..f40b3b558db4f 100644 --- a/apps/files/src/actions/editLocallyAction.spec.ts +++ b/apps/files/src/actions/editLocallyAction.spec.ts @@ -22,7 +22,7 @@ import { action } from './editLocallyAction' import { expect } from '@jest/globals' import { File, Permission } from '@nextcloud/files' -import { FileAction } from '../services/FileAction' +import { DefaultType, FileAction } from '../services/FileAction' import * as ncDialogs from '@nextcloud/dialogs' import axios from '@nextcloud/axios' import type { Navigation } from '../services/Navigation' @@ -37,8 +37,8 @@ describe('Edit locally action conditions tests', () => { expect(action).toBeInstanceOf(FileAction) expect(action.id).toBe('edit-locally') expect(action.displayName([], view)).toBe('Edit locally') - expect(action.iconSvgInline([], view)).toBe('SvgMock') - expect(action.default).toBe(true) + expect(action.iconSvgInline([], view)).toBe('SvgMock') + expect(action.default).toBeUndefined() expect(action.order).toBe(25) }) }) @@ -140,7 +140,7 @@ describe('Edit locally action execute tests', () => { test('Edit locally fails and show error', async () => { jest.spyOn(axios, 'post').mockImplementation(async () => ({})) - jest.spyOn(ncDialogs, 'showError').mockImplementation(async () => ({})) + jest.spyOn(ncDialogs, 'showError') const file = new File({ id: 1, diff --git a/apps/files/src/actions/editLocallyAction.ts b/apps/files/src/actions/editLocallyAction.ts index ad7e805ec2e0b..ce693adc157fe 100644 --- a/apps/files/src/actions/editLocallyAction.ts +++ b/apps/files/src/actions/editLocallyAction.ts @@ -23,11 +23,11 @@ import { encodePath } from '@nextcloud/paths' import { Permission, type Node } from '@nextcloud/files' import { translate as t } from '@nextcloud/l10n' import axios from '@nextcloud/axios' -import DevicesSvg from '@mdi/svg/svg/devices.svg?raw' +import LaptopSvg from '@mdi/svg/svg/laptop.svg?raw' import { generateOcsUrl } from '@nextcloud/router' import { getCurrentUser } from '@nextcloud/auth' -import { registerFileAction, FileAction } from '../services/FileAction' +import { registerFileAction, FileAction, DefaultType } from '../services/FileAction' import { showError } from '@nextcloud/dialogs' const openLocalClient = async function(path: string) { @@ -48,7 +48,7 @@ const openLocalClient = async function(path: string) { export const action = new FileAction({ id: 'edit-locally', displayName: () => t('files', 'Edit locally'), - iconSvgInline: () => DevicesSvg, + iconSvgInline: () => LaptopSvg, // Only works on single files enabled(nodes: Node[]) { @@ -65,7 +65,6 @@ export const action = new FileAction({ return null }, - default: true, order: 25, }) diff --git a/apps/files/src/actions/favoriteAction.spec.ts b/apps/files/src/actions/favoriteAction.spec.ts index 144c3a51dc8a0..48a00094a0d3b 100644 --- a/apps/files/src/actions/favoriteAction.spec.ts +++ b/apps/files/src/actions/favoriteAction.spec.ts @@ -55,8 +55,8 @@ describe('Favorite action conditions tests', () => { expect(action).toBeInstanceOf(FileAction) expect(action.id).toBe('favorite') expect(action.displayName([file], view)).toBe('Add to favorites') - expect(action.iconSvgInline([], view)).toBe('SvgMock') - expect(action.default).toBe(false) + expect(action.iconSvgInline([], view)).toBe('SvgMock') + expect(action.default).toBeUndefined() expect(action.order).toBe(-50) }) diff --git a/apps/files/src/actions/favoriteAction.ts b/apps/files/src/actions/favoriteAction.ts index 5b9ba7f95e639..1ae77b6fb2151 100644 --- a/apps/files/src/actions/favoriteAction.ts +++ b/apps/files/src/actions/favoriteAction.ts @@ -22,7 +22,8 @@ import { emit } from '@nextcloud/event-bus' import { translate as t } from '@nextcloud/l10n' import axios from '@nextcloud/axios' -import Star from '@mdi/svg/svg/star.svg?raw' +import StarSvg from '@mdi/svg/svg/star.svg?raw' +import StarOutlineSvg from '@mdi/svg/svg/star-outline.svg?raw' import type { Node } from '@nextcloud/files' import { generateUrl } from '@nextcloud/router' @@ -77,7 +78,11 @@ export const action = new FileAction({ ? t('files', 'Add to favorites') : t('files', 'Remove from favorites') }, - iconSvgInline: () => Star, + iconSvgInline: (nodes: Node[]) => { + return shouldFavorite(nodes) + ? StarOutlineSvg + : StarSvg + }, enabled(nodes: Node[]) { // We can only favorite nodes within files diff --git a/apps/files/src/actions/openFolderAction.spec.ts b/apps/files/src/actions/openFolderAction.spec.ts index 140b6722608d2..5a0ccc98978cc 100644 --- a/apps/files/src/actions/openFolderAction.spec.ts +++ b/apps/files/src/actions/openFolderAction.spec.ts @@ -22,7 +22,7 @@ import { action } from './openFolderAction' import { expect } from '@jest/globals' import { File, Folder, Node, Permission } from '@nextcloud/files' -import { FileAction } from '../services/FileAction' +import { DefaultType, FileAction } from '../services/FileAction' import type { Navigation } from '../services/Navigation' const view = { @@ -42,8 +42,8 @@ describe('Open folder action conditions tests', () => { expect(action).toBeInstanceOf(FileAction) expect(action.id).toBe('open-folder') expect(action.displayName([folder], view)).toBe('Open folder FooBar') - expect(action.iconSvgInline([], view)).toBe('SvgMock') - expect(action.default).toBe(true) + expect(action.iconSvgInline([], view)).toBe('SvgMock') + expect(action.default).toBe(DefaultType.HIDDEN) expect(action.order).toBe(-100) }) }) diff --git a/apps/files/src/actions/openFolderAction.ts b/apps/files/src/actions/openFolderAction.ts index 76467796a2b32..ccb3f1a43ea07 100644 --- a/apps/files/src/actions/openFolderAction.ts +++ b/apps/files/src/actions/openFolderAction.ts @@ -21,11 +21,11 @@ */ import { Permission, Node, FileType } from '@nextcloud/files' import { translate as t } from '@nextcloud/l10n' -import Folder from '@mdi/svg/svg/folder.svg?raw' +import FolderSvg from '@mdi/svg/svg/folder.svg?raw' import type { Navigation } from '../services/Navigation' import { join } from 'path' -import { registerFileAction, FileAction } from '../services/FileAction' +import { registerFileAction, FileAction, DefaultType } from '../services/FileAction' export const action = new FileAction({ id: 'open-folder', @@ -34,7 +34,7 @@ export const action = new FileAction({ const displayName = files[0].attributes.displayName || files[0].basename return t('files', 'Open folder {displayName}', { displayName }) }, - iconSvgInline: () => Folder, + iconSvgInline: () => FolderSvg, enabled(nodes: Node[]) { // Only works on single node @@ -66,7 +66,7 @@ export const action = new FileAction({ }, // Main action if enabled, meaning folders only - default: true, + default: DefaultType.HIDDEN, order: -100, }) diff --git a/apps/files/src/actions/renameAction.spec.ts b/apps/files/src/actions/renameAction.spec.ts index ae2cfcec7ebd1..c4d5d45cde9b9 100644 --- a/apps/files/src/actions/renameAction.spec.ts +++ b/apps/files/src/actions/renameAction.spec.ts @@ -36,8 +36,8 @@ describe('Rename action conditions tests', () => { expect(action).toBeInstanceOf(FileAction) expect(action.id).toBe('rename') expect(action.displayName([], view)).toBe('Rename') - expect(action.iconSvgInline([], view)).toBe('SvgMock') - expect(action.default).toBe(false) + expect(action.iconSvgInline([], view)).toBe('SvgMock') + expect(action.default).toBeUndefined() expect(action.order).toBe(10) }) }) diff --git a/apps/files/src/actions/sidebarAction.spec.ts b/apps/files/src/actions/sidebarAction.spec.ts index f6594090c53e6..c4750092ebcdd 100644 --- a/apps/files/src/actions/sidebarAction.spec.ts +++ b/apps/files/src/actions/sidebarAction.spec.ts @@ -35,9 +35,9 @@ describe('Open sidebar action conditions tests', () => { test('Default values', () => { expect(action).toBeInstanceOf(FileAction) expect(action.id).toBe('details') - expect(action.displayName([], view)).toBe('Details') - expect(action.iconSvgInline([], view)).toBe('SvgMock') - expect(action.default).toBe(true) + expect(action.displayName([], view)).toBe('Open details') + expect(action.iconSvgInline([], view)).toBe('SvgMock') + expect(action.default).toBeUndefined() expect(action.order).toBe(-50) }) }) diff --git a/apps/files/src/actions/sidebarAction.ts b/apps/files/src/actions/sidebarAction.ts index 4766d2e90df99..141cd75ff190e 100644 --- a/apps/files/src/actions/sidebarAction.ts +++ b/apps/files/src/actions/sidebarAction.ts @@ -23,14 +23,14 @@ import { translate as t } from '@nextcloud/l10n' import InformationSvg from '@mdi/svg/svg/information-variant.svg?raw' import type { Node } from '@nextcloud/files' -import { registerFileAction, FileAction } from '../services/FileAction' +import { registerFileAction, FileAction, DefaultType } from '../services/FileAction' import logger from '../logger.js' export const ACTION_DETAILS = 'details' export const action = new FileAction({ id: ACTION_DETAILS, - displayName: () => t('files', 'Details'), + displayName: () => t('files', 'Open details'), iconSvgInline: () => InformationSvg, // Sidebar currently supports user folder only, /files/USER @@ -60,7 +60,6 @@ export const action = new FileAction({ } }, - default: true, order: -50, }) diff --git a/apps/files/src/actions/viewInFolderAction.spec.ts b/apps/files/src/actions/viewInFolderAction.spec.ts new file mode 100644 index 0000000000000..b16f2663f335d --- /dev/null +++ b/apps/files/src/actions/viewInFolderAction.spec.ts @@ -0,0 +1,161 @@ +/** + * @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 { action } from './viewInFolderAction' +import { expect } from '@jest/globals' +import { File, Folder, Node, Permission } from '@nextcloud/files' +import { FileAction } from '../services/FileAction' +import type { Navigation } from '../services/Navigation' + +const view = { + id: 'files', + name: 'Files', +} as Navigation + +describe('View in folder action conditions tests', () => { + test('Default values', () => { + expect(action).toBeInstanceOf(FileAction) + expect(action.id).toBe('view-in-folder') + expect(action.displayName([], view)).toBe('View in folder') + expect(action.iconSvgInline([], view)).toBe('SvgMock') + expect(action.default).toBeUndefined() + expect(action.order).toBe(80) + }) +}) + +describe('View in folder action enabled tests', () => { + test('Enabled for files', () => { + const file = new File({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt', + owner: 'admin', + mime: 'text/plain', + }) + + expect(action.enabled).toBeDefined() + expect(action.enabled!([file], view)).toBe(true) + }) + + test('Disabled for non-dav ressources', () => { + const file = new File({ + id: 1, + source: 'https://domain.com/foobar.txt', + owner: 'admin', + mime: 'text/plain', + }) + + expect(action.enabled).toBeDefined() + expect(action.enabled!([file], view)).toBe(false) + }) + + test('Disabled if more than one node', () => { + const file1 = new File({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/foo.txt', + owner: 'admin', + mime: 'text/plain', + }) + const file2 = new File({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/bar.txt', + owner: 'admin', + mime: 'text/plain', + }) + + expect(action.enabled).toBeDefined() + expect(action.enabled!([file1, file2], view)).toBe(false) + }) + + test('Disabled for folders', () => { + const folder = new Folder({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/FooBar/', + owner: 'admin', + permissions: Permission.READ, + }) + + expect(action.enabled).toBeDefined() + expect(action.enabled!([folder], view)).toBe(false) + }) +}) + +describe('View in folder action execute tests', () => { + test('View in folder', async () => { + const goToRouteMock = jest.fn() + window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } } + + const file = new File({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt', + owner: 'admin', + mime: 'text/plain', + }) + + const exec = await action.exec(file, view, '/') + // Silent action + expect(exec).toBe(null) + expect(goToRouteMock).toBeCalledTimes(1) + expect(goToRouteMock).toBeCalledWith(null, { view: 'files' }, { dir: '/' }) + }) + + test('View in (sub) folder', async () => { + const goToRouteMock = jest.fn() + window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } } + + const file = new File({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/Bar/foobar.txt', + root: '/files/admin', + owner: 'admin', + mime: 'text/plain', + }) + + const exec = await action.exec(file, view, '/') + // Silent action + expect(exec).toBe(null) + expect(goToRouteMock).toBeCalledTimes(1) + expect(goToRouteMock).toBeCalledWith(null, { view: 'files' }, { dir: '/Foo/Bar' }) + }) + + test('View in folder fails without node', async () => { + const goToRouteMock = jest.fn() + window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } } + + const exec = await action.exec(null as unknown as Node, view, '/') + expect(exec).toBe(false) + expect(goToRouteMock).toBeCalledTimes(0) + }) + + test('View in folder fails without File', async () => { + const goToRouteMock = jest.fn() + window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } } + + const folder = new Folder({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/', + owner: 'admin', + }) + + const exec = await action.exec(folder, view, '/') + expect(exec).toBe(false) + expect(goToRouteMock).toBeCalledTimes(0) + }) +}) diff --git a/apps/files/src/actions/viewInFolderAction.ts b/apps/files/src/actions/viewInFolderAction.ts new file mode 100644 index 0000000000000..67e276112dccf --- /dev/null +++ b/apps/files/src/actions/viewInFolderAction.ts @@ -0,0 +1,68 @@ +/** + * @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 { Node, FileType } from '@nextcloud/files' +import { translate as t } from '@nextcloud/l10n' +import FolderMoveSvg from '@mdi/svg/svg/folder-move.svg?raw' + +import type { Navigation } from '../services/Navigation' +import { join } from 'path' +import { registerFileAction, FileAction } from '../services/FileAction' + +export const action = new FileAction({ + id: 'view-in-folder', + displayName() { + return t('files', 'View in folder') + }, + iconSvgInline: () => FolderMoveSvg, + + enabled(nodes: Node[]) { + // Only works on single node + if (nodes.length !== 1) { + return false + } + + const node = nodes[0] + + if (!node.isDavRessource) { + return false + } + + return node.type === FileType.File + }, + + async exec(node: Node, view: Navigation, dir: string) { + if (!node || node.type !== FileType.File) { + return false + } + + window.OCP.Files.Router.goToRoute( + null, + { view: 'files', fileid: node.fileid }, + { dir: node.dirname, fileid: node.fileid }, + ) + return null + }, + + order: 80, +}) + +registerFileAction(action) diff --git a/apps/files/src/components/FileEntry.vue b/apps/files/src/components/FileEntry.vue index fd61f5e362322..58b914041b2e9 100644 --- a/apps/files/src/components/FileEntry.vue +++ b/apps/files/src/components/FileEntry.vue @@ -33,38 +33,64 @@ - - - - - - - - - - - - - - - + + + + + + + + + + + + + + - + + + +
+ + + +
- {{ displayName }} + + + + + - + @@ -75,12 +101,13 @@ :container="boundariesElement" :disabled="source._loading" :force-title="true" - :force-menu="true" + :force-menu="enabledInlineActions.length === 0 /* forceMenu only if no inline actions */" :inline="enabledInlineActions.length" :open.sync="openedMenu"> @@ -57,6 +59,7 @@