aboutsummaryrefslogtreecommitdiffstats
path: root/apps/systemtags
diff options
context:
space:
mode:
authorJohn Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>2023-09-17 19:05:54 +0200
committerJohn Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>2023-09-18 15:33:21 +0200
commit2845319187c6ae118338575798a3413b4613ecb6 (patch)
tree7c4ed385a4be2ddd68d810a4e84fbefa9ea67d6a /apps/systemtags
parente0c778f769a10a1f3edc365cc7aa115f119937aa (diff)
downloadnextcloud-server-2845319187c6ae118338575798a3413b4613ecb6.tar.gz
nextcloud-server-2845319187c6ae118338575798a3413b4613ecb6.zip
feat(files): add systemtags view
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
Diffstat (limited to 'apps/systemtags')
-rw-r--r--apps/systemtags/lib/AppInfo/Application.php2
-rw-r--r--apps/systemtags/src/app.js131
-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.ts98
-rw-r--r--apps/systemtags/src/systemtagsfilelist.js355
6 files changed, 122 insertions, 497 deletions
diff --git a/apps/systemtags/lib/AppInfo/Application.php b/apps/systemtags/lib/AppInfo/Application.php
index 8d82931296c..c07f32e81c1 100644
--- a/apps/systemtags/lib/AppInfo/Application.php
+++ b/apps/systemtags/lib/AppInfo/Application.php
@@ -56,7 +56,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');
}
);
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/systemtags.js b/apps/systemtags/src/init.ts
index b4f767e0f12..ff2e2332633 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: 'systemtags',
+ 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..d5ef942b91a
--- /dev/null
+++ b/apps/systemtags/src/services/systemtags.ts
@@ -0,0 +1,98 @@
+/**
+ * @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'
+
+let tagsCache = [] as TagWithId[]
+
+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
+ tagsCache = await fetchTags()
+
+ if (path === '/') {
+ return {
+ folder: new Folder({
+ id: 0,
+ source: generateRemoteUrl('dav/systemtags'),
+ owner: getCurrentUser()?.uid as string,
+ root: '/systemtags',
+ }),
+ 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
-})()