Merge pull request #14180 from nextcloud/feature/sharing-recommendations
Show sharing recommendations
This commit is contained in:
commit
d48cc08fc5
6 changed files with 469 additions and 2 deletions
|
@ -94,6 +94,11 @@ return [
|
|||
'url' => '/api/v1/sharees',
|
||||
'verb' => 'GET',
|
||||
],
|
||||
[
|
||||
'name' => 'ShareesAPI#findRecommended',
|
||||
'url' => '/api/v1/sharees_recommended',
|
||||
'verb' => 'GET',
|
||||
],
|
||||
/*
|
||||
* Remote Shares
|
||||
*/
|
||||
|
|
|
@ -29,17 +29,29 @@ declare(strict_types=1);
|
|||
*/
|
||||
namespace OCA\Files_Sharing\Controller;
|
||||
|
||||
use function array_filter;
|
||||
use function array_slice;
|
||||
use function array_values;
|
||||
use Generator;
|
||||
use OC\Collaboration\Collaborators\SearchResult;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\AppFramework\OCS\OCSBadRequestException;
|
||||
use OCP\AppFramework\OCSController;
|
||||
use OCP\Collaboration\Collaborators\ISearch;
|
||||
use OCP\Collaboration\Collaborators\ISearchResult;
|
||||
use OCP\Collaboration\Collaborators\SearchResultType;
|
||||
use OCP\IRequest;
|
||||
use OCP\IConfig;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\Share;
|
||||
use OCP\Share\IManager;
|
||||
use function usort;
|
||||
|
||||
class ShareesAPIController extends OCSController {
|
||||
|
||||
/** @var userId */
|
||||
protected $userId;
|
||||
|
||||
/** @var IConfig */
|
||||
protected $config;
|
||||
|
||||
|
@ -87,6 +99,7 @@ class ShareesAPIController extends OCSController {
|
|||
private $collaboratorSearch;
|
||||
|
||||
/**
|
||||
* @param string $UserId
|
||||
* @param string $appName
|
||||
* @param IRequest $request
|
||||
* @param IConfig $config
|
||||
|
@ -95,6 +108,7 @@ class ShareesAPIController extends OCSController {
|
|||
* @param ISearch $collaboratorSearch
|
||||
*/
|
||||
public function __construct(
|
||||
$UserId,
|
||||
string $appName,
|
||||
IRequest $request,
|
||||
IConfig $config,
|
||||
|
@ -103,7 +117,7 @@ class ShareesAPIController extends OCSController {
|
|||
ISearch $collaboratorSearch
|
||||
) {
|
||||
parent::__construct($appName, $request);
|
||||
|
||||
$this->userId = $UserId;
|
||||
$this->config = $config;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->shareManager = $shareManager;
|
||||
|
@ -212,6 +226,148 @@ class ShareesAPIController extends OCSController {
|
|||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $user
|
||||
* @param int $shareType
|
||||
*
|
||||
* @return Generator<array<string>>
|
||||
*/
|
||||
private function getAllShareesByType(string $user, int $shareType): Generator {
|
||||
$offset = 0;
|
||||
$pageSize = 50;
|
||||
|
||||
while (count($page = $this->shareManager->getSharesBy(
|
||||
$user,
|
||||
$shareType,
|
||||
null,
|
||||
false,
|
||||
$pageSize,
|
||||
$offset
|
||||
))) {
|
||||
foreach ($page as $share) {
|
||||
yield [$share->getSharedWith(), $share->getSharedWithDisplayName() ?? $share->getSharedWith()];
|
||||
}
|
||||
|
||||
$offset += $pageSize;
|
||||
}
|
||||
}
|
||||
|
||||
private function sortShareesByFrequency(array $sharees): array {
|
||||
usort($sharees, function(array $s1, array $s2) {
|
||||
return $s2['count'] - $s1['count'];
|
||||
});
|
||||
return $sharees;
|
||||
}
|
||||
|
||||
private $searchResultTypeMap = [
|
||||
Share::SHARE_TYPE_USER => 'users',
|
||||
Share::SHARE_TYPE_GROUP => 'groups',
|
||||
Share::SHARE_TYPE_REMOTE => 'remotes',
|
||||
Share::SHARE_TYPE_REMOTE_GROUP => 'remote_groups',
|
||||
Share::SHARE_TYPE_EMAIL => 'emails',
|
||||
];
|
||||
|
||||
private function getAllSharees(string $user, array $shareTypes): ISearchResult {
|
||||
$result = [];
|
||||
foreach ($shareTypes as $shareType) {
|
||||
$sharees = $this->getAllShareesByType($user, $shareType);
|
||||
$shareTypeResults = [];
|
||||
foreach ($sharees as list($sharee, $displayname)) {
|
||||
if (!isset($this->searchResultTypeMap[$shareType])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($shareTypeResults[$sharee])) {
|
||||
$shareTypeResults[$sharee] = [
|
||||
'count' => 1,
|
||||
'label' => $displayname,
|
||||
'value' => [
|
||||
'shareType' => $shareType,
|
||||
'shareWith' => $sharee,
|
||||
],
|
||||
];
|
||||
} else {
|
||||
$shareTypeResults[$sharee]['count']++;
|
||||
}
|
||||
}
|
||||
$result = array_merge($result, array_values($shareTypeResults));
|
||||
}
|
||||
|
||||
$top5 = array_slice(
|
||||
$this->sortShareesByFrequency($result),
|
||||
0,
|
||||
5
|
||||
);
|
||||
|
||||
$searchResult = new SearchResult();
|
||||
foreach ($this->searchResultTypeMap as $int => $str) {
|
||||
$searchResult->addResultSet(new SearchResultType($str), [], []);
|
||||
foreach ($top5 as $x) {
|
||||
if ($x['value']['shareType'] === $int) {
|
||||
$searchResult->addResultSet(new SearchResultType($str), [], [$x]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $searchResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*
|
||||
* @param string $itemType
|
||||
* @return DataResponse
|
||||
* @throws OCSBadRequestException
|
||||
*/
|
||||
public function findRecommended(string $itemType = null, $shareType = null): DataResponse {
|
||||
$shareTypes = [
|
||||
Share::SHARE_TYPE_USER,
|
||||
];
|
||||
|
||||
if ($itemType === null) {
|
||||
throw new OCSBadRequestException('Missing itemType');
|
||||
} elseif ($itemType === 'file' || $itemType === 'folder') {
|
||||
if ($this->shareManager->allowGroupSharing()) {
|
||||
$shareTypes[] = Share::SHARE_TYPE_GROUP;
|
||||
}
|
||||
|
||||
if ($this->isRemoteSharingAllowed($itemType)) {
|
||||
$shareTypes[] = Share::SHARE_TYPE_REMOTE;
|
||||
}
|
||||
|
||||
if ($this->isRemoteGroupSharingAllowed($itemType)) {
|
||||
$shareTypes[] = Share::SHARE_TYPE_REMOTE_GROUP;
|
||||
}
|
||||
|
||||
if ($this->shareManager->shareProviderExists(Share::SHARE_TYPE_EMAIL)) {
|
||||
$shareTypes[] = Share::SHARE_TYPE_EMAIL;
|
||||
}
|
||||
|
||||
if ($this->shareManager->shareProviderExists(Share::SHARE_TYPE_ROOM)) {
|
||||
$shareTypes[] = Share::SHARE_TYPE_ROOM;
|
||||
}
|
||||
} else {
|
||||
$shareTypes[] = Share::SHARE_TYPE_GROUP;
|
||||
$shareTypes[] = Share::SHARE_TYPE_EMAIL;
|
||||
}
|
||||
|
||||
// FIXME: DI
|
||||
if (\OC::$server->getAppManager()->isEnabledForUser('circles') && class_exists('\OCA\Circles\ShareByCircleProvider')) {
|
||||
$shareTypes[] = Share::SHARE_TYPE_CIRCLE;
|
||||
}
|
||||
|
||||
if (isset($_GET['shareType']) && is_array($_GET['shareType'])) {
|
||||
$shareTypes = array_intersect($shareTypes, $_GET['shareType']);
|
||||
sort($shareTypes);
|
||||
} else if (is_numeric($shareType)) {
|
||||
$shareTypes = array_intersect($shareTypes, [(int) $shareType]);
|
||||
sort($shareTypes);
|
||||
}
|
||||
|
||||
return new DataResponse(
|
||||
$this->getAllSharees($this->userId, $shareTypes)->asArray()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get out the static call for better testing
|
||||
*
|
||||
|
|
|
@ -50,6 +50,9 @@ class ShareesAPIControllerTest extends TestCase {
|
|||
/** @var ShareesAPIController */
|
||||
protected $sharees;
|
||||
|
||||
/** @var string */
|
||||
protected $uid;
|
||||
|
||||
/** @var IRequest|\PHPUnit_Framework_MockObject_MockObject */
|
||||
protected $request;
|
||||
|
||||
|
@ -62,6 +65,7 @@ class ShareesAPIControllerTest extends TestCase {
|
|||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->uid = 'test123';
|
||||
$this->request = $this->createMock(IRequest::class);
|
||||
$this->shareManager = $this->createMock(IManager::class);
|
||||
|
||||
|
@ -74,6 +78,7 @@ class ShareesAPIControllerTest extends TestCase {
|
|||
$this->collaboratorSearch = $this->createMock(ISearch::class);
|
||||
|
||||
$this->sharees = new ShareesAPIController(
|
||||
$this->uid,
|
||||
'files_sharing',
|
||||
$this->request,
|
||||
$configMock,
|
||||
|
@ -243,6 +248,8 @@ class ShareesAPIControllerTest extends TestCase {
|
|||
->method('allowGroupSharing')
|
||||
->willReturn($allowGroupSharing);
|
||||
|
||||
/** @var string */
|
||||
$uid = 'test123';
|
||||
/** @var IRequest|\PHPUnit_Framework_MockObject_MockObject $request */
|
||||
$request = $this->createMock(IRequest::class);
|
||||
/** @var IURLGenerator|\PHPUnit_Framework_MockObject_MockObject $urlGenerator */
|
||||
|
@ -251,6 +258,7 @@ class ShareesAPIControllerTest extends TestCase {
|
|||
/** @var \PHPUnit_Framework_MockObject_MockObject|\OCA\Files_Sharing\Controller\ShareesAPIController $sharees */
|
||||
$sharees = $this->getMockBuilder('\OCA\Files_Sharing\Controller\ShareesAPIController')
|
||||
->setConstructorArgs([
|
||||
$uid,
|
||||
'files_sharing',
|
||||
$request,
|
||||
$config,
|
||||
|
@ -335,6 +343,8 @@ class ShareesAPIControllerTest extends TestCase {
|
|||
$config->expects($this->never())
|
||||
->method('getAppValue');
|
||||
|
||||
/** @var string */
|
||||
$uid = 'test123';
|
||||
/** @var IRequest|\PHPUnit_Framework_MockObject_MockObject $request */
|
||||
$request = $this->createMock(IRequest::class);
|
||||
/** @var IURLGenerator|\PHPUnit_Framework_MockObject_MockObject $urlGenerator */
|
||||
|
@ -343,6 +353,7 @@ class ShareesAPIControllerTest extends TestCase {
|
|||
/** @var \PHPUnit_Framework_MockObject_MockObject|\OCA\Files_Sharing\Controller\ShareesAPIController $sharees */
|
||||
$sharees = $this->getMockBuilder('\OCA\Files_Sharing\Controller\ShareesAPIController')
|
||||
->setConstructorArgs([
|
||||
$uid,
|
||||
'files_sharing',
|
||||
$request,
|
||||
$config,
|
||||
|
|
BIN
core/js/dist/share_backend.js
vendored
BIN
core/js/dist/share_backend.js
vendored
Binary file not shown.
BIN
core/js/dist/share_backend.js.map
vendored
BIN
core/js/dist/share_backend.js.map
vendored
Binary file not shown.
|
@ -50,6 +50,9 @@
|
|||
/** @type {object} **/
|
||||
_lastSuggestions: undefined,
|
||||
|
||||
/** @type {object} **/
|
||||
_lastRecommendations: undefined,
|
||||
|
||||
/** @type {int} **/
|
||||
_pendingOperationsCount: 0,
|
||||
|
||||
|
@ -382,7 +385,299 @@
|
|||
return this._lastSuggestions.promise;
|
||||
},
|
||||
|
||||
_getRecommendations: function(model) {
|
||||
if (this._lastRecommendations &&
|
||||
this._lastRecommendations.model === model) {
|
||||
return this._lastRecommendations.promise;
|
||||
}
|
||||
|
||||
var deferred = $.Deferred();
|
||||
|
||||
$.get(
|
||||
OC.linkToOCS('apps/files_sharing/api/v1') + 'sharees_recommended',
|
||||
{
|
||||
format: 'json',
|
||||
itemType: model.get('itemType')
|
||||
},
|
||||
function (result) {
|
||||
if (result.ocs.meta.statuscode === 100) {
|
||||
var filter = function(users, groups, remotes, remote_groups, emails, circles, rooms) {
|
||||
if (typeof(emails) === 'undefined') {
|
||||
emails = [];
|
||||
}
|
||||
if (typeof(circles) === 'undefined') {
|
||||
circles = [];
|
||||
}
|
||||
if (typeof(rooms) === 'undefined') {
|
||||
rooms = [];
|
||||
}
|
||||
|
||||
var usersLength;
|
||||
var groupsLength;
|
||||
var remotesLength;
|
||||
var remoteGroupsLength;
|
||||
var emailsLength;
|
||||
var circlesLength;
|
||||
var roomsLength;
|
||||
|
||||
var i, j;
|
||||
|
||||
//Filter out the current user
|
||||
usersLength = users.length;
|
||||
for (i = 0; i < usersLength; i++) {
|
||||
if (users[i].value.shareWith === OC.currentUser) {
|
||||
users.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out the owner of the share
|
||||
if (model.hasReshare()) {
|
||||
usersLength = users.length;
|
||||
for (i = 0 ; i < usersLength; i++) {
|
||||
if (users[i].value.shareWith === model.getReshareOwner()) {
|
||||
users.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var shares = model.get('shares');
|
||||
var sharesLength = shares.length;
|
||||
|
||||
// Now filter out all sharees that are already shared with
|
||||
for (i = 0; i < sharesLength; i++) {
|
||||
var share = shares[i];
|
||||
|
||||
if (share.share_type === OC.Share.SHARE_TYPE_USER) {
|
||||
usersLength = users.length;
|
||||
for (j = 0; j < usersLength; j++) {
|
||||
if (users[j].value.shareWith === share.share_with) {
|
||||
users.splice(j, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (share.share_type === OC.Share.SHARE_TYPE_GROUP) {
|
||||
groupsLength = groups.length;
|
||||
for (j = 0; j < groupsLength; j++) {
|
||||
if (groups[j].value.shareWith === share.share_with) {
|
||||
groups.splice(j, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (share.share_type === OC.Share.SHARE_TYPE_REMOTE) {
|
||||
remotesLength = remotes.length;
|
||||
for (j = 0; j < remotesLength; j++) {
|
||||
if (remotes[j].value.shareWith === share.share_with) {
|
||||
remotes.splice(j, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (share.share_type === OC.Share.SHARE_TYPE_REMOTE_GROUP) {
|
||||
remoteGroupsLength = remote_groups.length;
|
||||
for (j = 0; j < remoteGroupsLength; j++) {
|
||||
if (remote_groups[j].value.shareWith === share.share_with) {
|
||||
remote_groups.splice(j, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (share.share_type === OC.Share.SHARE_TYPE_EMAIL) {
|
||||
emailsLength = emails.length;
|
||||
for (j = 0; j < emailsLength; j++) {
|
||||
if (emails[j].value.shareWith === share.share_with) {
|
||||
emails.splice(j, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (share.share_type === OC.Share.SHARE_TYPE_CIRCLE) {
|
||||
circlesLength = circles.length;
|
||||
for (j = 0; j < circlesLength; j++) {
|
||||
if (circles[j].value.shareWith === share.share_with) {
|
||||
circles.splice(j, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (share.share_type === OC.Share.SHARE_TYPE_ROOM) {
|
||||
roomsLength = rooms.length;
|
||||
for (j = 0; j < roomsLength; j++) {
|
||||
if (rooms[j].value.shareWith === share.share_with) {
|
||||
rooms.splice(j, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
filter(
|
||||
result.ocs.data.exact.users,
|
||||
result.ocs.data.exact.groups,
|
||||
result.ocs.data.exact.remotes,
|
||||
result.ocs.data.exact.remote_groups,
|
||||
result.ocs.data.exact.emails,
|
||||
result.ocs.data.exact.circles,
|
||||
result.ocs.data.exact.rooms
|
||||
);
|
||||
|
||||
var exactUsers = result.ocs.data.exact.users;
|
||||
var exactGroups = result.ocs.data.exact.groups;
|
||||
var exactRemotes = result.ocs.data.exact.remotes || [];
|
||||
var exactRemoteGroups = result.ocs.data.exact.remote_groups || [];
|
||||
var exactEmails = [];
|
||||
if (typeof(result.ocs.data.emails) !== 'undefined') {
|
||||
exactEmails = result.ocs.data.exact.emails;
|
||||
}
|
||||
var exactCircles = [];
|
||||
if (typeof(result.ocs.data.circles) !== 'undefined') {
|
||||
exactCircles = result.ocs.data.exact.circles;
|
||||
}
|
||||
var exactRooms = [];
|
||||
if (typeof(result.ocs.data.rooms) !== 'undefined') {
|
||||
exactRooms = result.ocs.data.exact.rooms;
|
||||
}
|
||||
|
||||
var exactMatches = exactUsers.concat(exactGroups).concat(exactRemotes).concat(exactRemoteGroups).concat(exactEmails).concat(exactCircles).concat(exactRooms);
|
||||
|
||||
filter(
|
||||
result.ocs.data.users,
|
||||
result.ocs.data.groups,
|
||||
result.ocs.data.remotes,
|
||||
result.ocs.data.remote_groups,
|
||||
result.ocs.data.emails,
|
||||
result.ocs.data.circles,
|
||||
result.ocs.data.rooms
|
||||
);
|
||||
|
||||
var users = result.ocs.data.users;
|
||||
var groups = result.ocs.data.groups;
|
||||
var remotes = result.ocs.data.remotes || [];
|
||||
var remoteGroups = result.ocs.data.remote_groups || [];
|
||||
var lookup = result.ocs.data.lookup || [];
|
||||
var emails = [];
|
||||
if (typeof(result.ocs.data.emails) !== 'undefined') {
|
||||
emails = result.ocs.data.emails;
|
||||
}
|
||||
var circles = [];
|
||||
if (typeof(result.ocs.data.circles) !== 'undefined') {
|
||||
circles = result.ocs.data.circles;
|
||||
}
|
||||
var rooms = [];
|
||||
if (typeof(result.ocs.data.rooms) !== 'undefined') {
|
||||
rooms = result.ocs.data.rooms;
|
||||
}
|
||||
|
||||
var suggestions = exactMatches.concat(users).concat(groups).concat(remotes).concat(remoteGroups).concat(emails).concat(circles).concat(rooms).concat(lookup);
|
||||
|
||||
function dynamicSort(property) {
|
||||
return function (a,b) {
|
||||
var aProperty = '';
|
||||
var bProperty = '';
|
||||
if (typeof a[property] !== 'undefined') {
|
||||
aProperty = a[property];
|
||||
}
|
||||
if (typeof b[property] !== 'undefined') {
|
||||
bProperty = b[property];
|
||||
}
|
||||
return (aProperty < bProperty) ? -1 : (aProperty > bProperty) ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort share entries by uuid to properly group them
|
||||
*/
|
||||
var grouped = suggestions.sort(dynamicSort('uuid'));
|
||||
|
||||
var previousUuid = null;
|
||||
var groupedLength = grouped.length;
|
||||
var result = [];
|
||||
/**
|
||||
* build the result array that only contains all contact entries from
|
||||
* merged contacts, if the search term matches its contact name
|
||||
*/
|
||||
for (var i = 0; i < groupedLength; i++) {
|
||||
if (typeof grouped[i].uuid !== 'undefined' && grouped[i].uuid === previousUuid) {
|
||||
grouped[i].merged = true;
|
||||
}
|
||||
if (typeof grouped[i].merged === 'undefined') {
|
||||
result.push(grouped[i]);
|
||||
}
|
||||
previousUuid = grouped[i].uuid;
|
||||
}
|
||||
var moreResultsAvailable =
|
||||
(
|
||||
oc_config['sharing.maxAutocompleteResults'] > 0
|
||||
&& Math.min(perPage, oc_config['sharing.maxAutocompleteResults'])
|
||||
<= Math.max(
|
||||
users.length + exactUsers.length,
|
||||
groups.length + exactGroups.length,
|
||||
remoteGroups.length + exactRemoteGroups.length,
|
||||
remotes.length + exactRemotes.length,
|
||||
emails.length + exactEmails.length,
|
||||
circles.length + exactCircles.length,
|
||||
rooms.length + exactRooms.length,
|
||||
lookup.length
|
||||
)
|
||||
);
|
||||
|
||||
deferred.resolve(result, exactMatches, moreResultsAvailable);
|
||||
} else {
|
||||
deferred.reject(result.ocs.meta.message);
|
||||
}
|
||||
}
|
||||
).fail(function() {
|
||||
deferred.reject();
|
||||
});
|
||||
|
||||
this._lastRecommendations = {
|
||||
model: model,
|
||||
promise: deferred.promise()
|
||||
};
|
||||
|
||||
return this._lastRecommendations.promise;
|
||||
},
|
||||
|
||||
recommendationHandler: function (response) {
|
||||
var view = this;
|
||||
var $shareWithField = $('.shareWithField');
|
||||
this._getRecommendations(
|
||||
view.model
|
||||
).done(function(suggestions, exactMatches) {
|
||||
view._pendingOperationsCount--;
|
||||
if (view._pendingOperationsCount === 0) {
|
||||
$loading.addClass('hidden');
|
||||
$loading.removeClass('inlineblock');
|
||||
$confirm.removeClass('hidden');
|
||||
}
|
||||
|
||||
if (suggestions.length > 0) {
|
||||
$shareWithField
|
||||
.autocomplete("option", "autoFocus", true);
|
||||
|
||||
response(suggestions);
|
||||
} else {
|
||||
console.info('no sharing recommendations found');
|
||||
response();
|
||||
}
|
||||
}).fail(function(message) {
|
||||
view._pendingOperationsCount--;
|
||||
if (view._pendingOperationsCount === 0) {
|
||||
$loading.addClass('hidden');
|
||||
$loading.removeClass('inlineblock');
|
||||
$confirm.removeClass('hidden');
|
||||
}
|
||||
|
||||
console.error('could not load recommendations', message)
|
||||
});
|
||||
},
|
||||
|
||||
autocompleteHandler: function (search, response) {
|
||||
// If nothing is entered we show recommendations instead of search
|
||||
// results
|
||||
if (search.term.length === 0) {
|
||||
this.recommendationHandler(response);
|
||||
return;
|
||||
}
|
||||
|
||||
var $shareWithField = $('.shareWithField'),
|
||||
view = this,
|
||||
$loading = this.$el.find('.shareWithLoading'),
|
||||
|
@ -766,7 +1061,7 @@
|
|||
};
|
||||
|
||||
$shareField.autocomplete({
|
||||
minLength: 1,
|
||||
minLength: 0,
|
||||
delay: 750,
|
||||
focus: function(event) {
|
||||
event.preventDefault();
|
||||
|
|
Loading…
Reference in a new issue