add contactsmenu popover
Signed-off-by: Georg Ehrke <developer@georgehrke.com>
This commit is contained in:
parent
7386bea23f
commit
60f9ed6241
12 changed files with 379 additions and 3 deletions
|
@ -26,6 +26,7 @@ namespace OC\Core\Controller;
|
||||||
|
|
||||||
use OC\Contacts\ContactsMenu\Manager;
|
use OC\Contacts\ContactsMenu\Manager;
|
||||||
use OCP\AppFramework\Controller;
|
use OCP\AppFramework\Controller;
|
||||||
|
use OCP\AppFramework\Http;
|
||||||
use OCP\AppFramework\Http\JSONResponse;
|
use OCP\AppFramework\Http\JSONResponse;
|
||||||
use OCP\IRequest;
|
use OCP\IRequest;
|
||||||
use OCP\IUserSession;
|
use OCP\IUserSession;
|
||||||
|
@ -59,4 +60,20 @@ class ContactsMenuController extends Controller {
|
||||||
return $this->manager->getEntries($this->userSession->getUser(), $filter);
|
return $this->manager->getEntries($this->userSession->getUser(), $filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoAdminRequired
|
||||||
|
*
|
||||||
|
* @param integer $shareType
|
||||||
|
* @param string $shareWith
|
||||||
|
* @return JSONResponse
|
||||||
|
*/
|
||||||
|
public function findOne($shareType, $shareWith) {
|
||||||
|
$contact = $this->manager->findOne($this->userSession->getUser(), $shareType, $shareWith);
|
||||||
|
|
||||||
|
if ($contact) {
|
||||||
|
return $contact;
|
||||||
|
} else {
|
||||||
|
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,6 +87,7 @@
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
> li {
|
> li {
|
||||||
|
position: relative;
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
@ -103,6 +104,7 @@
|
||||||
padding: 3px 6px;
|
padding: 3px 6px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.shareOption {
|
.shareOption {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -185,6 +187,19 @@ a {
|
||||||
color: rgba($color-main-text, .4);
|
color: rgba($color-main-text, .4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.contactsmenu-popover {
|
||||||
|
left: -8px;
|
||||||
|
right: auto;
|
||||||
|
padding: 3px 6px;
|
||||||
|
li.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
&:after {
|
||||||
|
left: 8px;
|
||||||
|
right: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.popovermenu .datepicker {
|
.popovermenu .datepicker {
|
||||||
margin-left: 35px;
|
margin-left: 35px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
"libraries": [
|
"libraries": [
|
||||||
"jquery-showpassword.js",
|
"jquery-showpassword.js",
|
||||||
"jquery.avatar.js",
|
"jquery.avatar.js",
|
||||||
|
"jquery.contactsmenu.js",
|
||||||
"placeholder.js"
|
"placeholder.js"
|
||||||
],
|
],
|
||||||
"modules": [
|
"modules": [
|
||||||
|
|
107
core/js/jquery.contactsmenu.js
Normal file
107
core/js/jquery.contactsmenu.js
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com>
|
||||||
|
* This file is licensed under the Affero General Public License version 3 or
|
||||||
|
* later.
|
||||||
|
* See the COPYING-README file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function ($) {
|
||||||
|
var ENTRY = ''
|
||||||
|
+ '<li>'
|
||||||
|
+ ' <a href="{{hyperlink}}">'
|
||||||
|
+ ' {{#if icon}}<img src="{{icon}}">{{/if}}'
|
||||||
|
+ ' <span>{{title}}</span>'
|
||||||
|
+ ' </a>'
|
||||||
|
+ '</li>';
|
||||||
|
|
||||||
|
$.fn.contactsMenu = function(shareWith, shareType, appendTo) {
|
||||||
|
if (typeof(shareWith) !== 'undefined') {
|
||||||
|
shareWith = String(shareWith);
|
||||||
|
} else {
|
||||||
|
if (typeof(this.data('share-with')) !== 'undefined') {
|
||||||
|
shareWith = this.data('share-with');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof(shareType) !== 'undefined') {
|
||||||
|
shareType = Number(shareType);
|
||||||
|
} else {
|
||||||
|
if (typeof(this.data('share-type')) !== 'undefined') {
|
||||||
|
shareType = this.data('share-type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof(appendTo) === 'undefined') {
|
||||||
|
appendTo = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0 - user, 4 - email, 6 - remote
|
||||||
|
var allowedTypes = [0, 4, 6];
|
||||||
|
if (allowedTypes.indexOf(shareType) === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var $div = this;
|
||||||
|
appendTo.append('<div class="menu popovermenu bubble hidden contactsmenu-popover"><ul><li><a><span class="icon-loading-small"></span></a></li></ul></div>');
|
||||||
|
var $list = appendTo.find('div.contactsmenu-popover');
|
||||||
|
var url = OC.generateUrl('/contactsmenu/findOne');
|
||||||
|
|
||||||
|
$div.click(function() {
|
||||||
|
$list.show();
|
||||||
|
|
||||||
|
if ($list.hasClass('loaded')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$list.addClass('loaded');
|
||||||
|
$.ajax(url, {
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
shareType: shareType,
|
||||||
|
shareWith: shareWith
|
||||||
|
}
|
||||||
|
}).then(function(data) {
|
||||||
|
$list.find('ul').find('li').addClass('hidden');
|
||||||
|
|
||||||
|
var actions;
|
||||||
|
if (!data.topAction) {
|
||||||
|
actions = [{
|
||||||
|
hyperlink: '#',
|
||||||
|
title: t('core', 'No action available')
|
||||||
|
}];
|
||||||
|
} else {
|
||||||
|
actions = [data.topAction].concat(data.actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.forEach(function(action) {
|
||||||
|
var template = Handlebars.compile(ENTRY);
|
||||||
|
$list.find('ul').append(template(action));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (actions.length === 0) {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).click(function(event) {
|
||||||
|
var clickedList = $.contains($list, event.target);
|
||||||
|
var clickedLi = $.contains($div, event.target);
|
||||||
|
|
||||||
|
$div.each(function() {
|
||||||
|
if ($(this).is(event.target)) {
|
||||||
|
clickedLi = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (clickedList) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clickedLi) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$list.hide();
|
||||||
|
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}(jQuery));
|
|
@ -13,5 +13,6 @@
|
||||||
"mimetypelist.js",
|
"mimetypelist.js",
|
||||||
"oc-backbone.js",
|
"oc-backbone.js",
|
||||||
"placeholder.js",
|
"placeholder.js",
|
||||||
"jquery.avatar.js"
|
"jquery.avatar.js",
|
||||||
|
"jquery.contactsmenu.js"
|
||||||
]
|
]
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
'{{#each sharees}}' +
|
'{{#each sharees}}' +
|
||||||
'<li data-share-id="{{shareId}}" data-share-type="{{shareType}}" data-share-with="{{shareWith}}">' +
|
'<li data-share-id="{{shareId}}" data-share-type="{{shareType}}" data-share-with="{{shareWith}}">' +
|
||||||
'<div class="avatar {{#if modSeed}}imageplaceholderseed{{/if}}" data-username="{{shareWith}}" data-displayname="{{shareWithDisplayName}}" {{#if modSeed}}data-seed="{{shareWith}} {{shareType}}"{{/if}}></div>' +
|
'<div class="avatar {{#if modSeed}}imageplaceholderseed{{/if}}" data-username="{{shareWith}}" data-displayname="{{shareWithDisplayName}}" {{#if modSeed}}data-seed="{{shareWith}} {{shareType}}"{{/if}}></div>' +
|
||||||
'<span class="has-tooltip username" title="{{shareWithTitle}}">{{shareWithDisplayName}}</span>' +
|
'<span class="username" title="{{shareWithTitle}}">{{shareWithDisplayName}}</span>' +
|
||||||
'<span class="sharingOptionsGroup">' +
|
'<span class="sharingOptionsGroup">' +
|
||||||
'{{#if editPermissionPossible}}' +
|
'{{#if editPermissionPossible}}' +
|
||||||
'<span class="shareOption">' +
|
'<span class="shareOption">' +
|
||||||
|
@ -361,6 +361,15 @@
|
||||||
this.$('.has-tooltip').tooltip({
|
this.$('.has-tooltip').tooltip({
|
||||||
placement: 'bottom'
|
placement: 'bottom'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.$('ul.shareWithList > li').each(function() {
|
||||||
|
var $this = $(this);
|
||||||
|
|
||||||
|
var shareWith = $this.data('share-with');
|
||||||
|
var shareType = $this.data('share-type');
|
||||||
|
|
||||||
|
$this.find('div.avatar, span.username').contactsMenu(shareWith, shareType, $this);
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
var permissionChangeShareId = parseInt(this._renderPermissionChange, 10);
|
var permissionChangeShareId = parseInt(this._renderPermissionChange, 10);
|
||||||
var shareWithIndex = this.model.findShareWithIndex(permissionChangeShareId);
|
var shareWithIndex = this.model.findShareWithIndex(permissionChangeShareId);
|
||||||
|
|
|
@ -61,6 +61,7 @@ $application->registerRoutes($this, [
|
||||||
['name' => 'Css#getCss', 'url' => '/css/{appName}/{fileName}', 'verb' => 'GET'],
|
['name' => 'Css#getCss', 'url' => '/css/{appName}/{fileName}', 'verb' => 'GET'],
|
||||||
['name' => 'Js#getJs', 'url' => '/js/{appName}/{fileName}', 'verb' => 'GET'],
|
['name' => 'Js#getJs', 'url' => '/js/{appName}/{fileName}', 'verb' => 'GET'],
|
||||||
['name' => 'contactsMenu#index', 'url' => '/contactsmenu/contacts', 'verb' => 'POST'],
|
['name' => 'contactsMenu#index', 'url' => '/contactsmenu/contacts', 'verb' => 'POST'],
|
||||||
|
['name' => 'contactsMenu#findOne', 'url' => '/contactsmenu/findOne', 'verb' => 'POST'],
|
||||||
],
|
],
|
||||||
'ocs' => [
|
'ocs' => [
|
||||||
['root' => '/cloud', 'name' => 'OCS#getCapabilities', 'url' => '/capabilities', 'verb' => 'GET'],
|
['root' => '/cloud', 'name' => 'OCS#getCapabilities', 'url' => '/capabilities', 'verb' => 'GET'],
|
||||||
|
|
|
@ -59,6 +59,50 @@ class ContactsStore {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param IUser $user
|
||||||
|
* @param integer $shareType
|
||||||
|
* @param string $shareWith
|
||||||
|
* @return IEntry|null
|
||||||
|
*/
|
||||||
|
public function findOne(IUser $user, $shareType, $shareWith) {
|
||||||
|
switch($shareType) {
|
||||||
|
case 0:
|
||||||
|
case 6:
|
||||||
|
$filter = ['UID'];
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
$filter = ['EMAIL'];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$userId = $user->getUID();
|
||||||
|
$allContacts = $this->contactsManager->search($shareWith, $filter);
|
||||||
|
$contacts = array_filter($allContacts, function($contact) use ($userId) {
|
||||||
|
return $contact['UID'] !== $userId;
|
||||||
|
});
|
||||||
|
$match = null;
|
||||||
|
|
||||||
|
foreach ($contacts as $contact) {
|
||||||
|
if ($shareType === 4 && isset($contact['EMAIL'])) {
|
||||||
|
if (in_array($shareWith, $contact['EMAIL'])) {
|
||||||
|
$match = $contact;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($shareType === 0 || $shareType === 6) {
|
||||||
|
if ($contact['UID'] === $shareWith && $contact['isLocalSystemBook'] === true) {
|
||||||
|
$match = $contact;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $match ? $this->contactArrayToEntry($match) : null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array $contact
|
* @param array $contact
|
||||||
* @return Entry
|
* @return Entry
|
||||||
|
|
|
@ -51,7 +51,7 @@ class Manager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $user
|
* @param IUser $user
|
||||||
* @param string $filter
|
* @param string $filter
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
|
@ -69,6 +69,21 @@ class Manager {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param IUser $user
|
||||||
|
* @param integer $shareType
|
||||||
|
* @param string $shareWith
|
||||||
|
* @return IEntry
|
||||||
|
*/
|
||||||
|
public function findOne(IUser $user, $shareType, $shareWith) {
|
||||||
|
$entry = $this->store->findOne($user, $shareType, $shareWith);
|
||||||
|
if ($entry) {
|
||||||
|
$this->processEntries([$entry], $user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $entry;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param IEntry[] $entries
|
* @param IEntry[] $entries
|
||||||
* @return IEntry[]
|
* @return IEntry[]
|
||||||
|
|
|
@ -76,4 +76,35 @@ class ContactsMenuControllerTest extends TestCase {
|
||||||
$this->assertEquals($entries, $response);
|
$this->assertEquals($entries, $response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testFindOne() {
|
||||||
|
$user = $this->createMock(IUser::class);
|
||||||
|
$entry = $this->createMock(IEntry::class);
|
||||||
|
$this->userSession->expects($this->once())
|
||||||
|
->method('getUser')
|
||||||
|
->willReturn($user);
|
||||||
|
$this->contactsManager->expects($this->once())
|
||||||
|
->method('findOne')
|
||||||
|
->with($this->equalTo($user), $this->equalTo(42), $this->equalTo('test-search-phrase'))
|
||||||
|
->willReturn($entry);
|
||||||
|
|
||||||
|
$response = $this->controller->findOne(42, 'test-search-phrase');
|
||||||
|
|
||||||
|
$this->assertEquals($entry, $response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFindOne404() {
|
||||||
|
$user = $this->createMock(IUser::class);
|
||||||
|
$this->userSession->expects($this->once())
|
||||||
|
->method('getUser')
|
||||||
|
->willReturn($user);
|
||||||
|
$this->contactsManager->expects($this->once())
|
||||||
|
->method('findOne')
|
||||||
|
->with($this->equalTo($user), $this->equalTo(42), $this->equalTo('test-search-phrase'))
|
||||||
|
->willReturn(null);
|
||||||
|
|
||||||
|
$response = $this->controller->findOne(42, 'test-search-phrase');
|
||||||
|
|
||||||
|
$this->assertEquals([], $response->getData());
|
||||||
|
$this->assertEquals(404, $response->getStatus());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -157,4 +157,94 @@ class ContactsStoreTest extends TestCase {
|
||||||
$this->assertEquals('https://photo', $entries[1]->getAvatar());
|
$this->assertEquals('https://photo', $entries[1]->getAvatar());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testFindOneUser() {
|
||||||
|
$user = $this->createMock(IUser::class);
|
||||||
|
$this->contactsManager->expects($this->once())
|
||||||
|
->method('search')
|
||||||
|
->with($this->equalTo(''), $this->equalTo(['FN']))
|
||||||
|
->willReturn([
|
||||||
|
[
|
||||||
|
'UID' => 123,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'UID' => 'a567',
|
||||||
|
'FN' => 'Darren Roner',
|
||||||
|
'EMAIL' => [
|
||||||
|
'darren@roner.au'
|
||||||
|
],
|
||||||
|
'isLocalSystemBook' => true
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$user->expects($this->once())
|
||||||
|
->method('getUID')
|
||||||
|
->willReturn('user123');
|
||||||
|
|
||||||
|
$entry = $this->contactsStore->findOne($user, 0, 'a567');
|
||||||
|
|
||||||
|
$this->assertEquals([
|
||||||
|
'darren@roner.au'
|
||||||
|
], $entry->getEMailAddresses());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFindOneEMail() {
|
||||||
|
$user = $this->createMock(IUser::class);
|
||||||
|
$this->contactsManager->expects($this->once())
|
||||||
|
->method('search')
|
||||||
|
->with($this->equalTo(''), $this->equalTo(['FN']))
|
||||||
|
->willReturn([
|
||||||
|
[
|
||||||
|
'UID' => 123,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'UID' => 'a567',
|
||||||
|
'FN' => 'Darren Roner',
|
||||||
|
'EMAIL' => [
|
||||||
|
'darren@roner.au'
|
||||||
|
]
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$user->expects($this->once())
|
||||||
|
->method('getUID')
|
||||||
|
->willReturn('user123');
|
||||||
|
|
||||||
|
$entry = $this->contactsStore->findOne($user, 4, 'darren@roner.au');
|
||||||
|
|
||||||
|
$this->assertEquals([
|
||||||
|
'darren@roner.au'
|
||||||
|
], $entry->getEMailAddresses());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFindOneNotSupportedType() {
|
||||||
|
$user = $this->createMock(IUser::class);
|
||||||
|
|
||||||
|
$entry = $this->contactsStore->findOne($user, 42, 'darren@roner.au');
|
||||||
|
|
||||||
|
$this->assertEquals(null, $entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFindOneNoMatches() {
|
||||||
|
$user = $this->createMock(IUser::class);
|
||||||
|
$this->contactsManager->expects($this->once())
|
||||||
|
->method('search')
|
||||||
|
->with($this->equalTo(''), $this->equalTo(['FN']))
|
||||||
|
->willReturn([
|
||||||
|
[
|
||||||
|
'UID' => 123,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'UID' => 'a567',
|
||||||
|
'FN' => 'Darren Roner',
|
||||||
|
'EMAIL' => [
|
||||||
|
'darren@roner.au123'
|
||||||
|
]
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$user->expects($this->once())
|
||||||
|
->method('getUID')
|
||||||
|
->willReturn('user123');
|
||||||
|
|
||||||
|
$entry = $this->contactsStore->findOne($user, 0, 'a567');
|
||||||
|
|
||||||
|
$this->assertEquals(null, $entry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,4 +99,49 @@ class ManagerTest extends TestCase {
|
||||||
$this->assertEquals($expected, $data);
|
$this->assertEquals($expected, $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testFindOne() {
|
||||||
|
$shareTypeFilter = 42;
|
||||||
|
$shareWithFilter = 'foobar';
|
||||||
|
|
||||||
|
$user = $this->createMock(IUser::class);
|
||||||
|
$entry = current($this->generateTestEntries());
|
||||||
|
$provider = $this->createMock(IProvider::class);
|
||||||
|
$this->contactsStore->expects($this->once())
|
||||||
|
->method('findOne')
|
||||||
|
->with($user, $shareTypeFilter, $shareWithFilter)
|
||||||
|
->willReturn($entry);
|
||||||
|
$this->actionProviderStore->expects($this->once())
|
||||||
|
->method('getProviders')
|
||||||
|
->with($user)
|
||||||
|
->willReturn([$provider]);
|
||||||
|
$provider->expects($this->once())
|
||||||
|
->method('process');
|
||||||
|
|
||||||
|
$data = $this->manager->findOne($user, $shareTypeFilter, $shareWithFilter);
|
||||||
|
|
||||||
|
$this->assertEquals($entry, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFindOne404() {
|
||||||
|
$shareTypeFilter = 42;
|
||||||
|
$shareWithFilter = 'foobar';
|
||||||
|
|
||||||
|
$user = $this->createMock(IUser::class);
|
||||||
|
$provider = $this->createMock(IProvider::class);
|
||||||
|
$this->contactsStore->expects($this->once())
|
||||||
|
->method('findOne')
|
||||||
|
->with($user, $shareTypeFilter, $shareWithFilter)
|
||||||
|
->willReturn(null);
|
||||||
|
$this->actionProviderStore->expects($this->never())
|
||||||
|
->method('getProviders')
|
||||||
|
->with($user)
|
||||||
|
->willReturn([$provider]);
|
||||||
|
$provider->expects($this->never())
|
||||||
|
->method('process');
|
||||||
|
|
||||||
|
$data = $this->manager->findOne($user, $shareTypeFilter, $shareWithFilter);
|
||||||
|
|
||||||
|
$this->assertEquals(null, $data);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue