diff options
author | John Molakvoæ <skjnldsv@users.noreply.github.com> | 2023-09-19 11:09:43 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-09-19 11:09:43 +0200 |
commit | 085568b75ccf686761a07aa4605e9f0ea330d64b (patch) | |
tree | 101a82d1cd434b9a3a5ef303f7a1238cc58ee35f /apps | |
parent | cd7c3f0a6fa60149bc752df7e5e137c2a0e3b65f (diff) | |
parent | ef1abd958c3aff7cfb56d83f5fb3b6c692845e17 (diff) | |
download | nextcloud-server-085568b75ccf686761a07aa4605e9f0ea330d64b.tar.gz nextcloud-server-085568b75ccf686761a07aa4605e9f0ea330d64b.zip |
Merge pull request #40475 from nextcloud/feat/f2v/systemtags
Diffstat (limited to 'apps')
19 files changed, 201 insertions, 831 deletions
diff --git a/apps/dav/lib/SystemTag/SystemTagsInUseCollection.php b/apps/dav/lib/SystemTag/SystemTagsInUseCollection.php index b57e685e7e7..4ace9bde412 100644 --- a/apps/dav/lib/SystemTag/SystemTagsInUseCollection.php +++ b/apps/dav/lib/SystemTag/SystemTagsInUseCollection.php @@ -99,8 +99,8 @@ class SystemTagsInUseCollection extends SimpleCollection { $tag = new SystemTag((string)$tagData['id'], $tagData['name'], (bool)$tagData['visibility'], (bool)$tagData['editable']); // read only, so we can submit the isAdmin parameter as false generally $node = new SystemTagNode($tag, $user, false, $this->systemTagManager); - $node->setNumberOfFiles($tagData['number_files']); - $node->setReferenceFileId($tagData['ref_file_id']); + $node->setNumberOfFiles((int) $tagData['number_files']); + $node->setReferenceFileId((int) $tagData['ref_file_id']); $children[] = $node; } return $children; diff --git a/apps/files/src/actions/downloadAction.ts b/apps/files/src/actions/downloadAction.ts index 030e0e818ec..ce9f22450e9 100644 --- a/apps/files/src/actions/downloadAction.ts +++ b/apps/files/src/actions/downloadAction.ts @@ -47,7 +47,19 @@ export const action = new FileAction({ iconSvgInline: () => ArrowDownSvg, enabled(nodes: Node[]) { - return nodes.length > 0 && nodes + if (nodes.length === 0) { + return false + } + + // We can download direct dav files. But if we have + // some folders, we need to use the /apps/files/ajax/download.php + // endpoint, which only supports user root folder. + if (nodes.some(node => node.type === FileType.Folder) + && nodes.some(node => !node.root?.startsWith('/files'))) { + return false + } + + return nodes .map(node => node.permissions) .every(permission => (permission & Permission.READ) !== 0) }, diff --git a/apps/files/src/components/FileEntry.vue b/apps/files/src/components/FileEntry.vue index 30ab98c9dc6..81a61843db0 100644 --- a/apps/files/src/components/FileEntry.vue +++ b/apps/files/src/components/FileEntry.vue @@ -190,6 +190,7 @@ import AccountGroupIcon from 'vue-material-design-icons/AccountGroup.vue' import FileIcon from 'vue-material-design-icons/File.vue' import FolderIcon from 'vue-material-design-icons/Folder.vue' import KeyIcon from 'vue-material-design-icons/Key.vue' +import TagIcon from 'vue-material-design-icons/Tag.vue' import LinkIcon from 'vue-material-design-icons/Link.vue' import NetworkIcon from 'vue-material-design-icons/Network.vue' import AccountPlusIcon from 'vue-material-design-icons/AccountPlus.vue' @@ -237,6 +238,7 @@ export default Vue.extend({ NcLoadingIcon, NcTextField, NetworkIcon, + TagIcon, }, props: { @@ -381,6 +383,11 @@ export default Vue.extend({ return KeyIcon } + // System tags + if (this.source?.attributes?.['is-tag']) { + return TagIcon + } + // Link and mail shared folders const shareTypes = Object.values(this.source?.attributes?.['share-types'] || {}).flat() as number[] if (shareTypes.some(type => type === ShareType.SHARE_TYPE_LINK || type === ShareType.SHARE_TYPE_EMAIL)) { diff --git a/apps/files/src/services/Favorites.ts b/apps/files/src/services/Favorites.ts index c993ef57d33..633ba718560 100644 --- a/apps/files/src/services/Favorites.ts +++ b/apps/files/src/services/Favorites.ts @@ -28,6 +28,7 @@ import { getCurrentUser } from '@nextcloud/auth' import { getClient, rootPath } from './WebdavClient' import { getDavNameSpaces, getDavProperties, getDefaultPropfind } from './DavProperties' +import { resultToNode } from './Files' const client = getClient() @@ -47,34 +48,6 @@ interface ResponseProps extends DAVResultResponseProps { size: number, } -const resultToNode = function(node: FileStat): File | Folder { - const props = node.props as ResponseProps - const permissions = davParsePermissions(props?.permissions) - const owner = getCurrentUser()?.uid as string - - const nodeData = { - id: props?.fileid as number || 0, - source: generateRemoteUrl('dav' + rootPath + node.filename), - mtime: new Date(node.lastmod), - mime: node.mime as string, - size: props?.size as number || 0, - permissions, - owner, - root: rootPath, - attributes: { - ...node, - ...props, - hasPreview: props?.['has-preview'], - }, - } - - delete nodeData.attributes.props - - return node.type === 'file' - ? new File(nodeData) - : new Folder(nodeData) -} - export const getContents = async (path = '/'): Promise<ContentsWithRoot> => { const propfindPayload = getDefaultPropfind() diff --git a/apps/files/src/services/Files.ts b/apps/files/src/services/Files.ts index d392dbb7751..fba65ac44ae 100644 --- a/apps/files/src/services/Files.ts +++ b/apps/files/src/services/Files.ts @@ -40,7 +40,7 @@ interface ResponseProps extends DAVResultResponseProps { size: number, } -const resultToNode = function(node: FileStat): File | Folder { +export const resultToNode = function(node: FileStat): File | Folder { const props = node.props as ResponseProps const permissions = davParsePermissions(props?.permissions) const owner = getCurrentUser()?.uid as string diff --git a/apps/files/src/services/Recent.ts b/apps/files/src/services/Recent.ts index cec604ba855..7a585d28fc5 100644 --- a/apps/files/src/services/Recent.ts +++ b/apps/files/src/services/Recent.ts @@ -28,6 +28,7 @@ import { getCurrentUser } from '@nextcloud/auth' import { getClient, rootPath } from './WebdavClient' import { getDavNameSpaces, getDavProperties } from './DavProperties' +import { resultToNode } from './Files' const client = getClient(generateRemoteUrl('dav')) @@ -94,34 +95,6 @@ interface ResponseProps extends DAVResultResponseProps { size: number, } -const resultToNode = function(node: FileStat): File | Folder { - const props = node.props as ResponseProps - const permissions = davParsePermissions(props?.permissions) - const owner = getCurrentUser()?.uid as string - - const nodeData = { - id: props?.fileid as number || 0, - source: generateRemoteUrl('dav' + node.filename), - mtime: new Date(node.lastmod), - mime: node.mime as string, - size: props?.size as number || 0, - permissions, - owner, - root: rootPath, - attributes: { - ...node, - ...props, - hasPreview: props?.['has-preview'], - }, - } - - delete nodeData.attributes.props - - return node.type === 'file' - ? new File(nodeData) - : new Folder(nodeData) -} - export const getContents = async (path = '/'): Promise<ContentsWithRoot> => { const contentsResponse = await client.getDirectoryContents(path, { details: true, diff --git a/apps/files/src/views/FilesList.vue b/apps/files/src/views/FilesList.vue index 0b858a15e4d..b7785e623b0 100644 --- a/apps/files/src/views/FilesList.vue +++ b/apps/files/src/views/FilesList.vue @@ -328,12 +328,21 @@ export default Vue.extend({ }, }, + mounted() { + this.fetchContent() + }, + methods: { async fetchContent() { this.loading = true const dir = this.dir const currentView = this.currentView + if (!currentView) { + logger.debug('The current view doesn\'t exists or is not ready.', { currentView }) + return + } + // If we have a cancellable promise ongoing, cancel it if (typeof this.promise?.cancel === 'function') { this.promise.cancel() diff --git a/apps/files/src/views/Sidebar.vue b/apps/files/src/views/Sidebar.vue index 65e4c302632..b9804d7931e 100644 --- a/apps/files/src/views/Sidebar.vue +++ b/apps/files/src/views/Sidebar.vue @@ -91,6 +91,7 @@ import { emit } from '@nextcloud/event-bus' import { encodePath } from '@nextcloud/paths' import { File, Folder } from '@nextcloud/files' +import { getCapabilities } from '@nextcloud/capabilities' import { getCurrentUser } from '@nextcloud/auth' import { Type as ShareTypes } from '@nextcloud/sharing' import $ from 'jquery' @@ -299,7 +300,7 @@ export default { }, isSystemTagsEnabled() { - return OCA && 'SystemTags' in OCA + return getCapabilities()?.systemtags?.enabled === true }, }, created() { diff --git a/apps/systemtags/composer/composer/autoload_classmap.php b/apps/systemtags/composer/composer/autoload_classmap.php index 604b7df1672..66d788547c6 100644 --- a/apps/systemtags/composer/composer/autoload_classmap.php +++ b/apps/systemtags/composer/composer/autoload_classmap.php @@ -11,6 +11,7 @@ return array( 'OCA\\SystemTags\\Activity\\Provider' => $baseDir . '/../lib/Activity/Provider.php', 'OCA\\SystemTags\\Activity\\Setting' => $baseDir . '/../lib/Activity/Setting.php', 'OCA\\SystemTags\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php', + 'OCA\\SystemTags\\Capabilities' => $baseDir . '/../lib/Capabilities.php', 'OCA\\SystemTags\\Controller\\LastUsedController' => $baseDir . '/../lib/Controller/LastUsedController.php', 'OCA\\SystemTags\\Search\\TagSearchProvider' => $baseDir . '/../lib/Search/TagSearchProvider.php', 'OCA\\SystemTags\\Settings\\Admin' => $baseDir . '/../lib/Settings/Admin.php', diff --git a/apps/systemtags/composer/composer/autoload_static.php b/apps/systemtags/composer/composer/autoload_static.php index 9c77f6d7a43..c1ea8635181 100644 --- a/apps/systemtags/composer/composer/autoload_static.php +++ b/apps/systemtags/composer/composer/autoload_static.php @@ -26,6 +26,7 @@ class ComposerStaticInitSystemTags 'OCA\\SystemTags\\Activity\\Provider' => __DIR__ . '/..' . '/../lib/Activity/Provider.php', 'OCA\\SystemTags\\Activity\\Setting' => __DIR__ . '/..' . '/../lib/Activity/Setting.php', 'OCA\\SystemTags\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php', + 'OCA\\SystemTags\\Capabilities' => __DIR__ . '/..' . '/../lib/Capabilities.php', 'OCA\\SystemTags\\Controller\\LastUsedController' => __DIR__ . '/..' . '/../lib/Controller/LastUsedController.php', 'OCA\\SystemTags\\Search\\TagSearchProvider' => __DIR__ . '/..' . '/../lib/Search/TagSearchProvider.php', 'OCA\\SystemTags\\Settings\\Admin' => __DIR__ . '/..' . '/../lib/Settings/Admin.php', diff --git a/apps/systemtags/lib/AppInfo/Application.php b/apps/systemtags/lib/AppInfo/Application.php index 8d82931296c..7484438092c 100644 --- a/apps/systemtags/lib/AppInfo/Application.php +++ b/apps/systemtags/lib/AppInfo/Application.php @@ -28,6 +28,7 @@ namespace OCA\SystemTags\AppInfo; use OCA\Files\Event\LoadAdditionalScriptsEvent; use OCA\SystemTags\Search\TagSearchProvider; use OCA\SystemTags\Activity\Listener; +use OCA\SystemTags\Capabilities; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; @@ -45,6 +46,7 @@ class Application extends App implements IBootstrap { public function register(IRegistrationContext $context): void { $context->registerSearchProvider(TagSearchProvider::class); + $context->registerCapability(Capabilities::class); } public function boot(IBootContext $context): void { @@ -56,7 +58,7 @@ class Application extends App implements IBootstrap { LoadAdditionalScriptsEvent::class, function () { \OCP\Util::addScript('core', 'systemtags'); - \OCP\Util::addScript(self::APP_ID, 'systemtags'); + \OCP\Util::addInitScript(self::APP_ID, 'init'); } ); @@ -77,16 +79,5 @@ class Application extends App implements IBootstrap { $dispatcher->addListener(MapperEvent::EVENT_ASSIGN, $mapperListener); $dispatcher->addListener(MapperEvent::EVENT_UNASSIGN, $mapperListener); }); - - \OCA\Files\App::getNavigationManager()->add(function () { - $l = \OC::$server->getL10N(self::APP_ID); - return [ - 'id' => 'systemtagsfilter', - 'appname' => self::APP_ID, - 'script' => 'list.php', - 'order' => 25, - 'name' => $l->t('Tags'), - ]; - }); } } diff --git a/apps/systemtags/lib/Capabilities.php b/apps/systemtags/lib/Capabilities.php new file mode 100644 index 00000000000..5da70a17758 --- /dev/null +++ b/apps/systemtags/lib/Capabilities.php @@ -0,0 +1,40 @@ +<?php +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com> + * + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ +namespace OCA\SystemTags; + +use OCP\Capabilities\ICapability; + +class Capabilities implements ICapability { + /** + * @return array{systemtags: array{enabled: true}} + */ + public function getCapabilities() { + $capabilities = [ + 'systemtags' => [ + 'enabled' => true, + ] + ]; + return $capabilities; + } +} diff --git a/apps/systemtags/src/app.js b/apps/systemtags/src/app.js deleted file mode 100644 index 9696f1edbad..00000000000 --- a/apps/systemtags/src/app.js +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Calviño Sánchez <danxuliu@gmail.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - */ - -(function() { - if (!OCA.SystemTags) { - /** - * @namespace - */ - OCA.SystemTags = {} - } - - OCA.SystemTags.App = { - - initFileList($el) { - if (this._fileList) { - return this._fileList - } - - const tagsParam = (new URL(window.location.href)).searchParams.get('tags') - const initialTags = tagsParam ? tagsParam.split(',').map(parseInt) : [] - - this._fileList = new OCA.SystemTags.FileList( - $el, - { - id: 'systemtags', - fileActions: this._createFileActions(), - config: OCA.Files.App.getFilesConfig(), - // 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, - systemTagIds: initialTags, - } - ) - - this._fileList.appName = t('systemtags', 'Tags') - return this._fileList - }, - - removeFileList() { - if (this._fileList) { - this._fileList.$fileList.empty() - } - }, - - _createFileActions() { - // inherit file actions from the files app - const 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.app-systemtags', this._onActionsUpdated) - OCA.Files.fileActions.on('registerAction.app-systemtags', 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(ev) { - if (!this._fileList) { - return - } - - if (ev.action) { - this._fileList.fileActions.registerAction(ev.action) - } else if (ev.defaultAction) { - this._fileList.fileActions.setDefault( - ev.defaultAction.mime, - ev.defaultAction.name - ) - } - }, - - /** - * Destroy the app - */ - destroy() { - OCA.Files.fileActions.off('setDefault.app-systemtags', this._onActionsUpdated) - OCA.Files.fileActions.off('registerAction.app-systemtags', this._onActionsUpdated) - this.removeFileList() - this._fileList = null - delete this._globalActionsInitialized - }, - } - -})() - -window.addEventListener('DOMContentLoaded', function() { - $('#app-content-systemtagsfilter').on('show', function(e) { - OCA.SystemTags.App.initFileList($(e.target)) - }) - $('#app-content-systemtagsfilter').on('hide', function() { - OCA.SystemTags.App.removeFileList() - }) -}) diff --git a/apps/systemtags/src/css/systemtagsfilelist.scss b/apps/systemtags/src/css/systemtagsfilelist.scss deleted file mode 100644 index 4068eb2d8c5..00000000000 --- a/apps/systemtags/src/css/systemtagsfilelist.scss +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (c) 2016 - * - * This file is licensed under the Affero General Public License version 3 - * or later. - * - * See the COPYING-README file. - * - */ -#app-content-systemtagsfilter .select2-container { - width: 30%; - margin-left: 10px; -} - -#app-sidebar .app-sidebar-header__action .tag-label { - cursor: pointer; - padding: 13px 0; - display: flex; - color: var(--color-text-light); - position: relative; - margin-top: -20px; -} diff --git a/apps/systemtags/src/systemtags.js b/apps/systemtags/src/init.ts index b4f767e0f12..3dbb606dc87 100644 --- a/apps/systemtags/src/systemtags.js +++ b/apps/systemtags/src/init.ts @@ -20,10 +20,25 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ +import './actions/inlineSystemTagsAction.js' -import './app.js' -import './systemtagsfilelist.js' -import './css/systemtagsfilelist.scss' -import './actions/inlineSystemTagsAction.ts' +import { translate as t } from '@nextcloud/l10n' +import { Column, Node, View, getNavigation } from '@nextcloud/files' +import TagMultipleSvg from '@mdi/svg/svg/tag-multiple.svg?raw' -window.OCA.SystemTags = OCA.SystemTags +import { getContents } from './services/systemtags.js' + +const Navigation = getNavigation() +Navigation.register(new View({ + id: 'tags', + name: t('systemtags', 'Tags'), + caption: t('systemtags', 'List of tags and their associated files and folders.'), + + emptyTitle: t('systemtags', 'No tags found'), + emptyCaption: t('systemtags', 'Tags you have created will show up here.'), + + icon: TagMultipleSvg, + order: 25, + + getContents, +})) diff --git a/apps/systemtags/src/services/api.ts b/apps/systemtags/src/services/api.ts index e8094aa3a92..91393e0afe4 100644 --- a/apps/systemtags/src/services/api.ts +++ b/apps/systemtags/src/services/api.ts @@ -19,19 +19,17 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ +import type { FileStat, ResponseDataDetailed } from 'webdav' +import type { ServerTag, Tag, TagWithId } from '../types.js' import axios from '@nextcloud/axios' import { generateUrl } from '@nextcloud/router' import { translate as t } from '@nextcloud/l10n' import { davClient } from './davClient.js' -import { formatTag, parseIdFromLocation, parseTags } from '../utils.js' +import { formatTag, parseIdFromLocation, parseTags } from '../utils' import { logger } from '../logger.js' -import type { FileStat, ResponseDataDetailed } from 'webdav' - -import type { ServerTag, Tag, TagWithId } from '../types.js' - const fetchTagsBody = `<?xml version="1.0"?> <d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns"> <d:prop> diff --git a/apps/systemtags/src/services/systemtags.ts b/apps/systemtags/src/services/systemtags.ts new file mode 100644 index 00000000000..4e81ec7dce0 --- /dev/null +++ b/apps/systemtags/src/services/systemtags.ts @@ -0,0 +1,97 @@ +/** + * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com> + * + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ +import type { FileStat, ResponseDataDetailed } from 'webdav' +import type { TagWithId } from '../types' + +import { Folder, type ContentsWithRoot, Permission, getDavNameSpaces, getDavProperties } from '@nextcloud/files' +import { generateRemoteUrl } from '@nextcloud/router' +import { getCurrentUser } from '@nextcloud/auth' + +import { fetchTags } from './api' +import { getClient } from '../../../files/src/services/WebdavClient' +import { resultToNode } from '../../../files/src/services/Files' + +const formatReportPayload = (tagId: number) => `<?xml version="1.0"?> +<oc:filter-files ${getDavNameSpaces()}> + <d:prop> + ${getDavProperties()} + </d:prop> + <oc:filter-rules> + <oc:systemtag>${tagId}</oc:systemtag> + </oc:filter-rules> +</oc:filter-files>` + +const tagToNode = function(tag: TagWithId): Folder { + return new Folder({ + id: tag.id, + source: generateRemoteUrl('dav/systemtags/' + tag.id), + owner: getCurrentUser()?.uid as string, + root: '/systemtags', + permissions: Permission.READ, + attributes: { + ...tag, + 'is-tag': true, + }, + }) +} + +export const getContents = async (path = '/'): Promise<ContentsWithRoot> => { + // List tags in the root + const tagsCache = (await fetchTags()).filter(tag => tag.userVisible) as TagWithId[] + + if (path === '/') { + return { + folder: new Folder({ + id: 0, + source: generateRemoteUrl('dav/systemtags'), + owner: getCurrentUser()?.uid as string, + root: '/systemtags', + permissions: Permission.NONE, + }), + contents: tagsCache.map(tagToNode), + } + } + + const tagId = parseInt(path.replace('/', ''), 10) + const tag = tagsCache.find(tag => tag.id === tagId) + + if (!tag) { + throw new Error('Tag not found') + } + + const folder = tagToNode(tag) + const contentsResponse = await getClient().getDirectoryContents('/', { + details: true, + // Only filter favorites if we're at the root + data: formatReportPayload(tagId), + headers: { + // Patched in WebdavClient.ts + method: 'REPORT', + }, + }) as ResponseDataDetailed<FileStat[]> + + return { + folder, + contents: contentsResponse.data.map(resultToNode), + } + +} diff --git a/apps/systemtags/src/systemtagsfilelist.js b/apps/systemtags/src/systemtagsfilelist.js deleted file mode 100644 index 04ce6aebd4b..00000000000 --- a/apps/systemtags/src/systemtagsfilelist.js +++ /dev/null @@ -1,355 +0,0 @@ -/** - * Copyright (c) 2016 Vincent Petry <pvince81@owncloud.com> - * - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - */ - -(function() { - /** - * @class OCA.SystemTags.FileList - * @augments OCA.Files.FileList - * - * @classdesc SystemTags file list. - * Contains a list of files filtered by system tags. - * - * @param {object} $el container element with existing markup for the .files-controls and a table - * @param {Array} [options] map of options, see other parameters - * @param {Array.<string>} [options.systemTagIds] array of system tag ids to - * filter by - */ - const FileList = function($el, options) { - this.initialize($el, options) - } - FileList.prototype = _.extend( - {}, - OCA.Files.FileList.prototype, - /** @lends OCA.SystemTags.FileList.prototype */ { - id: 'systemtagsfilter', - appName: t('systemtags', 'Tagged files'), - - /** - * Array of system tag ids to filter by - * - * @type {Array.<string>} - */ - _systemTagIds: [], - _lastUsedTags: [], - - _clientSideSort: true, - _allowSelection: false, - - _filterField: null, - - /** - * @private - * @param {object} $el container element - * @param {object} [options] map of options, see other parameters - */ - initialize($el, options) { - OCA.Files.FileList.prototype.initialize.apply(this, arguments) - if (this.initialized) { - return - } - - if (options && options.systemTagIds) { - this._systemTagIds = options.systemTagIds - } - - OC.Plugins.attach('OCA.SystemTags.FileList', this) - - const $controls = this.$el.find('.files-controls').empty() - - _.defer(_.bind(this._getLastUsedTags, this)) - this._initFilterField($controls) - }, - - destroy() { - this.$filterField.remove() - - OCA.Files.FileList.prototype.destroy.apply(this, arguments) - }, - - _getLastUsedTags() { - const self = this - $.ajax({ - type: 'GET', - url: OC.generateUrl('/apps/systemtags/lastused'), - success(response) { - self._lastUsedTags = response - }, - }) - }, - - _initFilterField($container) { - const self = this - this.$filterField = $('<input type="hidden" name="tags"/>') - this.$filterField.val(this._systemTagIds.join(',')) - $container.append(this.$filterField) - this.$filterField.select2({ - placeholder: t('systemtags', 'Select tags to filter by'), - allowClear: false, - multiple: true, - toggleSelect: true, - separator: ',', - query: _.bind(this._queryTagsAutocomplete, this), - - id(tag) { - return tag.id - }, - - initSelection(element, callback) { - const val = $(element) - .val() - .trim() - if (val) { - const tagIds = val.split(',') - const tags = [] - - OC.SystemTags.collection.fetch({ - success() { - _.each(tagIds, function(tagId) { - const tag = OC.SystemTags.collection.get( - tagId - ) - if (!_.isUndefined(tag)) { - tags.push(tag.toJSON()) - } - }) - callback(tags) - self._onTagsChanged({ target: element }) - }, - }) - } else { - // eslint-disable-next-line n/no-callback-literal - callback([]) - } - }, - - formatResult(tag) { - return OC.SystemTags.getDescriptiveTag(tag) - }, - - formatSelection(tag) { - return OC.SystemTags.getDescriptiveTag(tag).outerHTML - }, - - sortResults(results) { - results.sort(function(a, b) { - const aLastUsed = self._lastUsedTags.indexOf(a.id) - const bLastUsed = self._lastUsedTags.indexOf(b.id) - - if (aLastUsed !== bLastUsed) { - if (bLastUsed === -1) { - return -1 - } - if (aLastUsed === -1) { - return 1 - } - return aLastUsed < bLastUsed ? -1 : 1 - } - - // Both not found - return OC.Util.naturalSortCompare(a.name, b.name) - }) - return results - }, - - escapeMarkup(m) { - // prevent double markup escape - return m - }, - formatNoMatches() { - return t('systemtags', 'No tags found') - }, - }) - this.$filterField.parent().children('.select2-container').attr('aria-expanded', 'false') - this.$filterField.on('select2-open', () => { - this.$filterField.parent().children('.select2-container').attr('aria-expanded', 'true') - }) - this.$filterField.on('select2-close', () => { - this.$filterField.parent().children('.select2-container').attr('aria-expanded', 'false') - }) - this.$filterField.on( - 'change', - _.bind(this._onTagsChanged, this) - ) - return this.$filterField - }, - - /** - * Autocomplete function for dropdown results - * - * @param {object} query select2 query object - */ - _queryTagsAutocomplete(query) { - OC.SystemTags.collection.fetch({ - success() { - const results = OC.SystemTags.collection.filterByName( - query.term - ) - - query.callback({ - results: _.invoke(results, 'toJSON'), - }) - }, - }) - }, - - /** - * Event handler for when the URL changed - * - * @param {Event} e the urlchanged event - */ - _onUrlChanged(e) { - if (e.dir) { - const tags = _.filter(e.dir.split('/'), function(val) { - return val.trim() !== '' - }) - this.$filterField.select2('val', tags || []) - this._systemTagIds = tags - this.reload() - } - }, - - _onTagsChanged(ev) { - const val = $(ev.target) - .val() - .trim() - if (val !== '') { - this._systemTagIds = val.split(',') - } else { - this._systemTagIds = [] - } - - this.$el.trigger( - $.Event('changeDirectory', { - dir: this._systemTagIds.join('/'), - }) - ) - this.reload() - }, - - updateEmptyContent() { - const dir = this.getCurrentDirectory() - if (dir === '/') { - // root has special permissions - if (!this._systemTagIds.length) { - // no tags selected - this.$el - .find('.emptyfilelist.emptycontent') - .html( - '<div class="icon-systemtags"></div>' - + '<h2>' - + t( - 'systemtags', - 'Please select tags to filter by' - ) - + '</h2>' - ) - } else { - // tags selected but no results - this.$el - .find('.emptyfilelist.emptycontent') - .html( - '<div class="icon-systemtags"></div>' - + '<h2>' - + t( - 'systemtags', - 'No files found for the selected tags' - ) - + '</h2>' - ) - } - 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() { - return OC.PERMISSION_READ | OC.PERMISSION_DELETE - }, - - updateStorageStatistics() { - // no op because it doesn't have - // storage info like free space / used space - }, - - reload() { - // there is only root - this._setCurrentDir('/', false) - - if (!this._systemTagIds.length) { - // don't reload - this.updateEmptyContent() - this.setFiles([]) - return $.Deferred().resolve() - } - - this._selectedFiles = {} - this._selectionSummary.clear() - if (this._currentFileModel) { - this._currentFileModel.off() - } - this._currentFileModel = null - this.$el.find('.select-all').prop('checked', false) - this.showMask() - this._reloadCall = this.filesClient.getFilteredFiles( - { - systemTagIds: this._systemTagIds, - }, - { - properties: this._getWebdavProperties(), - } - ) - if (this._detailsView) { - // close sidebar - this._updateDetailsView(null) - } - const callBack = this.reloadCallback.bind(this) - return this._reloadCall.then(callBack, callBack) - }, - - reloadCallback(status, result) { - if (result) { - // prepend empty dir info because original handler - result.unshift({}) - } - - return OCA.Files.FileList.prototype.reloadCallback.call( - this, - status, - result - ) - }, - } - ) - - OCA.SystemTags.FileList = FileList -})() diff --git a/apps/systemtags/tests/js/systemtagsfilelistSpec.js b/apps/systemtags/tests/js/systemtagsfilelistSpec.js deleted file mode 100644 index facdf8dc42c..00000000000 --- a/apps/systemtags/tests/js/systemtagsfilelistSpec.js +++ /dev/null @@ -1,240 +0,0 @@ -/** - * Copyright (c) 2016 Vincent Petry <pvince81@owncloud.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - */ - -describe('OCA.SystemTags.FileList tests', function() { - var FileInfo = OC.Files.FileInfo; - var fileList; - - beforeEach(function() { - // init parameters and test table elements - $('#testArea').append( - '<div id="app-content">' + - // init horrible parameters - '<input type="hidden" id="permissions" value="31"></input>' + - '<div class="files-controls"></div>' + - // dummy table - // TODO: at some point this will be rendered by the fileList class itself! - '<table class="files-filestable">' + - '<thead><tr>' + - '<th class="hidden column-name">' + - '<input type="checkbox" id="select_all_files" class="select-all">' + - '<a class="name columntitle" data-sort="name"><span>Name</span><span class="sort-indicator"></span></a>' + - '<span class="selectedActions hidden"></span>' + - '</th>' + - '<th class="hidden column-mtime">' + - '<a class="columntitle" data-sort="mtime"><span class="sort-indicator"></span></a>' + - '</th>' + - '</tr></thead>' + - '<tbody class="files-fileList"></tbody>' + - '<tfoot></tfoot>' + - '</table>' + - '<div class="emptyfilelist emptycontent">Empty content message</div>' + - '</div>' - ); - }); - afterEach(function() { - fileList.destroy(); - fileList = undefined; - }); - - describe('filter field', function() { - var select2Stub, oldCollection, fetchTagsStub; - var $tagsField; - - beforeEach(function() { - fetchTagsStub = sinon.stub(OC.SystemTags.SystemTagsCollection.prototype, 'fetch'); - select2Stub = sinon.stub($.fn, 'select2'); - oldCollection = OC.SystemTags.collection; - OC.SystemTags.collection = new OC.SystemTags.SystemTagsCollection([ - { - id: '123', - name: 'abc' - }, - { - id: '456', - name: 'def' - } - ]); - - fileList = new OCA.SystemTags.FileList( - $('#app-content'), { - systemTagIds: [] - } - ); - $tagsField = fileList.$el.find('[name=tags]'); - }); - afterEach(function() { - select2Stub.restore(); - fetchTagsStub.restore(); - OC.SystemTags.collection = oldCollection; - }); - it('inits select2 on filter field', function() { - expect(select2Stub.calledOnce).toEqual(true); - }); - it('uses global system tags collection', function() { - var callback = sinon.stub(); - var opts = select2Stub.firstCall.args[0]; - - $tagsField.val('123'); - - opts.initSelection($tagsField, callback); - - expect(callback.notCalled).toEqual(true); - expect(fetchTagsStub.calledOnce).toEqual(true); - - fetchTagsStub.yieldTo('success', fetchTagsStub.thisValues[0]); - - expect(callback.calledOnce).toEqual(true); - expect(callback.lastCall.args[0]).toEqual([ - OC.SystemTags.collection.get('123').toJSON() - ]); - }); - it('fetches tag list from the global collection', function() { - var callback = sinon.stub(); - var opts = select2Stub.firstCall.args[0]; - - $tagsField.val('123'); - - opts.query({ - term: 'de', - callback: callback - }); - - expect(fetchTagsStub.calledOnce).toEqual(true); - expect(callback.notCalled).toEqual(true); - fetchTagsStub.yieldTo('success', fetchTagsStub.thisValues[0]); - - expect(callback.calledOnce).toEqual(true); - expect(callback.lastCall.args[0]).toEqual({ - results: [ - OC.SystemTags.collection.get('456').toJSON() - ] - }); - }); - it('reloads file list after selection', function() { - var reloadStub = sinon.stub(fileList, 'reload'); - $tagsField.val('456,123').change(); - expect(reloadStub.calledOnce).toEqual(true); - reloadStub.restore(); - }); - it('updates URL after selection', function() { - var handler = sinon.stub(); - fileList.$el.on('changeDirectory', handler); - $tagsField.val('456,123').change(); - - expect(handler.calledOnce).toEqual(true); - expect(handler.lastCall.args[0].dir).toEqual('456/123'); - }); - it('updates tag selection when url changed', function() { - fileList.$el.trigger(new $.Event('urlChanged', {dir: '456/123'})); - - expect(select2Stub.lastCall.args[0]).toEqual('val'); - expect(select2Stub.lastCall.args[1]).toEqual(['456', '123']); - }); - }); - - describe('loading results', function() { - var getFilteredFilesSpec, requestDeferred; - - beforeEach(function() { - requestDeferred = new $.Deferred(); - getFilteredFilesSpec = sinon.stub(OC.Files.Client.prototype, 'getFilteredFiles') - .returns(requestDeferred.promise()); - }); - afterEach(function() { - getFilteredFilesSpec.restore(); - }); - - it('renders empty message when no tags were set', function() { - fileList = new OCA.SystemTags.FileList( - $('#app-content'), { - systemTagIds: [] - } - ); - - fileList.reload(); - - expect(fileList.$el.find('.emptyfilelist.emptycontent').hasClass('hidden')).toEqual(false); - - expect(getFilteredFilesSpec.notCalled).toEqual(true); - }); - - it('render files', function(done) { - fileList = new OCA.SystemTags.FileList( - $('#app-content'), { - systemTagIds: ['123', '456'] - } - ); - - var reloading = fileList.reload(); - - expect(getFilteredFilesSpec.calledOnce).toEqual(true); - expect(getFilteredFilesSpec.lastCall.args[0].systemTagIds).toEqual(['123', '456']); - - var testFiles = [new FileInfo({ - id: 1, - type: 'file', - name: 'One.txt', - mimetype: 'text/plain', - mtime: 123456789, - size: 12, - etag: 'abc', - permissions: OC.PERMISSION_ALL - }), new FileInfo({ - id: 2, - type: 'file', - name: 'Two.jpg', - mimetype: 'image/jpeg', - mtime: 234567890, - size: 12049, - etag: 'def', - permissions: OC.PERMISSION_ALL - }), new FileInfo({ - id: 3, - type: 'file', - name: 'Three.pdf', - mimetype: 'application/pdf', - mtime: 234560000, - size: 58009, - etag: '123', - permissions: OC.PERMISSION_ALL - }), new FileInfo({ - id: 4, - type: 'dir', - name: 'somedir', - mimetype: 'httpd/unix-directory', - mtime: 134560000, - size: 250, - etag: '456', - permissions: OC.PERMISSION_ALL - })]; - - requestDeferred.resolve(207, testFiles); - - return reloading.then(function() { - expect(fileList.$el.find('.emptyfilelist.emptycontent').hasClass('hidden')).toEqual(true); - expect(fileList.$el.find('tbody>tr').length).toEqual(4); - }).then(done, done); - }); - }); -}); |