mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-06-22 18:03:37 +02:00
Implement a user friendly part select element.
This commit is contained in:
parent
c78bc01d23
commit
670dd76ef5
5 changed files with 175 additions and 3 deletions
58
assets/controllers/elements/part_select_controller.js
Normal file
58
assets/controllers/elements/part_select_controller.js
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import {Controller} from "@hotwired/stimulus";
|
||||||
|
|
||||||
|
import "tom-select/dist/css/tom-select.bootstrap5.css";
|
||||||
|
import '../../css/components/tom-select_extensions.css';
|
||||||
|
import TomSelect from "tom-select";
|
||||||
|
import {marked} from "marked";
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
_tomSelect;
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
|
||||||
|
let settings = {
|
||||||
|
allowEmptyOption: true,
|
||||||
|
searchField: "name",
|
||||||
|
valueField: "id",
|
||||||
|
render: {
|
||||||
|
item: (data, escape) => {
|
||||||
|
return '<span>' + "<img style='height: 1.5rem;' ' src='" + data.image + "'/>" + escape(data.name) + '</span>';
|
||||||
|
},
|
||||||
|
option: (data, escape) => {
|
||||||
|
let tmp = '<div class="row m-0">' +
|
||||||
|
"<div class='col-2 p-0 d-flex align-items-center'><img class='typeahead-image' src='" + data.image + "'/></div>" +
|
||||||
|
"<div class='col-10'>" +
|
||||||
|
'<h6 class="m-0">' + escape(data.name) + '</h6>' +
|
||||||
|
'<p class="m-0">' + marked.parseInline(data.description) + '</p>' +
|
||||||
|
'<p class="m-0"><span class="fa-solid fa-tags fa-fw"></span> ' + escape(data.category);
|
||||||
|
|
||||||
|
if (data.footprint) { //If footprint is defined for the part show it next to the category
|
||||||
|
tmp += ' <span class="fa-solid fa-microchip fa-fw"></span> ' + escape(data.footprint);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tmp + '</p>' +
|
||||||
|
'</div></div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
if (this.element.dataset.autocomplete) {
|
||||||
|
const base_url = this.element.dataset.autocomplete;
|
||||||
|
settings.valueField = "id";
|
||||||
|
settings.load = (query, callback) => {
|
||||||
|
const url = base_url.replace('__QUERY__', encodeURIComponent(query));
|
||||||
|
|
||||||
|
fetch(url)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(json => {callback(json);})
|
||||||
|
.catch(() => {
|
||||||
|
callback()
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
this._tomSelect = new TomSelect(this.element, settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,10 +32,12 @@ use App\Entity\Parameters\MeasurementUnitParameter;
|
||||||
use App\Entity\Parameters\PartParameter;
|
use App\Entity\Parameters\PartParameter;
|
||||||
use App\Entity\Parameters\StorelocationParameter;
|
use App\Entity\Parameters\StorelocationParameter;
|
||||||
use App\Entity\Parameters\SupplierParameter;
|
use App\Entity\Parameters\SupplierParameter;
|
||||||
|
use App\Entity\Parts\Part;
|
||||||
use App\Entity\PriceInformations\Currency;
|
use App\Entity\PriceInformations\Currency;
|
||||||
use App\Repository\ParameterRepository;
|
use App\Repository\ParameterRepository;
|
||||||
use App\Services\Attachments\AttachmentURLGenerator;
|
use App\Services\Attachments\AttachmentURLGenerator;
|
||||||
use App\Services\Attachments\BuiltinAttachmentsFinder;
|
use App\Services\Attachments\BuiltinAttachmentsFinder;
|
||||||
|
use App\Services\Attachments\PartPreviewGenerator;
|
||||||
use App\Services\Tools\TagFinder;
|
use App\Services\Tools\TagFinder;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
@ -128,6 +130,45 @@ class TypeaheadController extends AbstractController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/parts/search/{query}", name="typeahead_parts")
|
||||||
|
* @param string $query
|
||||||
|
* @param EntityManagerInterface $entityManager
|
||||||
|
* @return JsonResponse
|
||||||
|
*/
|
||||||
|
public function parts(string $query, EntityManagerInterface $entityManager, PartPreviewGenerator $previewGenerator,
|
||||||
|
AttachmentURLGenerator $attachmentURLGenerator): JsonResponse
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted('@parts.read');
|
||||||
|
|
||||||
|
$repo = $entityManager->getRepository(Part::class);
|
||||||
|
|
||||||
|
$parts = $repo->autocompleteSearch($query);
|
||||||
|
|
||||||
|
$data = [];
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
//Determine the picture to show:
|
||||||
|
$preview_attachment = $previewGenerator->getTablePreviewAttachment($part);
|
||||||
|
if($preview_attachment !== null) {
|
||||||
|
$preview_url = $attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_sm');
|
||||||
|
} else {
|
||||||
|
$preview_url = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var Part $part */
|
||||||
|
$data[] = [
|
||||||
|
'id' => $part->getID(),
|
||||||
|
'name' => $part->getName(),
|
||||||
|
'category' => $part->getCategory() ? $part->getCategory()->getName() : 'Unknown',
|
||||||
|
'footprint' => $part->getFootprint() ? $part->getFootprint()->getName() : '',
|
||||||
|
'description' => mb_strimwidth($part->getDescription(), 0, 127, '...'),
|
||||||
|
'image' => $preview_url,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonResponse($data);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Route("/parameters/{type}/search/{query}", name="typeahead_parameters", requirements={"type" = ".+"})
|
* @Route("/parameters/{type}/search/{query}", name="typeahead_parameters", requirements={"type" = ".+"})
|
||||||
* @param string $query
|
* @param string $query
|
||||||
|
|
|
@ -4,6 +4,7 @@ namespace App\Form\ProjectSystem;
|
||||||
|
|
||||||
use App\Entity\Parts\Part;
|
use App\Entity\Parts\Part;
|
||||||
use App\Entity\ProjectSystem\ProjectBOMEntry;
|
use App\Entity\ProjectSystem\ProjectBOMEntry;
|
||||||
|
use App\Form\Type\PartSelectType;
|
||||||
use Svg\Tag\Text;
|
use Svg\Tag\Text;
|
||||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
@ -23,9 +24,7 @@ class ProjectBOMEntryType extends AbstractType
|
||||||
'label' => 'project.bom.quantity',
|
'label' => 'project.bom.quantity',
|
||||||
])
|
])
|
||||||
|
|
||||||
->add('part', EntityType::class, [
|
->add('part', PartSelectType::class, [
|
||||||
'class' => Part::class,
|
|
||||||
'choice_label' => 'name',
|
|
||||||
'required' => false,
|
'required' => false,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
54
src/Form/Type/PartSelectType.php
Normal file
54
src/Form/Type/PartSelectType.php
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Form\Type;
|
||||||
|
|
||||||
|
use App\Entity\Parts\Part;
|
||||||
|
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\ChoiceList\ChoiceList;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
|
|
||||||
|
class PartSelectType extends AbstractType
|
||||||
|
{
|
||||||
|
private UrlGeneratorInterface $urlGenerator;
|
||||||
|
|
||||||
|
public function __construct(UrlGeneratorInterface $urlGenerator)
|
||||||
|
{
|
||||||
|
$this->urlGenerator = $urlGenerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'class' => Part::class,
|
||||||
|
'choice_label' => 'name',
|
||||||
|
'placeholder' => 'None'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'attr' => [
|
||||||
|
'data-controller' => 'elements--part-select',
|
||||||
|
'data-autocomplete' => $this->urlGenerator->generate('typeahead_parts', ['query' => '__QUERY__']),
|
||||||
|
//Disable browser autocomplete
|
||||||
|
'autocomplete' => 'off',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$resolver->setDefaults(['choices' => []]);
|
||||||
|
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'choice_attr' => ChoiceList::attr($this, function (?Part $part) {
|
||||||
|
return $part ? [
|
||||||
|
//'data-description' => $part->getDescription(),
|
||||||
|
//'data-category' => $part->getCategory() ? $part->getCategory()->getName() : '',
|
||||||
|
] : [];
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getParent()
|
||||||
|
{
|
||||||
|
return EntityType::class;
|
||||||
|
}
|
||||||
|
}
|
|
@ -66,4 +66,24 @@ class PartRepository extends NamedDBElementRepository
|
||||||
|
|
||||||
return (int) ($query->getSingleScalarResult() ?? 0);
|
return (int) ($query->getSingleScalarResult() ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function autocompleteSearch(string $query, int $max_limits = 50): array
|
||||||
|
{
|
||||||
|
$qb = $this->createQueryBuilder('part');
|
||||||
|
$qb->select('part')
|
||||||
|
->leftJoin('part.category', 'category')
|
||||||
|
|
||||||
|
->where('part.name LIKE :query')
|
||||||
|
->orWhere('part.description LIKE :query')
|
||||||
|
->orWhere('category.name LIKE :query')
|
||||||
|
|
||||||
|
;
|
||||||
|
|
||||||
|
$qb->setParameter('query', '%'.$query.'%');
|
||||||
|
|
||||||
|
$qb->setMaxResults($max_limits);
|
||||||
|
$qb->orderBy('part.name', 'ASC');
|
||||||
|
|
||||||
|
return $qb->getQuery()->getResult();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue