Implement file reference wiget

Signed-off-by: Julius Härtl <jus@bitgrid.net>
pull/33755/head
Julius Härtl 2 years ago
parent b6d8fc97af
commit 6130f1a78e
No known key found for this signature in database
GPG Key ID: 4C614C6ED2CDE6DF

@ -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',

@ -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',

@ -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);

@ -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');
}
}

@ -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()
})

@ -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>

@ -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>

@ -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');
}
}

@ -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' => [

@ -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'),

Loading…
Cancel
Save