Projects

Projects are a way to link items from different apps via a common interface.

App developers can integrate into projects and provide their own entity types to link with.

Register a resource provider

Things like files, deck cards and talk rooms are called Resources in projects. In order to add your own resource type, we need to create a class implementing the OCP\Collaboration\Resources\IProvider interface.

<?php

namespace OCA\MyApp\Collaboration\Resources;


use OCP\Collaboration\Resources\IProvider;
use OCP\Collaboration\Resources\IResource;
use OCP\IURLGenerator;
use OCP\IUser;

class MyResourceProvider implements IProvider {
    public const RESOURCE_TYPE = 'my-app-item';

    /**
     * @var IURLGenerator
     */
    private $url;

    public function __construct(IURLGenerator $url) {
        $this->url = $url;
    }

    /**
     * @inheritDoc
     */
    public function getType(): string {
        return self::RESOURCE_TYPE;
    }

    /**
     * @inheritDoc
     */
    public function getResourceRichObject(IResource $resource): array {
        $item = $this->getItem($resource);
        $icon = $this->url->linkToRouteAbsolute('myapp.images.get_icon', ['id' => $item->getId()]);
        $resourceUrl = $this->url->linkToRouteAbsolute('bookmarks.page.index', ['item' => $bookmark->getId()]);

        return [
            'type' => self::RESOURCE_TYPE,
            'id' => $resource->getId(),
            'name' => $item->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->getItem($resource);
        if ($bookmark === null) {
            return false;
        }
        return $bookmark->getUserId() === $user->getUID()
    }

    private function getItem(IResource $resource) : ?Item {
        // implement me
    }
}

The MyResourceProvider class needs to be registered during the app bootstrap.

<?php

declare(strict_types=1);

namespace OCA\MyApp\AppInfo;

use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCA\MyApp\Dashboard\MyAppWidget;

class Application extends App implements IBootstrap {

    public const APP_ID = 'myapp';

    public function __construct(array $urlParams = []) {
        parent::__construct(self::APP_ID, $urlParams);
    }

    public function register(IRegistrationContext $context): void {
    }

    public function boot(IBootContext $context): void {
        $context->injectFn(Closure::fromCallable([$this, 'registerCollaborationResources']));
    }

    protected function registerCollaborationResources(IProviderManager $resourceManager, IEventDispatcher $eventDispatcher): void {
        $resourceManager->registerResourceProvider(ResourceProvider::class);

        $eventDispatcher->addListener(\OCP\Collaboration\Resources\LoadAdditionalScriptsEvent::class, static function () {
            Util::addScript(self::APP_ID, 'collections');
        });
    }
}

As you can see we also already register a front-end script, which we are going to create next.

Provide a user interface

The user interface can be registered through the public OCP.Collaboration.registerType JavaScript method. The first parameter represents the resource type that has already been specified in the IResourceProvider implementation. The second parameter is an object with three properties:

  • typeString A localized string that will be displayed in the dropdown when choosing which resource type to link to

  • typeIconClass A CSS class of the icon that should be used for this entry

  • action An async function that will produce a resource picker UI and resolves with the resource id

The following example shows how a Vue.js component could be used to render the widget user interface, however this approach works for any other framework as well as plain JavaScript:

    import Vue from 'vue'
    import ItemPickerDialog from './components/ItemPickerDialog'

    OCP.Collaboration.registerType('myapp', {
    action: () => {
        return new Promise((resolve, reject) => {
            const container = document.createElement('div')
            container.id = 'myapp-item-select'
            const body = document.getElementById('body-user')
            body.appendChild(container)
            const ComponentVM = new Vue({
                render: h => h(ItemPickerDialog),
            })
            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('myapp', 'Link to an item'),
    typeIconClass: 'icon-file',
})

This will allow other apps to link to your items. We also want to link to other apps’ items. Since all apps with projects support are listening on the above LoadAdditionalScriptsEvent, we can simply dispatch it when we render our main page template.

<?php

class MyController extends Controller {
    private IEventDispatcher $eventDispatcher;

    public function __construct(string $appName, IRequest $request, IEventDispatcher $eventDispatcher) {
        parent::__construct($appName, $request);
        $this->eventDispatcher = $eventDispatcher;
    }

    public function index() {
        $this->eventDispatcher->dispatchTyped(new \OCP\Collaboration\Resources\LoadAdditionalScriptsEvent());
        return new TemplateResponse('my_app', 'main');
    }
}

In our Vue app, we can then render the pre-built projects picker available in the npm package nextcloud-vue-collections.

<template>
    <div>
        <CollectionList v-if="itemId"
            :id="itemId"
            :name="itemTitle"
            type="myapp" />
    </div>
</template>

<script>
import { CollectionList } from 'nextcloud-vue-collections'
export default {
    name: 'CollaborationView',
    components: {
        CollectionList,
    },
    props: {
        id: {
            type: String,
            default: '',
        },
        name: {
            type: String,
            default: '',
        }
    }
}
</script>