First pass at implementing projects

Signed-off-by: Marcel Klehr <mklehr@gmx.net>
This commit is contained in:
Marcel Klehr 2022-03-13 12:27:44 +01:00
parent 8e63225541
commit 164fd3af2a
10 changed files with 271 additions and 5 deletions

View File

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

View File

@ -0,0 +1,96 @@
<?php
/*
* 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.
*/
namespace OCA\Bookmarks\Collaboration\Resources;
use OCA\Bookmarks\Db\Bookmark;
use OCA\Bookmarks\Db\BookmarkMapper;
use OCA\Bookmarks\Service\Authorizer;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\Collaboration\Resources\IProvider;
use OCP\Collaboration\Resources\IResource;
use OCP\IURLGenerator;
use OCP\IUser;
use Psr\Log\LoggerInterface;
class ResourceProvider implements IProvider {
public const RESOURCE_TYPE = 'bookmarks';
/**
* @var BookmarkMapper
*/
private $bookmarkMapper;
/**
* @var IURLGenerator
*/
private $url;
/**
* @var Authorizer
*/
private $authorizer;
/**
* @var LoggerInterface
*/
private $logger;
public function __construct(BookmarkMapper $bookmarkMapper, IURLGenerator $url, Authorizer $authorizer, LoggerInterface $logger) {
$this->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;
}
}
}

View File

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

66
package-lock.json generated
View File

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

View File

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

75
src/collections.js Normal file
View File

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

View File

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

View File

@ -95,6 +95,15 @@
<a class="button" :href="archivedFile" target="_blank"><span class="icon-files-dark" /> {{ t('bookmarks', 'Open file location') }}</a>
</div>
</AppSidebarTab>
<AppSidebarTab id="bookmark-projects"
:name="t('bookmarks', 'Projects')"
icon="icon-projects"
:order="1">
<CollectionList v-if="bookmark"
:id="''+bookmark.id"
:name="bookmark.title"
type="bookmarks" />
</AppSidebarTab>
</AppSidebar>
</template>
<script>
@ -106,6 +115,7 @@ import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
import RichContenteditable from '@nextcloud/vue/dist/Components/RichContenteditable'
import FileDocumentIcon from 'vue-material-design-icons/FileDocument'
import FolderIcon from 'vue-material-design-icons/Folder'
import { CollectionList } from 'nextcloud-vue-collections'
import { getCurrentUser } from '@nextcloud/auth'
import { generateRemoteUrl, generateUrl } from '@nextcloud/router'
@ -116,7 +126,7 @@ const MAX_RELATIVE_DATE = 1000 * 60 * 60 * 24 * 7 // one week
export default {
name: 'SidebarBookmark',
components: { AppSidebar, AppSidebarTab, Multiselect, Actions, ActionButton, RichContenteditable, FileDocumentIcon, FolderIcon },
components: { AppSidebar, AppSidebarTab, Multiselect, Actions, ActionButton, RichContenteditable, FileDocumentIcon, FolderIcon, CollectionList },
data() {
return {
title: '',

View File

@ -1,4 +1,5 @@
<?php
script('bookmarks', 'bookmarks-main');
\OC::$server->getEventDispatcher()->dispatch('\OCP\Collaboration\Resources::loadAdditionalScripts');
?>
<div id="vue-content"></div>

View File

@ -8,3 +8,4 @@ webpackConfig.entry['service-worker'] = path.join(__dirname, 'src', 'service-wor
webpackConfig.entry.flow = path.join(__dirname, 'src', 'flow.js')
webpackConfig.entry.dashboard = path.join(__dirname, 'src', 'dashboard.js')
webpackConfig.entry.talk = path.join(__dirname, 'src', 'talk.js')
webpackConfig.entry.collections = path.join(__dirname, 'src', 'collections.js')