User search refactored, autocomplete added. bootstrap3-typeahead jQuery plugin added.

This commit is contained in:
Alexey Skobkin 2016-12-11 01:48:51 +03:00
parent d71ea8a116
commit d0c103eae0
8 changed files with 162 additions and 30 deletions

View file

@ -35,6 +35,9 @@ sensio_framework_extra:
# Twig Configuration # Twig Configuration
twig: twig:
form:
resources:
- bootstrap_3_layout.html.twig
debug: "%kernel.debug%" debug: "%kernel.debug%"
strict_variables: "%kernel.debug%" strict_variables: "%kernel.debug%"

View file

@ -3,17 +3,42 @@
namespace Skobkin\Bundle\PointToolsBundle\Controller; namespace Skobkin\Bundle\PointToolsBundle\Controller;
use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManager;
use Doctrine\ORM\QueryBuilder; use Skobkin\Bundle\PointToolsBundle\Form\UserSearchType;
use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Form\FormError;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
class MainController extends Controller class MainController extends Controller
{ {
public function indexAction() public function indexAction(Request $request)
{ {
/** @var EntityManager $em */ /** @var EntityManager $em */
$em = $this->getDoctrine()->getManager(); $em = $this->getDoctrine()->getManager();
$form = $this->createForm(
new UserSearchType(),
null,
[
'action' => $this->generateUrl('index'),
'method' => 'POST',
]
);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$login = $form->get('login')->getData();
if (null !== $user = $em->getRepository('SkobkinPointToolsBundle:User')->findOneBy(['login' => $login])) {
return $this->redirectToRoute('user_show', ['login' => $login]);
}
$form->get('login')->addError(new FormError('Login not found'));
}
return $this->render('SkobkinPointToolsBundle:Main:index.html.twig', [ return $this->render('SkobkinPointToolsBundle:Main:index.html.twig', [
'form' => $form->createView(),
'users_count' => $em->getRepository('SkobkinPointToolsBundle:User')->getUsersCount(), 'users_count' => $em->getRepository('SkobkinPointToolsBundle:User')->getUsersCount(),
'subscribers_count' => $em->getRepository('SkobkinPointToolsBundle:Subscription')->getUserSubscribersCountById($this->container->getParameter('point_id')), 'subscribers_count' => $em->getRepository('SkobkinPointToolsBundle:Subscription')->getUserSubscribersCountById($this->container->getParameter('point_id')),
'events_count' => $em->getRepository('SkobkinPointToolsBundle:SubscriptionEvent')->getLastDayEventsCount(), 'events_count' => $em->getRepository('SkobkinPointToolsBundle:SubscriptionEvent')->getLastDayEventsCount(),
@ -21,4 +46,20 @@ class MainController extends Controller
'last_events' => $em->getRepository('SkobkinPointToolsBundle:SubscriptionEvent')->getLastSubscriptionEvents(10), 'last_events' => $em->getRepository('SkobkinPointToolsBundle:SubscriptionEvent')->getLastSubscriptionEvents(10),
]); ]);
} }
public function searchUserAjaxAction($login)
{
$em = $this->getDoctrine()->getManager();
$result = [];
foreach ($em->getRepository('SkobkinPointToolsBundle:User')->findUsersLikeLogin($login) as $user) {
$result[] = [
'login' => $user->getLogin(),
'name' => $user->getName(),
];
}
return new JsonResponse($result);
}
} }

View file

@ -50,19 +50,6 @@ class UserController extends Controller
]); ]);
} }
/**
* @param Request $request
*/
public function searchUserAction(Request $request)
{
$login = $request->request->get('login');
if (!$login) {
return $this->redirectToRoute('index');
}
return $this->redirectToRoute('user_show', ['login' => $login]);
}
/** /**
* @param TopUserDTO[] $topUsers * @param TopUserDTO[] $topUsers
* @return Highchart * @return Highchart

View file

@ -0,0 +1,39 @@
<?php
namespace Skobkin\Bundle\PointToolsBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class UserSearchType extends AbstractType
{
/**
* @param FormBuilderInterface $builder
* @param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('login')
;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'Skobkin\Bundle\PointToolsBundle\Entity\User',
]);
}
/**
* @return string
*/
public function getName()
{
return 'skobkin_bundle_pointtoolsbundle_user_search';
}
}

View file

@ -12,7 +12,7 @@ class UserRepository extends EntityRepository
* Case-insensitive user search * Case-insensitive user search
* *
* @param string $login * @param string $login
* @return User[] * @return User|null
* @throws \Doctrine\ORM\NonUniqueResultException * @throws \Doctrine\ORM\NonUniqueResultException
*/ */
public function findUserByLogin($login) public function findUserByLogin($login)
@ -28,6 +28,31 @@ class UserRepository extends EntityRepository
; ;
} }
/**
* Case insensitive user LIKE %login% search
*
* @param string $login
*
* @return User[]
*/
public function findUsersLikeLogin($login)
{
if (empty($login)) {
return [];
}
$qb = $this->createQueryBuilder('u');
return $qb
->where('u.login LIKE :login')
->orderBy('u.login', 'ASC')
->setMaxResults(10)
->setParameter('login', '%'.strtolower($login).'%')
->getQuery()
->getResult()
;
}
/** /**
* @return integer * @return integer
*/ */

View file

@ -2,10 +2,12 @@ index:
path: / path: /
defaults: { _controller: SkobkinPointToolsBundle:Main:index } defaults: { _controller: SkobkinPointToolsBundle:Main:index }
user_search: user_search_ajax:
path: /user/search path: /ajax/users/search/{login}
defaults: { _controller: SkobkinPointToolsBundle:User:searchUser } defaults: { _controller: SkobkinPointToolsBundle:Main:searchUserAjax, _format: json }
methods: [POST] requirements:
login: "[\w-]*"
_format: json
user_show: user_show:
path: /user/{login} path: /user/{login}

View file

@ -1,18 +1,52 @@
{% extends "::base.html.twig" %} {% extends "::base.html.twig" %}
{% block head_js %}
{{ parent() }}
<script src="{{ asset('js/bootstrap3-typeahead.min.js') }}"></script>
{% endblock %}
{% block content %} {% block content %}
<div class="well well-lg"> <div class="well well-lg">
{# @todo rewrite to Symfony forms #} {{ form_start(form, {'attr': {'class': 'form-inline'} }) }}
<form class="form-inline" method="post" action="{{ path('user_search') }}">
<div class="form-group"> <div class="form-group">
<label class="sr-only" for="index-input-username">{{ 'Username'|trans }}</label> {{ form_errors(form.login) }}
<div class="input-group"> {{ form_widget(form.login, {'attr': {'autocomplete': 'off'}}) }}
<div class="input-group-addon">@</div>
<input name="login" type="text" class="form-control" id="index-input-username" placeholder="username"> <script type="text/javascript">
</div> $(function() {
$field = $('#{{ form.login.vars.id }}');
$field.typeahead({
minLength: 2,
delay: 500,
autoSelect: true,
source: function (query, processCallback) {
$.get('{{ path('user_search_ajax', {'login': ''}) }}' + query, function (data) {
processCallback(data);
});
},
afterSelect: function () {
$field.parents('form').first().submit();
},
displayText: function (item) {
// Crutches to place only login into the field after selecting the item
if (typeof item === 'object') {
return item.login+(item.name ? ' ('+item.name+')' : '');
} else if (typeof item === 'string') {
return item;
}
},
updater: function (item) {
// Crutches to place only login into the field after selecting the item
return item.login;
}
});
});
</script>
<input type="submit" value="{{ 'Search'|trans }}" class="btn btn-default" />
</div> </div>
<button type="submit" class="btn btn-primary">{{ 'Search'|trans }}</button> {{ form_end(form) }}
</form>
</div> </div>
<div class="container service-stats"> <div class="container service-stats">

1
web/js/bootstrap3-typeahead.min.js vendored Normal file

File diff suppressed because one or more lines are too long