mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-06-21 01:25:55 +02:00
Added autocomplete for part parameters
This commit is contained in:
parent
44b288b807
commit
9a7e47863b
18 changed files with 209 additions and 134 deletions
|
@ -8,7 +8,6 @@ export default class extends Controller {
|
|||
_tomSelect;
|
||||
|
||||
connect() {
|
||||
|
||||
let settings = {
|
||||
plugins: {
|
||||
remove_button:{
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
import {Controller} from "@hotwired/stimulus";
|
||||
import TomSelect from "tom-select";
|
||||
import katex from "katex";
|
||||
import "katex/dist/katex.css";
|
||||
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default class extends Controller
|
||||
{
|
||||
static values = {
|
||||
url: String,
|
||||
}
|
||||
|
||||
static targets = ["name", "symbol", "unit"]
|
||||
|
||||
onItemAdd(value, item) {
|
||||
//Retrieve the unit and symbol from the item
|
||||
const symbol = item.dataset.symbol;
|
||||
const unit = item.dataset.unit;
|
||||
|
||||
if (this.symbolTarget && symbol !== undefined) {
|
||||
this.symbolTarget.value = symbol;
|
||||
}
|
||||
if (this.unitTarget && unit !== undefined) {
|
||||
this.unitTarget.value = unit;
|
||||
}
|
||||
}
|
||||
|
||||
connect() {
|
||||
const settings = {
|
||||
plugins: {
|
||||
clear_button:{}
|
||||
},
|
||||
persistent: false,
|
||||
maxItems: 1,
|
||||
//This a an ugly solution to disable the delimiter parsing of the TomSelect plugin
|
||||
delimiter: 'VERY_L0NG_D€LIMITER_WHICH_WILL_NEVER_BE_ENCOUNTERED_IN_A_STRING',
|
||||
createOnBlur: true,
|
||||
create: true,
|
||||
searchField: "name",
|
||||
//labelField: "name",
|
||||
valueField: "name",
|
||||
onItemAdd: this.onItemAdd.bind(this),
|
||||
render: {
|
||||
option: (data, escape) => {
|
||||
let tmp = '<div>'
|
||||
+ '<span>' + escape(data.name) + '</span><br>';
|
||||
|
||||
if (data.symbol) {
|
||||
tmp += '<span>' + katex.renderToString(data.symbol) + '</span>'
|
||||
}
|
||||
if (data.unit) {
|
||||
tmp += '<span class="ms-2">' + katex.renderToString('[' + data.unit + ']') + '</span>'
|
||||
}
|
||||
|
||||
|
||||
//+ '<span class="text-muted">' + escape(data.unit) + '</span>'
|
||||
tmp += '</div>';
|
||||
|
||||
return tmp;
|
||||
},
|
||||
item: (data, escape) => {
|
||||
//We use the item to transfert data to the onItemAdd function using data attributes
|
||||
const element = document.createElement('div');
|
||||
element.innerText = data.name;
|
||||
if(data.unit !== undefined) {
|
||||
element.dataset.unit = data.unit;
|
||||
}
|
||||
if (data.symbol !== undefined) {
|
||||
element.dataset.symbol = data.symbol;
|
||||
}
|
||||
|
||||
return element.outerHTML;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if(this.urlValue) {
|
||||
const base_url = this.urlValue;
|
||||
settings.load = (query, callback) => {
|
||||
const url = base_url.replace('__QUERY__', encodeURIComponent(query));
|
||||
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
//const data = json.map(x => {return {"value": x, "text": x}});
|
||||
callback(json);
|
||||
}).catch(()=>{
|
||||
callback();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this._tomSelect = new TomSelect(this.nameTarget, settings);
|
||||
}
|
||||
}
|
|
@ -42,9 +42,22 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Parameters\AttachmentTypeParameter;
|
||||
use App\Entity\Parameters\CategoryParameter;
|
||||
use App\Entity\Parameters\DeviceParameter;
|
||||
use App\Entity\Parameters\FootprintParameter;
|
||||
use App\Entity\Parameters\GroupParameter;
|
||||
use App\Entity\Parameters\ManufacturerParameter;
|
||||
use App\Entity\Parameters\MeasurementUnitParameter;
|
||||
use App\Entity\Parameters\PartParameter;
|
||||
use App\Entity\Parameters\StorelocationParameter;
|
||||
use App\Entity\Parameters\SupplierParameter;
|
||||
use App\Entity\PriceInformations\Currency;
|
||||
use App\Repository\ParameterRepository;
|
||||
use App\Services\Attachments\AttachmentURLGenerator;
|
||||
use App\Services\Attachments\BuiltinAttachmentsFinder;
|
||||
use App\Services\TagFinder;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Asset\Packages;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
|
@ -99,6 +112,58 @@ class TypeaheadController extends AbstractController
|
|||
return new JsonResponse($data, 200, [], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* This functions map the parameter type to the class, so we can access its repository
|
||||
* @param string $type
|
||||
* @return class-string
|
||||
*/
|
||||
private function typeToParameterClass(string $type): string
|
||||
{
|
||||
switch ($type) {
|
||||
case 'category':
|
||||
return CategoryParameter::class;
|
||||
case 'part':
|
||||
return PartParameter::class;
|
||||
case 'device':
|
||||
return DeviceParameter::class;
|
||||
case 'footprint':
|
||||
return FootprintParameter::class;
|
||||
case 'manufacturer':
|
||||
return ManufacturerParameter::class;
|
||||
case 'storelocation':
|
||||
return StorelocationParameter::class;
|
||||
case 'supplier':
|
||||
return SupplierParameter::class;
|
||||
case 'attachment_type':
|
||||
return AttachmentTypeParameter::class;
|
||||
case 'group':
|
||||
return GroupParameter::class;
|
||||
case 'measurement_unit':
|
||||
return MeasurementUnitParameter::class;
|
||||
case 'currency':
|
||||
return Currency::class;
|
||||
|
||||
default:
|
||||
throw new \InvalidArgumentException('Invalid parameter type: '.$type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/parameters/{type}/search/{query}", name="typeahead_parameters", requirements={"type" = ".+"})
|
||||
* @param string $query
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function parameters(string $type, EntityManagerInterface $entityManager, string $query = ""): JsonResponse
|
||||
{
|
||||
$class = $this->typeToParameterClass($type);
|
||||
/** @var ParameterRepository $repository */
|
||||
$repository = $entityManager->getRepository($class);
|
||||
|
||||
$data = $repository->autocompleteParamName($query);
|
||||
|
||||
return new JsonResponse($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/tags/search/{query}", name="typeahead_tags", requirements={"query"= ".+"})
|
||||
*/
|
||||
|
|
|
@ -33,7 +33,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||
use function sprintf;
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ParameterRepository")
|
||||
* @ORM\Table("parameters")
|
||||
* @ORM\InheritanceType("SINGLE_TABLE")
|
||||
* @ORM\DiscriminatorColumn(name="type", type="smallint")
|
||||
|
|
|
@ -28,7 +28,7 @@ use Doctrine\ORM\Mapping as ORM;
|
|||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ParameterRepository")
|
||||
* @UniqueEntity(fields={"name", "group", "element"})
|
||||
*/
|
||||
class AttachmentTypeParameter extends AbstractParameter
|
||||
|
|
|
@ -28,7 +28,7 @@ use Doctrine\ORM\Mapping as ORM;
|
|||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ParameterRepository")
|
||||
* @UniqueEntity(fields={"name", "group", "element"})
|
||||
*/
|
||||
class CategoryParameter extends AbstractParameter
|
||||
|
|
|
@ -30,7 +30,7 @@ use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
|||
/**
|
||||
* A attachment attached to a category element.
|
||||
*
|
||||
* @ORM\Entity()
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ParameterRepository")
|
||||
* @UniqueEntity(fields={"name", "group", "element"})
|
||||
*/
|
||||
class CurrencyParameter extends AbstractParameter
|
||||
|
|
|
@ -28,7 +28,7 @@ use Doctrine\ORM\Mapping as ORM;
|
|||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ParameterRepository")
|
||||
* @UniqueEntity(fields={"name", "group", "element"})
|
||||
*/
|
||||
class DeviceParameter extends AbstractParameter
|
||||
|
|
|
@ -28,7 +28,7 @@ use Doctrine\ORM\Mapping as ORM;
|
|||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ParameterRepository")
|
||||
* @UniqueEntity(fields={"name", "group", "element"})
|
||||
*/
|
||||
class FootprintParameter extends AbstractParameter
|
||||
|
|
|
@ -28,7 +28,7 @@ use Doctrine\ORM\Mapping as ORM;
|
|||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ParameterRepository")
|
||||
* @UniqueEntity(fields={"name", "group", "element"})
|
||||
*/
|
||||
class GroupParameter extends AbstractParameter
|
||||
|
|
|
@ -28,7 +28,7 @@ use Doctrine\ORM\Mapping as ORM;
|
|||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ParameterRepository")
|
||||
* @UniqueEntity(fields={"name", "group", "element"})
|
||||
*/
|
||||
class ManufacturerParameter extends AbstractParameter
|
||||
|
|
|
@ -28,7 +28,7 @@ use Doctrine\ORM\Mapping as ORM;
|
|||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ParameterRepository")
|
||||
* @UniqueEntity(fields={"name", "group", "element"})
|
||||
*/
|
||||
class MeasurementUnitParameter extends AbstractParameter
|
||||
|
|
|
@ -28,7 +28,7 @@ use Doctrine\ORM\Mapping as ORM;
|
|||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ParameterRepository")
|
||||
* @UniqueEntity(fields={"name", "group", "element"})
|
||||
*/
|
||||
class PartParameter extends AbstractParameter
|
||||
|
|
|
@ -28,7 +28,7 @@ use Doctrine\ORM\Mapping as ORM;
|
|||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ParameterRepository")
|
||||
* @UniqueEntity(fields={"name", "group", "element"})
|
||||
*/
|
||||
class StorelocationParameter extends AbstractParameter
|
||||
|
|
|
@ -28,7 +28,7 @@ use Doctrine\ORM\Mapping as ORM;
|
|||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ParameterRepository")
|
||||
* @UniqueEntity(fields={"name", "group", "element"})
|
||||
*/
|
||||
class SupplierParameter extends AbstractParameter
|
||||
|
|
33
src/Repository/ParameterRepository.php
Normal file
33
src/Repository/ParameterRepository.php
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
class ParameterRepository extends DBElementRepository
|
||||
{
|
||||
/**
|
||||
* Find parameters using a parameter name
|
||||
* @param string $name The name to search for
|
||||
* @param bool $exact True, if only exact names should match. False, if the name just needs to be contained in the parameter name
|
||||
* @param int $max_results
|
||||
* @return array
|
||||
*/
|
||||
public function autocompleteParamName(string $name, bool $exact = false, int $max_results = 50): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder('parameter');
|
||||
|
||||
$qb->distinct()
|
||||
->select('parameter.name')
|
||||
->addSelect('parameter.symbol')
|
||||
->addSelect('parameter.unit')
|
||||
->where('parameter.name LIKE :name');
|
||||
if ($exact) {
|
||||
$qb->setParameter('name', $name);
|
||||
} else {
|
||||
$qb->setParameter('name', '%'.$name.'%');
|
||||
}
|
||||
|
||||
$qb->setMaxResults($max_results);
|
||||
|
||||
return $qb->getQuery()->getArrayResult();
|
||||
}
|
||||
}
|
|
@ -1,117 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2020 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 Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
use function array_slice;
|
||||
|
||||
/**
|
||||
* A service related for searching for tags. Mostly useful for autocomplete reasons.
|
||||
*/
|
||||
class TagFinder
|
||||
{
|
||||
protected $em;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
$this->em = $entityManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = []): array
|
||||
{
|
||||
$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']);
|
||||
}
|
||||
|
||||
protected function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'query_limit' => 75,
|
||||
'return_limit' => 75,
|
||||
'min_keyword_length' => 2,
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -68,13 +68,13 @@
|
|||
|
||||
{% block parameter_widget %}
|
||||
{% import 'components/collection_type.macro.html.twig' as collection %}
|
||||
<tr>
|
||||
<td>{{ form_widget(form.name) }}{{ form_errors(form.name) }}</td>
|
||||
<td>{{ form_widget(form.symbol) }}{{ form_errors(form.symbol) }}</td>
|
||||
<tr {{ stimulus_controller('pages/parameters_autocomplete', {"url": url('typeahead_parameters', {"query": "__QUERY__", "type": "part"})}) }}>
|
||||
<td>{{ form_widget(form.name, {"attr": {"data-pages--parameters-autocomplete-target": "name"}}) }}{{ form_errors(form.name) }}</td>
|
||||
<td>{{ form_widget(form.symbol, {"attr": {"data-pages--parameters-autocomplete-target": "symbol"}}) }}{{ form_errors(form.symbol) }}</td>
|
||||
<td>{{ form_widget(form.value_min) }}{{ form_errors(form.value_min) }}</td>
|
||||
<td>{{ form_widget(form.value_typical) }}{{ form_errors(form.value_typical) }}</td>
|
||||
<td>{{ form_widget(form.value_max) }}{{ form_errors(form.value_max) }}</td>
|
||||
<td>{{ form_widget(form.unit) }}{{ form_errors(form.unit) }}</td>
|
||||
<td>{{ form_widget(form.unit, {"attr": {"data-pages--parameters-autocomplete-target": "unit"}}) }}{{ form_errors(form.unit) }}</td>
|
||||
<td>{{ form_widget(form.value_text) }}{{ form_errors(form.value_text) }}</td>
|
||||
<td>{{ form_widget(form.group) }}{{ form_errors(form.group) }}</td>
|
||||
<td>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue