summaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
authorJohn Molakvoæ <skjnldsv@users.noreply.github.com>2023-09-19 11:09:43 +0200
committerGitHub <noreply@github.com>2023-09-19 11:09:43 +0200
commit085568b75ccf686761a07aa4605e9f0ea330d64b (patch)
tree101a82d1cd434b9a3a5ef303f7a1238cc58ee35f /apps
parentcd7c3f0a6fa60149bc752df7e5e137c2a0e3b65f (diff)
parentef1abd958c3aff7cfb56d83f5fb3b6c692845e17 (diff)
downloadnextcloud-server-085568b75ccf686761a07aa4605e9f0ea330d64b.tar.gz
nextcloud-server-085568b75ccf686761a07aa4605e9f0ea330d64b.zip
Merge pull request #40475 from nextcloud/feat/f2v/systemtags
Diffstat (limited to 'apps')
-rw-r--r--apps/dav/lib/SystemTag/SystemTagsInUseCollection.php4
-rw-r--r--apps/files/src/actions/downloadAction.ts14
-rw-r--r--apps/files/src/components/FileEntry.vue7
-rw-r--r--apps/files/src/services/Favorites.ts29
-rw-r--r--apps/files/src/services/Files.ts2
-rw-r--r--apps/files/src/services/Recent.ts29
-rw-r--r--apps/files/src/views/FilesList.vue9
-rw-r--r--apps/files/src/views/Sidebar.vue3
-rw-r--r--apps/systemtags/composer/composer/autoload_classmap.php1
-rw-r--r--apps/systemtags/composer/composer/autoload_static.php1
-rw-r--r--apps/systemtags/lib/AppInfo/Application.php15
-rw-r--r--apps/systemtags/lib/Capabilities.php40
-rw-r--r--apps/systemtags/src/app.js131
-rw-r--r--apps/systemtags/src/css/systemtagsfilelist.scss22
-rw-r--r--apps/systemtags/src/init.ts (renamed from apps/systemtags/src/systemtags.js)25
-rw-r--r--apps/systemtags/src/services/api.ts8
-rw-r--r--apps/systemtags/src/services/systemtags.ts97
-rw-r--r--apps/systemtags/src/systemtagsfilelist.js355
-rw-r--r--apps/systemtags/tests/js/systemtagsfilelistSpec.js240
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);
- });
- });
-});