mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-06-28 12:40:08 +02:00
Added autocomplete for part tags input.
This commit is contained in:
parent
bddd5b758a
commit
7a5a2f65f9
4 changed files with 153 additions and 5 deletions
|
@ -231,9 +231,38 @@ $(document).on("ajaxUI:reload", function () {
|
||||||
$(".file").fileinput();
|
$(".file").fileinput();
|
||||||
});
|
});
|
||||||
|
|
||||||
$(document).on("ajaxUI:reload", function () {
|
$(document).on("ajaxUI:start ajaxUI:reload", function () {
|
||||||
//@ts-ignore
|
$('input.tagsinput').each(function() {
|
||||||
$("input[data-role='tagsinput']").tagsinput();
|
|
||||||
|
//Use typeahead if an autocomplete url was specified.
|
||||||
|
if($(this).data('autocomplete')) {
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
var engine = new Bloodhound({
|
||||||
|
//@ts-ignore
|
||||||
|
datumTokenizer: Bloodhound.tokenizers.obj.whitespace(''),
|
||||||
|
//@ts-ignore
|
||||||
|
queryTokenizer: Bloodhound.tokenizers.obj.whitespace(''),
|
||||||
|
remote: {
|
||||||
|
url: $(this).data('autocomplete'),
|
||||||
|
wildcard: 'QUERY'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
$(this).tagsinput({
|
||||||
|
typeaheadjs: {
|
||||||
|
name: 'tags',
|
||||||
|
source: engine.ttAdapter()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
} else { //Init tagsinput without typeahead
|
||||||
|
//@ts-ignore
|
||||||
|
$(this).tagsinput();
|
||||||
|
}
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -24,6 +24,7 @@ namespace App\Controller;
|
||||||
|
|
||||||
|
|
||||||
use App\Services\Attachments\BuiltinAttachmentsFinder;
|
use App\Services\Attachments\BuiltinAttachmentsFinder;
|
||||||
|
use App\Services\TagFinder;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
@ -47,6 +48,24 @@ class TypeaheadController extends AbstractController
|
||||||
$array = $finder->find($query);
|
$array = $finder->find($query);
|
||||||
|
|
||||||
|
|
||||||
|
$normalizers = [
|
||||||
|
new ObjectNormalizer()
|
||||||
|
];
|
||||||
|
$encoders = [
|
||||||
|
new JsonEncoder()
|
||||||
|
];
|
||||||
|
$serializer = new Serializer($normalizers, $encoders);
|
||||||
|
$data = $serializer->serialize($array, 'json');
|
||||||
|
return new JsonResponse($data, 200, [], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/tags/search/{query}", name="typeahead_tags", requirements={"query"= ".+"})
|
||||||
|
*/
|
||||||
|
public function tags(string $query, TagFinder $finder)
|
||||||
|
{
|
||||||
|
$array = $finder->searchTags($query);
|
||||||
|
|
||||||
$normalizers = [
|
$normalizers = [
|
||||||
new ObjectNormalizer()
|
new ObjectNormalizer()
|
||||||
];
|
];
|
||||||
|
|
|
@ -51,6 +51,7 @@ use Symfony\Component\Form\Extension\Core\Type\ResetType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
use Symfony\Component\Security\Core\Security;
|
use Symfony\Component\Security\Core\Security;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
use function foo\func;
|
use function foo\func;
|
||||||
|
@ -59,11 +60,13 @@ class PartBaseType extends AbstractType
|
||||||
{
|
{
|
||||||
protected $security;
|
protected $security;
|
||||||
protected $trans;
|
protected $trans;
|
||||||
|
protected $urlGenerator;
|
||||||
|
|
||||||
public function __construct(Security $security, TranslatorInterface $trans)
|
public function __construct(Security $security, TranslatorInterface $trans, UrlGeneratorInterface $urlGenerator)
|
||||||
{
|
{
|
||||||
$this->security = $security;
|
$this->security = $security;
|
||||||
$this->trans = $trans;
|
$this->trans = $trans;
|
||||||
|
$this->urlGenerator = $urlGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
@ -119,7 +122,9 @@ class PartBaseType extends AbstractType
|
||||||
'required' => false,
|
'required' => false,
|
||||||
'label' => $this->trans->trans('part.edit.tags'),
|
'label' => $this->trans->trans('part.edit.tags'),
|
||||||
'empty_data' => "",
|
'empty_data' => "",
|
||||||
'attr' => ['data-role' => 'tagsinput'],
|
'attr' => [
|
||||||
|
'class' => 'tagsinput',
|
||||||
|
'data-autocomplete' => $this->urlGenerator->generate('typeahead_tags', ['query' => 'QUERY']),],
|
||||||
'disabled' => !$this->security->isGranted('tags.edit', $part)
|
'disabled' => !$this->security->isGranted('tags.edit', $part)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
95
src/Services/TagFinder.php
Normal file
95
src/Services/TagFinder.php
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony)
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
|
||||||
|
use App\Entity\Parts\Part;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service related for searching for tags. Mostly useful for autocomplete reasons.
|
||||||
|
* @package App\Services
|
||||||
|
*/
|
||||||
|
class TagFinder
|
||||||
|
{
|
||||||
|
protected $em;
|
||||||
|
|
||||||
|
public function __construct(EntityManagerInterface $entityManager)
|
||||||
|
{
|
||||||
|
$this->em = $entityManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'query_limit' => 75,
|
||||||
|
'return_limit' => 25,
|
||||||
|
'min_keyword_length' => 3
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search tags that begins with the certain keyword.
|
||||||
|
* @param string $keyword The keyword the tag must begin with
|
||||||
|
* @param array $options Some options specifying the search behavior. See configureOptions for possible options.
|
||||||
|
* @return string[] An array containing the tags that match the given keyword.
|
||||||
|
*/
|
||||||
|
public function searchTags(string $keyword, array $options = [])
|
||||||
|
{
|
||||||
|
$results = [];
|
||||||
|
$keyword_regex = '/^' . preg_quote($keyword, '/') . '/';
|
||||||
|
|
||||||
|
$resolver = new OptionsResolver();
|
||||||
|
$this->configureOptions($resolver);
|
||||||
|
|
||||||
|
$options = $resolver->resolve($options);
|
||||||
|
|
||||||
|
//If the keyword is too short we will get to much results, which takes too much time...
|
||||||
|
if (mb_strlen($keyword) < $options['min_keyword_length']) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
//Build a query to get all
|
||||||
|
$qb = $this->em->createQueryBuilder();
|
||||||
|
|
||||||
|
$qb->select('p.tags')
|
||||||
|
->from(Part::class, 'p')
|
||||||
|
->where("p.tags LIKE ?1")
|
||||||
|
->setMaxResults($options['query_limit'])
|
||||||
|
//->orderBy('RAND()')
|
||||||
|
->setParameter(1, '%' . $keyword . '%');
|
||||||
|
|
||||||
|
$possible_tags = $qb->getQuery()->getArrayResult();
|
||||||
|
|
||||||
|
//Iterate over each possible tags (which are comma separated) and extract tags which match our keyword
|
||||||
|
foreach ($possible_tags as $tags) {
|
||||||
|
$tags = explode(',', $tags['tags']);
|
||||||
|
$results = array_merge($results, preg_grep($keyword_regex, $tags));
|
||||||
|
}
|
||||||
|
|
||||||
|
$results = array_unique($results);
|
||||||
|
//Limit the returned tag count to specified value.
|
||||||
|
return array_slice($results, 0, $options['return_limit']);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue