From 164fd3af2a351c77cd7524be6d93b26ccf0cc302 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Sun, 13 Mar 2022 12:27:44 +0100 Subject: [PATCH] First pass at implementing projects Signed-off-by: Marcel Klehr --- lib/AppInfo/Application.php | 19 ++++ .../Resources/ResourceProvider.php | 96 +++++++++++++++++++ lib/Db/BookmarkMapper.php | 2 +- package-lock.json | 66 ++++++++++++- package.json | 1 + src/collections.js | 75 +++++++++++++++ src/components/FolderPickerDialog.vue | 3 +- src/components/SidebarBookmark.vue | 12 ++- templates/main.php | 1 + webpack.js | 1 + 10 files changed, 271 insertions(+), 5 deletions(-) create mode 100644 lib/Collaboration/Resources/ResourceProvider.php create mode 100644 src/collections.js diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index d48ecc0b..2855f030 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -8,7 +8,10 @@ namespace OCA\Bookmarks\AppInfo; +use Closure; +use OC\EventDispatcher\SymfonyAdapter; use OCA\Bookmarks\Activity\ActivityPublisher; +use OCA\Bookmarks\Collaboration\Resources\ResourceProvider; use OCA\Bookmarks\Dashboard\Frequent; use OCA\Bookmarks\Dashboard\Recent; use OCA\Bookmarks\Events\BeforeDeleteEvent; @@ -26,6 +29,7 @@ use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent; +use OCP\Collaboration\Resources\IProviderManager; use OCP\EventDispatcher\IEventDispatcher; use OCP\Group\Events\UserAddedEvent; use OCP\Group\Events\UserRemovedEvent; @@ -33,6 +37,7 @@ use OCP\IRequest; use OCP\IUser; use OCP\IUserSession; use OCP\User\Events\BeforeUserDeletedEvent; +use OCP\Util; class Application extends App implements IBootstrap { public const APP_ID = 'bookmarks'; @@ -77,8 +82,22 @@ class Application extends App implements IBootstrap { $context->registerMiddleware(ExceptionMiddleware::class); } + /** + * @throws \Psr\Container\ContainerExceptionInterface + * @throws \Psr\Container\NotFoundExceptionInterface + * @throws \Throwable + */ public function boot(IBootContext $context): void { $container = $context->getServerContainer(); CreateBookmark::register($container->get(IEventDispatcher::class)); + $context->injectFn(Closure::fromCallable([$this, 'registerCollaborationResources'])); + } + + protected function registerCollaborationResources(IProviderManager $resourceManager, SymfonyAdapter $symfonyAdapter): void { + $resourceManager->registerResourceProvider(ResourceProvider::class); + + $symfonyAdapter->addListener('\OCP\Collaboration\Resources::loadAdditionalScripts', static function () { + Util::addScript('bookmarks', 'bookmarks-collections'); + }); } } diff --git a/lib/Collaboration/Resources/ResourceProvider.php b/lib/Collaboration/Resources/ResourceProvider.php new file mode 100644 index 00000000..f7619320 --- /dev/null +++ b/lib/Collaboration/Resources/ResourceProvider.php @@ -0,0 +1,96 @@ +bookmarkMapper = $bookmarkMapper; + $this->url = $url; + $this->authorizer = $authorizer; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function getType(): string { + return self::RESOURCE_TYPE; + } + + /** + * @inheritDoc + */ + public function getResourceRichObject(IResource $resource): array { + $bookmark = $this->getBookmark($resource); + $favicon = $this->url->linkToRouteAbsolute('bookmarks.internal_bookmark.get_bookmark_favicon', ['id' => $bookmark->getId()]); + $resourceUrl = $this->url->linkToRouteAbsolute('bookmarks.web_view.indexbookmark', ['bookmark' => $bookmark->getId()]); + + return [ + 'type' => self::RESOURCE_TYPE, + 'id' => $resource->getId(), + 'name' => $bookmark->getTitle(), + 'link' => $resourceUrl, + 'iconUrl' => $favicon, + ]; + } + + /** + * @inheritDoc + */ + public function canAccessResource(IResource $resource, ?IUser $user): bool { + if ($resource->getType() !== self::RESOURCE_TYPE || !($user instanceof IUser)) { + return false; + } + $bookmark = $this->getBookmark($resource); + if ($bookmark === null) { + return false; + } + if ($bookmark->getUserId() === $user->getUID()) { + return true; + } + $permissions = $this->authorizer->getUserPermissionsForBookmark($user->getUID(), $bookmark->getId()); + return Authorizer::hasPermission(Authorizer::PERM_READ, $permissions); + } + + private function getBookmark(IResource $resource) : ?Bookmark { + try { + return $this->bookmarkMapper->find((int) $resource->getId()); + } catch (MultipleObjectsReturnedException|DoesNotExistException $e) { + return null; + } + } +} diff --git a/lib/Db/BookmarkMapper.php b/lib/Db/BookmarkMapper.php index 4ea079a9..e6048feb 100644 --- a/lib/Db/BookmarkMapper.php +++ b/lib/Db/BookmarkMapper.php @@ -178,7 +178,7 @@ class BookmarkMapper extends QBMapper { * @throws DoesNotExistException if not found * @throws MultipleObjectsReturnedException if more than one result */ - public function find(int $id): Entity { + public function find(int $id): Bookmark { $qb = $this->db->getQueryBuilder(); $qb ->select(Bookmark::$columns) diff --git a/package-lock.json b/package-lock.json index 22f39442..b8729292 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bookmarks", - "version": "10.0.3", + "version": "10.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "bookmarks", - "version": "10.0.3", + "version": "10.1.0", "license": "AGPL-3.0-or-later", "dependencies": { "@nextcloud/auth": "^1.3.0", @@ -23,6 +23,7 @@ "humanize-duration": "^3.27.1", "linkify-it": "^3.0.3", "lodash": "^4.17.21", + "nextcloud-vue-collections": "^0.9.0", "sanitize-html": "^2.7.0", "vue": "^2.6.14", "vue-click-outside": "^1.1.0", @@ -8880,6 +8881,39 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "peer": true }, + "node_modules/nextcloud-vue-collections": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/nextcloud-vue-collections/-/nextcloud-vue-collections-0.9.0.tgz", + "integrity": "sha512-GItjPWV4O53CNRPxdRegjEpZUM2ZV1mun2gVM/tLr3Nc2WzchgjAEzHjLxWomdW7kRv0sFJNS20udYJ2wEX76Q==", + "dependencies": { + "@nextcloud/axios": "^1.5.0", + "@nextcloud/browserslist-config": "^1.0.0", + "@nextcloud/router": "^1.2.0", + "@nextcloud/vue": "^3.1.2", + "lodash": "^4.17.20", + "vue": "^2.6.12" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "@nextcloud/vue": "^3.1.2", + "vue": "^2.6.12" + } + }, + "node_modules/nextcloud-vue-collections/node_modules/@nextcloud/browserslist-config": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@nextcloud/browserslist-config/-/browserslist-config-1.0.0.tgz", + "integrity": "sha512-f+sKpdLZXkODV+OY39K1M+Spmd4RgxmtEXmNn4Bviv4R7uBFHXuw+JX9ZdfDeOryfHjJ/TRQxQEp0GMpBwZFUw==" + }, + "node_modules/nextcloud-vue-collections/node_modules/@nextcloud/router": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@nextcloud/router/-/router-1.2.0.tgz", + "integrity": "sha512-kn9QsL9LuhkIMaSSgdiqRL3SZ6PatuAjXUiyq343BbSnI99Oc5eJH8kU6cT2AHije7wKy/tK8Xe3VQuVO32SZQ==", + "dependencies": { + "core-js": "^3.6.4" + } + }, "node_modules/node-forge": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz", @@ -21229,6 +21263,34 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "peer": true }, + "nextcloud-vue-collections": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/nextcloud-vue-collections/-/nextcloud-vue-collections-0.9.0.tgz", + "integrity": "sha512-GItjPWV4O53CNRPxdRegjEpZUM2ZV1mun2gVM/tLr3Nc2WzchgjAEzHjLxWomdW7kRv0sFJNS20udYJ2wEX76Q==", + "requires": { + "@nextcloud/axios": "^1.5.0", + "@nextcloud/browserslist-config": "^1.0.0", + "@nextcloud/router": "^1.2.0", + "@nextcloud/vue": "^3.1.2", + "lodash": "^4.17.20", + "vue": "^2.6.12" + }, + "dependencies": { + "@nextcloud/browserslist-config": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@nextcloud/browserslist-config/-/browserslist-config-1.0.0.tgz", + "integrity": "sha512-f+sKpdLZXkODV+OY39K1M+Spmd4RgxmtEXmNn4Bviv4R7uBFHXuw+JX9ZdfDeOryfHjJ/TRQxQEp0GMpBwZFUw==" + }, + "@nextcloud/router": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@nextcloud/router/-/router-1.2.0.tgz", + "integrity": "sha512-kn9QsL9LuhkIMaSSgdiqRL3SZ6PatuAjXUiyq343BbSnI99Oc5eJH8kU6cT2AHije7wKy/tK8Xe3VQuVO32SZQ==", + "requires": { + "core-js": "^3.6.4" + } + } + } + }, "node-forge": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz", diff --git a/package.json b/package.json index bd1cff9f..72d07663 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "humanize-duration": "^3.27.1", "linkify-it": "^3.0.3", "lodash": "^4.17.21", + "nextcloud-vue-collections": "^0.9.0", "sanitize-html": "^2.7.0", "vue": "^2.6.14", "vue-click-outside": "^1.1.0", diff --git a/src/collections.js b/src/collections.js new file mode 100644 index 00000000..c81d16f8 --- /dev/null +++ b/src/collections.js @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2022. The Nextcloud Bookmarks contributors. + * + * This file is licensed under the Affero General Public License version 3 or later. See the COPYING file. + */ + +import Vue from 'vue' +import FolderPickerDialog from './components/FolderPickerDialog' + +// eslint-disable-next-line no-unexpected-multiline +(function(OCP, OC) { + + // eslint-disable-next-line + __webpack_nonce__ = btoa(OC.requestToken) + // eslint-disable-next-line + __webpack_public_path__ = OC.linkTo('bookmarks', 'js/') + + Vue.prototype.t = t + Vue.prototype.n = n + Vue.prototype.OC = OC + + OCP.Collaboration.registerType('bookmarks', { + action: () => { + return new Promise((resolve, reject) => { + const container = document.createElement('div') + container.id = 'bookmarks-bookmark-select' + const body = document.getElementById('body-user') + body.appendChild(container) + const ComponentVM = new Vue({ + render: h => h(FolderPickerDialog), + }) + ComponentVM.$mount(container) + ComponentVM.$root.$on('close', () => { + ComponentVM.$el.remove() + ComponentVM.$destroy() + reject(new Error('User cancelled resource selection')) + }) + ComponentVM.$root.$on('select', (id) => { + resolve(id) + ComponentVM.$el.remove() + ComponentVM.$destroy() + }) + }) + }, + typeString: t('bookmarks', 'Link to a bookmark'), + typeIconClass: 'icon-favorite', + }) + + OCP.Collaboration.registerType('bookmarks::folder', { + action: () => { + return new Promise((resolve, reject) => { + const container = document.createElement('div') + container.id = 'bookmarks-bookmark-folder-select' + const body = document.getElementById('body-user') + body.appendChild(container) + const ComponentVM = new Vue({ + render: h => h(FolderPickerDialog), + }) + ComponentVM.$mount(container) + ComponentVM.$root.$on('close', () => { + ComponentVM.$el.remove() + ComponentVM.$destroy() + reject(new Error('User cancelled resource selection')) + }) + ComponentVM.$root.$on('select', (id) => { + resolve(id) + ComponentVM.$el.remove() + ComponentVM.$destroy() + }) + }) + }, + typeString: t('bookmarks', 'Link to a bookmark folder'), + typeIconClass: 'icon-favorite', + }) +})(window.OCP, window.OC) diff --git a/src/components/FolderPickerDialog.vue b/src/components/FolderPickerDialog.vue index 69eae88a..33010ac5 100644 --- a/src/components/FolderPickerDialog.vue +++ b/src/components/FolderPickerDialog.vue @@ -28,7 +28,7 @@ export default { }, show: { type: Boolean, - required: true, + default: true, }, }, computed: { @@ -40,6 +40,7 @@ export default { methods: { onSelect(folderId) { this.$emit('input', folderId) + this.$emit('select', folderId) this.$emit('close') }, onClose() { diff --git a/src/components/SidebarBookmark.vue b/src/components/SidebarBookmark.vue index eec77ec9..8523e9a6 100644 --- a/src/components/SidebarBookmark.vue +++ b/src/components/SidebarBookmark.vue @@ -95,6 +95,15 @@ {{ t('bookmarks', 'Open file location') }} + + +