diff options
author | John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | 2023-09-17 19:05:54 +0200 |
---|---|---|
committer | John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | 2023-09-18 15:33:21 +0200 |
commit | 2845319187c6ae118338575798a3413b4613ecb6 (patch) | |
tree | 7c4ed385a4be2ddd68d810a4e84fbefa9ea67d6a /apps/systemtags | |
parent | e0c778f769a10a1f3edc365cc7aa115f119937aa (diff) | |
download | nextcloud-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.php | 2 | ||||
-rw-r--r-- | apps/systemtags/src/app.js | 131 | ||||
-rw-r--r-- | apps/systemtags/src/init.ts (renamed from apps/systemtags/src/systemtags.js) | 25 | ||||
-rw-r--r-- | apps/systemtags/src/services/api.ts | 8 | ||||
-rw-r--r-- | apps/systemtags/src/services/systemtags.ts | 98 | ||||
-rw-r--r-- | apps/systemtags/src/systemtagsfilelist.js | 355 |
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 -})() |