diff options
author | Julius Härtl <jus@bitgrid.net> | 2022-09-01 20:09:33 +0200 |
---|---|---|
committer | Julius Härtl <jus@bitgrid.net> | 2023-03-02 21:01:44 +0100 |
commit | 6130f1a78ea4f7fba3e3690528f2b53a0d9f5b82 (patch) | |
tree | 404bcf6ad7790b7ecda2eed0efb3a9d4618fddbb | |
parent | b6d8fc97afe28bc1b017026447264ba37ed37caa (diff) | |
download | nextcloud-server-6130f1a78ea4f7fba3e3690528f2b53a0d9f5b82.tar.gz nextcloud-server-6130f1a78ea4f7fba3e3690528f2b53a0d9f5b82.zip |
Implement file reference wiget
Signed-off-by: Julius Härtl <jus@bitgrid.net>
-rw-r--r-- | apps/files/composer/composer/autoload_classmap.php | 1 | ||||
-rw-r--r-- | apps/files/composer/composer/autoload_static.php | 1 | ||||
-rw-r--r-- | apps/files/lib/AppInfo/Application.php | 3 | ||||
-rw-r--r-- | apps/files/lib/Listener/RenderReferenceEventListener.php | 39 | ||||
-rw-r--r-- | apps/files/src/reference-files.js | 58 | ||||
-rw-r--r-- | apps/files/src/views/FileReferencePickerElement.vue | 113 | ||||
-rw-r--r-- | apps/files/src/views/ReferenceFileWidget.vue | 182 | ||||
-rw-r--r-- | lib/private/Collaboration/Reference/File/FileReferenceProvider.php | 42 | ||||
-rw-r--r-- | lib/public/RichObjectStrings/Definitions.php | 6 | ||||
-rw-r--r-- | webpack.modules.js | 1 |
10 files changed, 437 insertions, 9 deletions
diff --git a/apps/files/composer/composer/autoload_classmap.php b/apps/files/composer/composer/autoload_classmap.php index ef3480081e0..29ad9921eae 100644 --- a/apps/files/composer/composer/autoload_classmap.php +++ b/apps/files/composer/composer/autoload_classmap.php @@ -50,6 +50,7 @@ return array( 'OCA\\Files\\Helper' => $baseDir . '/../lib/Helper.php', 'OCA\\Files\\Listener\\LegacyLoadAdditionalScriptsAdapter' => $baseDir . '/../lib/Listener/LegacyLoadAdditionalScriptsAdapter.php', 'OCA\\Files\\Listener\\LoadSidebarListener' => $baseDir . '/../lib/Listener/LoadSidebarListener.php', + 'OCA\\Files\\Listener\\RenderReferenceEventListener' => $baseDir . '/../lib/Listener/RenderReferenceEventListener.php', 'OCA\\Files\\Migration\\Version11301Date20191205150729' => $baseDir . '/../lib/Migration/Version11301Date20191205150729.php', 'OCA\\Files\\Migration\\Version12101Date20221011153334' => $baseDir . '/../lib/Migration/Version12101Date20221011153334.php', 'OCA\\Files\\Notification\\Notifier' => $baseDir . '/../lib/Notification/Notifier.php', diff --git a/apps/files/composer/composer/autoload_static.php b/apps/files/composer/composer/autoload_static.php index 4f7872e39df..5ed4124cbde 100644 --- a/apps/files/composer/composer/autoload_static.php +++ b/apps/files/composer/composer/autoload_static.php @@ -65,6 +65,7 @@ class ComposerStaticInitFiles 'OCA\\Files\\Helper' => __DIR__ . '/..' . '/../lib/Helper.php', 'OCA\\Files\\Listener\\LegacyLoadAdditionalScriptsAdapter' => __DIR__ . '/..' . '/../lib/Listener/LegacyLoadAdditionalScriptsAdapter.php', 'OCA\\Files\\Listener\\LoadSidebarListener' => __DIR__ . '/..' . '/../lib/Listener/LoadSidebarListener.php', + 'OCA\\Files\\Listener\\RenderReferenceEventListener' => __DIR__ . '/..' . '/../lib/Listener/RenderReferenceEventListener.php', 'OCA\\Files\\Migration\\Version11301Date20191205150729' => __DIR__ . '/..' . '/../lib/Migration/Version11301Date20191205150729.php', 'OCA\\Files\\Migration\\Version12101Date20221011153334' => __DIR__ . '/..' . '/../lib/Migration/Version12101Date20221011153334.php', 'OCA\\Files\\Notification\\Notifier' => __DIR__ . '/..' . '/../lib/Notification/Notifier.php', diff --git a/apps/files/lib/AppInfo/Application.php b/apps/files/lib/AppInfo/Application.php index 01fe46bb877..e3152c77abc 100644 --- a/apps/files/lib/AppInfo/Application.php +++ b/apps/files/lib/AppInfo/Application.php @@ -44,6 +44,7 @@ use OCA\Files\Event\LoadAdditionalScriptsEvent; use OCA\Files\Event\LoadSidebar; use OCA\Files\Listener\LegacyLoadAdditionalScriptsAdapter; use OCA\Files\Listener\LoadSidebarListener; +use OCA\Files\Listener\RenderReferenceEventListener; use OCA\Files\Notification\Notifier; use OCA\Files\Search\FilesSearchProvider; use OCA\Files\Service\TagService; @@ -53,6 +54,7 @@ use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; +use OCP\Collaboration\Reference\RenderReferenceEvent; use OCP\Collaboration\Resources\IProviderManager; use OCP\IConfig; use OCP\IL10N; @@ -118,6 +120,7 @@ class Application extends App implements IBootstrap { $context->registerEventListener(LoadAdditionalScriptsEvent::class, LegacyLoadAdditionalScriptsAdapter::class); $context->registerEventListener(LoadSidebar::class, LoadSidebarListener::class); + $context->registerEventListener(RenderReferenceEvent::class, RenderReferenceEventListener::class); $context->registerSearchProvider(FilesSearchProvider::class); diff --git a/apps/files/lib/Listener/RenderReferenceEventListener.php b/apps/files/lib/Listener/RenderReferenceEventListener.php new file mode 100644 index 00000000000..121ff745065 --- /dev/null +++ b/apps/files/lib/Listener/RenderReferenceEventListener.php @@ -0,0 +1,39 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * 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/>. + */ + +namespace OCA\Files\Listener; + +use OCP\Collaboration\Reference\RenderReferenceEvent; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; + +class RenderReferenceEventListener implements IEventListener { + public function handle(Event $event): void { + if (!$event instanceof RenderReferenceEvent) { + return; + } + + \OCP\Util::addScript('files', 'reference-files'); + } +} diff --git a/apps/files/src/reference-files.js b/apps/files/src/reference-files.js new file mode 100644 index 00000000000..db563200cd0 --- /dev/null +++ b/apps/files/src/reference-files.js @@ -0,0 +1,58 @@ +/** + * @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net> + * + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * 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 Vue from 'vue' +import { translate as t } from '@nextcloud/l10n' + +import { registerWidget, registerCustomPickerElement, NcCustomPickerRenderResult } from '@nextcloud/vue/dist/Components/NcRichText.js' + +import FileWidget from './views/ReferenceFileWidget.vue' +import FileReferencePickerElement from './views/FileReferencePickerElement.vue' + +Vue.mixin({ + methods: { + t, + }, +}) + +registerWidget('file', (el, { richObjectType, richObject, accessible }) => { + const Widget = Vue.extend(FileWidget) + new Widget({ + propsData: { + richObjectType, + richObject, + accessible, + }, + }).$mount(el) +}) + +registerCustomPickerElement('files', (el, { providerId, accessible }) => { + const Element = Vue.extend(FileReferencePickerElement) + const vueElement = new Element({ + propsData: { + providerId, + accessible, + }, + }).$mount(el) + return new NcCustomPickerRenderResult(vueElement.$el, vueElement) +}, (el, renderResult) => { + renderResult.object.$destroy() +}) diff --git a/apps/files/src/views/FileReferencePickerElement.vue b/apps/files/src/views/FileReferencePickerElement.vue new file mode 100644 index 00000000000..543dba3350d --- /dev/null +++ b/apps/files/src/views/FileReferencePickerElement.vue @@ -0,0 +1,113 @@ +<!-- + - @copyright Copyright (c) 2023 Julius Härtl <jus@bitgrid.net> + - + - @author Julius Härtl <jus@bitgrid.net> + - + - @license GNU AGPL version 3 or any later version + - + - 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/>. + --> + +<template> + <div ref="picker" class="reference-file-picker" /> +</template> + +<script> +import { FilePickerType } from '@nextcloud/dialogs' +import { generateUrl } from '@nextcloud/router' +export default { + name: 'FileReferencePickerElement', + components: { + }, + props: { + providerId: { + type: String, + required: true, + }, + accessible: { + type: Boolean, + default: false, + }, + }, + mounted() { + this.openFilePicker() + window.addEventListener('click', this.onWindowClick) + }, + beforeDestroy() { + window.removeEventListener('click', this.onWindowClick) + }, + methods: { + onWindowClick(e) { + if (e.target.tagName === 'A' && e.target.classList.contains('oc-dialog-close')) { + this.$emit('cancel') + } + }, + async openFilePicker() { + OC.dialogs.filepicker( + t('files', 'Select file or folder to link to'), + (file) => { + const client = OC.Files.getClient() + client.getFileInfo(file).then((_status, fileInfo) => { + this.submit(fileInfo.id) + }) + }, + false, // multiselect + [], // mime filter + false, // modal + FilePickerType.Choose, // type + '', + { + target: this.$refs.picker, + }, + ) + }, + submit(fileId) { + const fileLink = window.location.protocol + '//' + window.location.host + + generateUrl('/f/{fileId}', { fileId }) + this.$emit('submit', fileLink) + }, + }, +} +</script> + +<style scoped lang="scss"> +.reference-file-picker { + flex-grow: 1; + margin-top: 44px; + + &:deep(.oc-dialog) { + transform: none !important; + box-shadow: none !important; + flex-grow: 1 !important; + position: static !important; + width: 100% !important; + height: auto !important; + padding: 0 !important; + max-width: initial; + + .oc-dialog-close { + display: none; + } + + .oc-dialog-buttonrow.onebutton.aside { + position: absolute; + padding: 12px 32px; + } + + .oc-dialog-content { + max-width: 100% !important; + } + } +} +</style> diff --git a/apps/files/src/views/ReferenceFileWidget.vue b/apps/files/src/views/ReferenceFileWidget.vue new file mode 100644 index 00000000000..f0ac7007312 --- /dev/null +++ b/apps/files/src/views/ReferenceFileWidget.vue @@ -0,0 +1,182 @@ +<!-- + - @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net> + - + - @author Julius Härtl <jus@bitgrid.net> + - + - @license GNU AGPL version 3 or any later version + - + - 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/>. + --> + +<template> + <div v-if="!accessible" class="widget-file widget-file--no-access"> + <div class="widget-file--image widget-file--image--icon icon-folder" /> + <div class="widget-file--details"> + <p class="widget-file--title"> + {{ t('files', 'File cannot be accessed') }} + </p> + <p class="widget-file--description"> + {{ t('files', 'You might not have have permissions to view it, ask the sender to share it') }} + </p> + </div> + </div> + <a v-else + class="widget-file" + :href="richObject.link" + @click.prevent="navigate"> + <div class="widget-file--image" :class="filePreviewClass" :style="filePreview" /> + <div class="widget-file--details"> + <p class="widget-file--title">{{ richObject.name }}</p> + <p class="widget-file--description">{{ fileSize }}<br>{{ fileMtime }}</p> + <p class="widget-file--link">{{ filePath }}</p> + </div> + </a> +</template> +<script> +import { generateUrl } from '@nextcloud/router' +import path from 'path' + +export default { + name: 'ReferenceFileWidget', + props: { + richObject: { + type: Object, + required: true, + }, + accessible: { + type: Boolean, + default: true, + }, + }, + data() { + return { + previewUrl: window.OC.MimeType.getIconUrl(this.richObject.mimetype), + } + }, + computed: { + fileSize() { + return window.OC.Util.humanFileSize(this.richObject.size) + }, + fileMtime() { + return window.OC.Util.relativeModifiedDate(this.richObject.mtime * 1000) + }, + filePath() { + return path.dirname(this.richObject.path) + }, + filePreview() { + if (this.previewUrl) { + return { + backgroundImage: 'url(' + this.previewUrl + ')', + } + } + + return { + backgroundImage: 'url(' + window.OC.MimeType.getIconUrl(this.richObject.mimetype) + ')', + } + + }, + filePreviewClass() { + if (this.previewUrl) { + return 'widget-file--image--preview' + } + return 'widget-file--image--icon' + + }, + }, + mounted() { + if (this.richObject['preview-available']) { + const previewUrl = generateUrl('/core/preview?fileId={fileId}&x=250&y=250', { + fileId: this.richObject.id, + }) + const img = new Image() + img.onload = () => { + this.previewUrl = previewUrl + } + img.onerror = err => { + console.error('could not load recommendation preview', err) + } + img.src = previewUrl + } + }, + methods: { + navigate() { + if (OCA.Viewer && OCA.Viewer.mimetypes.indexOf(this.richObject.mimetype) !== -1) { + OCA.Viewer.open({ path: this.richObject.path }) + return + } + window.location = generateUrl('/f/' + this.id) + }, + }, +} +</script> +<style lang="scss" scoped> +.widget-file { + display: flex; + flex-grow: 1; + color: var(--color-main-text) !important; + text-decoration: none !important; + + &--image { + min-width: 40%; + background-position: center; + background-size: cover; + background-repeat: no-repeat; + + &.widget-file--image--icon { + min-width: 88px; + background-size: 44px; + } + } + + &--title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: bold; + } + + &--details { + padding: 12px; + flex-grow: 1; + display: flex; + flex-direction: column; + + p { + margin: 0; + padding: 0; + } + } + + &--description { + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 3; + line-clamp: 3; + -webkit-box-orient: vertical; + } + + &--link { + color: var(--color-text-maxcontrast); + } + + &.widget-file--no-access { + padding: 12px; + + .widget-file--details { + padding: 0; + } + } +} +</style> diff --git a/lib/private/Collaboration/Reference/File/FileReferenceProvider.php b/lib/private/Collaboration/Reference/File/FileReferenceProvider.php index 95e49cdf860..d423a830495 100644 --- a/lib/private/Collaboration/Reference/File/FileReferenceProvider.php +++ b/lib/private/Collaboration/Reference/File/FileReferenceProvider.php @@ -25,8 +25,8 @@ declare(strict_types=1); namespace OC\Collaboration\Reference\File; use OC\User\NoUserException; +use OCP\Collaboration\Reference\ADiscoverableReferenceProvider; use OCP\Collaboration\Reference\IReference; -use OCP\Collaboration\Reference\IReferenceProvider; use OCP\Collaboration\Reference\Reference; use OCP\Files\IMimeTypeDetector; use OCP\Files\InvalidPathException; @@ -34,27 +34,34 @@ use OCP\Files\IRootFolder; use OCP\Files\Node; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; +use OCP\IL10N; use OCP\IPreview; use OCP\IURLGenerator; use OCP\IUserSession; +use OCP\L10N\IFactory; -class FileReferenceProvider implements IReferenceProvider { +class FileReferenceProvider extends ADiscoverableReferenceProvider { private IURLGenerator $urlGenerator; private IRootFolder $rootFolder; private ?string $userId; private IPreview $previewManager; private IMimeTypeDetector $mimeTypeDetector; - - public function __construct(IURLGenerator $urlGenerator, - IRootFolder $rootFolder, - IUserSession $userSession, - IMimeTypeDetector $mimeTypeDetector, - IPreview $previewManager) { + private IL10N $l10n; + + public function __construct( + IURLGenerator $urlGenerator, + IRootFolder $rootFolder, + IUserSession $userSession, + IMimeTypeDetector $mimeTypeDetector, + IPreview $previewManager, + IFactory $l10n + ) { $this->urlGenerator = $urlGenerator; $this->rootFolder = $rootFolder; $this->userId = $userSession->getUser() ? $userSession->getUser()->getUID() : null; $this->previewManager = $previewManager; $this->mimeTypeDetector = $mimeTypeDetector; + $this->l10n = $l10n->get('files'); } public function matchReference(string $referenceText): bool { @@ -145,9 +152,10 @@ class FileReferenceProvider implements IReferenceProvider { 'id' => $file->getId(), 'name' => $file->getName(), 'size' => $file->getSize(), - 'path' => $file->getPath(), + 'path' => $userFolder->getRelativePath($file->getPath()), 'link' => $reference->getUrl(), 'mimetype' => $file->getMimetype(), + 'mtime' => $file->getMTime(), 'preview-available' => $this->previewManager->isAvailable($file) ]); } catch (InvalidPathException|NotFoundException|NotPermittedException|NoUserException $e) { @@ -162,4 +170,20 @@ class FileReferenceProvider implements IReferenceProvider { public function getCacheKey(string $referenceId): ?string { return $this->userId ?? ''; } + + public function getId(): string { + return 'files'; + } + + public function getTitle(): string { + return $this->l10n->t('Files'); + } + + public function getOrder(): int { + return 0; + } + + public function getIconUrl(): string { + return $this->urlGenerator->imagePath('files', 'folder.svg'); + } } diff --git a/lib/public/RichObjectStrings/Definitions.php b/lib/public/RichObjectStrings/Definitions.php index 383d626c155..57da9f4eb30 100644 --- a/lib/public/RichObjectStrings/Definitions.php +++ b/lib/public/RichObjectStrings/Definitions.php @@ -347,6 +347,12 @@ class Definitions { 'description' => 'Whether or not a preview is available. If `no` the mimetype icon should be used', 'example' => 'yes', ], + 'mtime' => [ + 'since' => '25.0.0', + 'required' => false, + 'description' => 'The mtime of the file/folder as unix timestamp', + 'example' => '1661854213', + ], ], ], 'forms-form' => [ diff --git a/webpack.modules.js b/webpack.modules.js index 75524e2fa7f..8bc42d81e3a 100644 --- a/webpack.modules.js +++ b/webpack.modules.js @@ -52,6 +52,7 @@ module.exports = { sidebar: path.join(__dirname, 'apps/files/src', 'sidebar.js'), main: path.join(__dirname, 'apps/files/src', 'main.js'), 'personal-settings': path.join(__dirname, 'apps/files/src', 'main-personal-settings.js'), + 'reference-files': path.join(__dirname, 'apps/files/src', 'reference-files.js'), }, files_sharing: { additionalScripts: path.join(__dirname, 'apps/files_sharing/src', 'additionalScripts.js'), |