Assemblies einführen

This commit is contained in:
Marcel Diegelmann 2025-03-19 08:13:45 +01:00
parent e1418dfdc1
commit 6fa960df42
107 changed files with 14101 additions and 96 deletions

View file

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 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/>.
*/
namespace App\Form\AdminPages;
use App\Entity\Base\AbstractNamedDBElement;
use App\Form\AssemblySystem\AssemblyBOMEntryCollectionType;
use App\Form\Type\RichTextEditorType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
class AssemblyAdminForm extends BaseEntityAdminForm
{
protected function additionalFormElements(FormBuilderInterface $builder, array $options, AbstractNamedDBElement $entity): void
{
$builder->add('description', RichTextEditorType::class, [
'required' => false,
'label' => 'part.edit.description',
'mode' => 'markdown-single_line',
'empty_data' => '',
'attr' => [
'placeholder' => 'part.edit.description.placeholder',
'rows' => 2,
],
]);
$builder->add('bom_entries', AssemblyBOMEntryCollectionType::class);
$builder->add('status', ChoiceType::class, [
'attr' => [
'class' => 'form-select',
],
'label' => 'assembly.edit.status',
'required' => false,
'empty_data' => '',
'choices' => [
'assembly.status.draft' => 'draft',
'assembly.status.planning' => 'planning',
'assembly.status.in_production' => 'in_production',
'assembly.status.finished' => 'finished',
'assembly.status.archived' => 'archived',
],
]);
}
}

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Form\AdminPages;
use App\Entity\AssemblySystem\Assembly;
use App\Entity\PriceInformations\Currency;
use App\Entity\ProjectSystem\Project;
use App\Entity\UserSystem\Group;
@ -114,7 +115,7 @@ class BaseEntityAdminForm extends AbstractType
);
}
if ($entity instanceof AbstractStructuralDBElement && !($entity instanceof Group || $entity instanceof Project || $entity instanceof Currency)) {
if ($entity instanceof AbstractStructuralDBElement && !($entity instanceof Group || $entity instanceof Project || $entity instanceof Assembly || $entity instanceof Currency)) {
$builder->add('alternative_names', TextType::class, [
'required' => false,
'label' => 'entity.edit.alternative_names.label',

View file

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 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/>.
*/
namespace App\Form\AssemblySystem;
use App\Entity\AssemblySystem\Assembly;
use App\Entity\AssemblySystem\AssemblyBOMEntry;
use App\Form\Type\StructuralEntityType;
use App\Validator\Constraints\UniqueObjectCollection;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotNull;
class AssemblyAddPartsType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('assembly', StructuralEntityType::class, [
'class' => Assembly::class,
'required' => true,
'disabled' => $options['assembly'] instanceof Assembly, //If a assembly is given, disable the field
'data' => $options['assembly'],
'constraints' => [
new NotNull()
]
]);
$builder->add('bom_entries', AssemblyBOMEntryCollectionType::class, [
'entry_options' => [
'constraints' => [
new UniqueEntity(fields: ['part', 'assembly'], message: 'assembly.bom_entry.part_already_in_bom',
entityClass: AssemblyBOMEntry::class),
new UniqueEntity(fields: ['name', 'assembly'], message: 'assembly.bom_entry.name_already_in_bom',
entityClass: AssemblyBOMEntry::class, ignoreNull: true),
]
],
'constraints' => [
new UniqueObjectCollection(message: 'assembly.bom_entry.part_already_in_bom', fields: ['part']),
new UniqueObjectCollection(message: 'assembly.bom_entry.name_already_in_bom', fields: ['name']),
]
]);
$builder->add('submit', SubmitType::class, ['label' => 'save']);
//After submit set the assembly for all bom entries, so that it can be validated properly
$builder->addEventListener(FormEvents::SUBMIT, function (FormEvent $event) {
$form = $event->getForm();
/** @var Assembly $assembly */
$assembly = $form->get('assembly')->getData();
$bom_entries = $form->get('bom_entries')->getData();
foreach ($bom_entries as $bom_entry) {
$bom_entry->setAssembly($assembly);
}
});
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'assembly' => null,
]);
$resolver->setAllowedTypes('assembly', ['null', Assembly::class]);
}
}

View file

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Form\AssemblySystem;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AssemblyBOMEntryCollectionType extends AbstractType
{
public function getParent(): string
{
return CollectionType::class;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'entry_type' => AssemblyBOMEntryType::class,
'entry_options' => [
'label' => false,
],
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'reindex_enable' => true,
'label' => false,
]);
}
}

View file

@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace App\Form\AssemblySystem;
use App\Entity\AssemblySystem\AssemblyBOMEntry;
use App\Form\Type\BigDecimalNumberType;
use App\Form\Type\CurrencyEntityType;
use App\Form\Type\PartSelectType;
use App\Form\Type\RichTextEditorType;
use App\Form\Type\SIUnitType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Event\PreSetDataEvent;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AssemblyBOMEntryType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) {
$form = $event->getForm();
/** @var AssemblyBOMEntry $data */
$data = $event->getData();
$form->add('quantity', SIUnitType::class, [
'label' => 'assembly.bom.quantity',
'measurement_unit' => $data && $data->getPart() ? $data->getPart()->getPartUnit() : null,
]);
});
$builder
->add('part', PartSelectType::class, [
'required' => false,
])
->add('name', TextType::class, [
'label' => 'assembly.bom.name',
'required' => false,
])
->add('mountnames', TextType::class, [
'required' => false,
'label' => 'assembly.bom.mountnames',
'empty_data' => '',
'attr' => [
'class' => 'tagsinput',
'data-controller' => 'elements--tagsinput',
]
])
->add('comment', RichTextEditorType::class, [
'required' => false,
'label' => 'assembly.bom.comment',
'empty_data' => '',
'mode' => 'markdown-single_line',
'attr' => [
'rows' => 2,
],
])
->add('price', BigDecimalNumberType::class, [
'label' => false,
'required' => false,
'scale' => 5,
'html5' => true,
'attr' => [
'min' => 0,
'step' => 'any',
],
])
->add('priceCurrency', CurrencyEntityType::class, [
'required' => false,
'label' => false,
'short' => true,
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => AssemblyBOMEntry::class,
]);
}
}

View file

@ -0,0 +1,183 @@
<?php
declare(strict_types=1);
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 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/>.
*/
namespace App\Form\AssemblySystem;
use App\Helpers\Assemblies\AssemblyBuildRequest;
use Symfony\Bundle\SecurityBundle\Security;
use App\Entity\Parts\Part;
use App\Entity\Parts\PartLot;
use App\Form\Type\PartLotSelectType;
use App\Form\Type\SIUnitType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\DataMapperInterface;
use Symfony\Component\Form\Event\PreSetDataEvent;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AssemblyBuildType extends AbstractType implements DataMapperInterface
{
public function __construct(private readonly Security $security)
{
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'compound' => true,
'data_class' => AssemblyBuildRequest::class
]);
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->setDataMapper($this);
$builder->add('submit', SubmitType::class, [
'label' => 'assembly.build.btn_build',
'disabled' => !$this->security->isGranted('@parts_stock.withdraw'),
]);
$builder->add('dontCheckQuantity', CheckboxType::class, [
'label' => 'assembly.build.dont_check_quantity',
'help' => 'assembly.build.dont_check_quantity.help',
'required' => false,
'attr' => [
'data-controller' => 'pages--dont-check-quantity-checkbox'
]
]);
$builder->add('comment', TextType::class, [
'label' => 'part.info.withdraw_modal.comment',
'help' => 'part.info.withdraw_modal.comment.hint',
'empty_data' => '',
'required' => false,
]);
//The form is initially empty, we have to define the fields after we know the data
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) {
$form = $event->getForm();
/** @var AssemblyBuildRequest $build_request */
$build_request = $event->getData();
$form->add('addBuildsToBuildsPart', CheckboxType::class, [
'label' => 'assembly.build.add_builds_to_builds_part',
'required' => false,
'disabled' => !$build_request->getAssembly()->getBuildPart() instanceof Part,
]);
if ($build_request->getAssembly()->getBuildPart() instanceof Part) {
$form->add('buildsPartLot', PartLotSelectType::class, [
'label' => 'assembly.build.builds_part_lot',
'required' => false,
'part' => $build_request->getAssembly()->getBuildPart(),
'placeholder' => 'assembly.build.buildsPartLot.new_lot'
]);
}
foreach ($build_request->getPartBomEntries() as $bomEntry) {
//Every part lot has a field to specify the number of parts to take from this lot
foreach ($build_request->getPartLotsForBOMEntry($bomEntry) as $lot) {
$form->add('lot_' . $lot->getID(), SIUnitType::class, [
'label' => false,
'measurement_unit' => $bomEntry->getPart()->getPartUnit(),
'max' => min($build_request->getNeededAmountForBOMEntry($bomEntry), $lot->getAmount()),
'disabled' => !$this->security->isGranted('withdraw', $lot),
]);
}
}
});
}
public function mapDataToForms($data, \Traversable $forms): void
{
if (!$data instanceof AssemblyBuildRequest) {
throw new \RuntimeException('Data must be an instance of ' . AssemblyBuildRequest::class);
}
/** @var FormInterface[] $forms */
$forms = iterator_to_array($forms);
foreach ($forms as $key => $form) {
//Extract the lot id from the form name
$matches = [];
if (preg_match('/^lot_(\d+)$/', $key, $matches)) {
$lot_id = (int) $matches[1];
$form->setData($data->getLotWithdrawAmount($lot_id));
}
}
$forms['comment']->setData($data->getComment());
$forms['dontCheckQuantity']->setData($data->isDontCheckQuantity());
$forms['addBuildsToBuildsPart']->setData($data->getAddBuildsToBuildsPart());
if (isset($forms['buildsPartLot'])) {
$forms['buildsPartLot']->setData($data->getBuildsPartLot());
}
}
public function mapFormsToData(\Traversable $forms, &$data): void
{
if (!$data instanceof AssemblyBuildRequest) {
throw new \RuntimeException('Data must be an instance of ' . AssemblyBuildRequest::class);
}
/** @var FormInterface[] $forms */
$forms = iterator_to_array($forms);
foreach ($forms as $key => $form) {
//Extract the lot id from the form name
$matches = [];
if (preg_match('/^lot_(\d+)$/', $key, $matches)) {
$lot_id = (int) $matches[1];
$data->setLotWithdrawAmount($lot_id, (float) $form->getData());
}
}
$data->setComment($forms['comment']->getData());
$data->setDontCheckQuantity($forms['dontCheckQuantity']->getData());
if (isset($forms['buildsPartLot'])) {
$lot = $forms['buildsPartLot']->getData();
if (!$lot) { //When the user selected "Create new lot", create a new lot
$lot = new PartLot();
$description = 'Build ' . date('Y-m-d H:i:s');
if ($data->getComment() !== '') {
$description .= ' (' . $data->getComment() . ')';
}
$lot->setDescription($description);
$data->getAssembly()->getBuildPart()->addPartLot($lot);
}
$data->setBuildsPartLot($lot);
}
//This has to be set after the builds part lot, so that it can disable the option
$data->setAddBuildsToBuildsPart($forms['addBuildsToBuildsPart']->getData());
}
}

View file

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace App\Form\Filters;
use App\DataTables\Filters\AttachmentFilter;
use App\Entity\Attachments\AssemblyAttachment;
use App\Entity\Attachments\AttachmentType;
use App\Entity\Attachments\AttachmentTypeAttachment;
use App\Entity\Attachments\CategoryAttachment;
@ -80,6 +81,7 @@ class AttachmentFilterType extends AbstractType
'category.label' => CategoryAttachment::class,
'currency.label' => CurrencyAttachment::class,
'project.label' => ProjectAttachment::class,
'assembly.label' => AssemblyAttachment::class,
'footprint.label' => FootprintAttachment::class,
'group.label' => GroupAttachment::class,
'label_profile.label' => LabelAttachment::class,

View file

@ -59,6 +59,7 @@ class ProjectAddPartsType extends AbstractType
],
'constraints' => [
new UniqueObjectCollection(message: 'project.bom_entry.part_already_in_bom', fields: ['part']),
new UniqueObjectCollection(message: 'project.bom_entry.assembly_already_in_bom', fields: ['assembly']),
new UniqueObjectCollection(message: 'project.bom_entry.name_already_in_bom', fields: ['name']),
]
]);

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Form\ProjectSystem;
use App\Entity\ProjectSystem\ProjectBOMEntry;
use App\Form\Type\AssemblySelectType;
use App\Form\Type\BigDecimalNumberType;
use App\Form\Type\CurrencyEntityType;
use App\Form\Type\PartSelectType;
@ -22,8 +23,6 @@ class ProjectBOMEntryType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) {
$form = $event->getForm();
/** @var ProjectBOMEntry $data */
@ -36,11 +35,14 @@ class ProjectBOMEntryType extends AbstractType
});
$builder
->add('part', PartSelectType::class, [
'label' => 'project.bom.part',
'required' => false,
])
->add('assembly', AssemblySelectType::class, [
'label' => 'project.bom.assembly',
'required' => false,
])
->add('name', TextType::class, [
'label' => 'project.bom.name',
'required' => false,
@ -77,10 +79,7 @@ class ProjectBOMEntryType extends AbstractType
'required' => false,
'label' => false,
'short' => true,
])
;
]);
}
public function configureOptions(OptionsResolver $resolver): void

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
*/
namespace App\Form\ProjectSystem;
use App\Helpers\Assemblies\AssemblyBuildRequest;
use Symfony\Bundle\SecurityBundle\Security;
use App\Entity\Parts\Part;
use App\Entity\Parts\PartLot;
@ -38,10 +39,11 @@ use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
class ProjectBuildType extends AbstractType implements DataMapperInterface
{
public function __construct(private readonly Security $security)
public function __construct(private readonly Security $security, private readonly TranslatorInterface $translator)
{
}
@ -82,36 +84,54 @@ class ProjectBuildType extends AbstractType implements DataMapperInterface
//The form is initially empty, we have to define the fields after we know the data
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) {
$form = $event->getForm();
/** @var ProjectBuildRequest $build_request */
$build_request = $event->getData();
/** @var ProjectBuildRequest $projectBuildRequest */
$projectBuildRequest = $event->getData();
$form->add('addBuildsToBuildsPart', CheckboxType::class, [
'label' => 'project.build.add_builds_to_builds_part',
'required' => false,
'disabled' => !$build_request->getProject()->getBuildPart() instanceof Part,
'disabled' => !$projectBuildRequest->getProject()->getBuildPart() instanceof Part,
]);
if ($build_request->getProject()->getBuildPart() instanceof Part) {
if ($projectBuildRequest->getProject()->getBuildPart() instanceof Part) {
$form->add('buildsPartLot', PartLotSelectType::class, [
'label' => 'project.build.builds_part_lot',
'required' => false,
'part' => $build_request->getProject()->getBuildPart(),
'part' => $projectBuildRequest->getProject()->getBuildPart(),
'placeholder' => 'project.build.buildsPartLot.new_lot'
]);
}
foreach ($build_request->getPartBomEntries() as $bomEntry) {
foreach ($projectBuildRequest->getPartBomEntries() as $bomEntry) {
//Every part lot has a field to specify the number of parts to take from this lot
foreach ($build_request->getPartLotsForBOMEntry($bomEntry) as $lot) {
foreach ($projectBuildRequest->getPartLotsForBOMEntry($bomEntry) as $lot) {
$form->add('lot_' . $lot->getID(), SIUnitType::class, [
'label' => false,
'measurement_unit' => $bomEntry->getPart()->getPartUnit(),
'max' => min($build_request->getNeededAmountForBOMEntry($bomEntry), $lot->getAmount()),
'max' => min($projectBuildRequest->getNeededAmountForBOMEntry($bomEntry), $lot->getAmount()),
'disabled' => !$this->security->isGranted('withdraw', $lot),
]);
}
}
foreach ($projectBuildRequest->getAssemblyBomEntries() as $bomEntry) {
$assemblyBuildRequest = new AssemblyBuildRequest($bomEntry->getAssembly(), $projectBuildRequest->getNumberOfBuilds());
//Add fields for assembly bom entries
foreach ($assemblyBuildRequest->getPartBomEntries() as $partBomEntry) {
foreach ($assemblyBuildRequest->getPartLotsForBOMEntry($partBomEntry) as $lot) {
$form->add('lot_' . $lot->getID(), SIUnitType::class, [
'label' => $this->translator->trans('project.build.builds_part_lot_label', [
'%name%' => $partBomEntry->getPart()->getName(),
'%quantity%' => $partBomEntry->getQuantity() * $projectBuildRequest->getNumberOfBuilds()
]),
'measurement_unit' => $partBomEntry->getPart()->getPartUnit(),
'max' => min($assemblyBuildRequest->getNeededAmountForBOMEntry($partBomEntry), $lot->getAmount()),
'disabled' => !$this->security->isGranted('withdraw', $lot),
]);
}
}
}
});
}

View file

@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace App\Form\Type;
use App\Entity\AssemblySystem\Assembly;
use App\Entity\Attachments\Attachment;
use App\Services\Attachments\AssemblyPreviewGenerator;
use App\Services\Attachments\AttachmentURLGenerator;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\ChoiceList;
use Symfony\Component\Form\DataMapperInterface;
use Symfony\Component\Form\Event\PreSetDataEvent;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class AssemblySelectType extends AbstractType implements DataMapperInterface
{
public function __construct(private readonly UrlGeneratorInterface $urlGenerator, private readonly EntityManagerInterface $em, private readonly AssemblyPreviewGenerator $previewGenerator, private readonly AttachmentURLGenerator $attachmentURLGenerator)
{
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
//At initialization, we have to fill the form element with our selected data, so the user can see it
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) {
$form = $event->getForm();
$config = $form->getConfig()->getOptions();
$data = $event->getData() ?? [];
$config['compound'] = false;
$config['choices'] = is_iterable($data) ? $data : [$data];
$config['error_bubbling'] = true;
$form->add('autocomplete', EntityType::class, $config);
});
//After form submit, we have to add the selected element as choice, otherwise the form will not accept this element
$builder->addEventListener(FormEvents::PRE_SUBMIT, function(FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
$options = $form->get('autocomplete')->getConfig()->getOptions();
if (!isset($data['autocomplete']) || '' === $data['autocomplete'] || empty($data['autocomplete'])) {
$options['choices'] = [];
} else {
//Extract the ID from the submitted data
$id = $data['autocomplete'];
//Find the element in the database
$element = $this->em->find($options['class'], $id);
//Add the element as choice
$options['choices'] = [$element];
$options['error_bubbling'] = true;
$form->add('autocomplete', EntityType::class, $options);
}
});
$builder->setDataMapper($this);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'class' => Assembly::class,
'choice_label' => 'name',
'placeholder' => 'None',
'compound' => true,
'error_bubbling' => false,
]);
error_log($this->urlGenerator->generate('typeahead_assemblies', ['query' => '__QUERY__']));
$resolver->setDefaults([
'attr' => [
'data-controller' => 'elements--assembly-select',
'data-autocomplete' => $this->urlGenerator->generate('typeahead_assemblies', ['query' => '__QUERY__']),
'autocomplete' => 'off',
],
]);
$resolver->setDefaults([
//Prefill the selected choice with the needed data, so the user can see it without an additional Ajax request
'choice_attr' => ChoiceList::attr($this, function (?Assembly $assembly) {
if($assembly instanceof Assembly) {
//Determine the picture to show:
$preview_attachment = $this->previewGenerator->getTablePreviewAttachment($assembly);
if ($preview_attachment instanceof Attachment) {
$preview_url = $this->attachmentURLGenerator->getThumbnailURL($preview_attachment,
'thumbnail_sm');
} else {
$preview_url = '';
}
}
return $assembly instanceof Assembly ? [
'data-description' => $assembly->getDescription() ? mb_strimwidth($assembly->getDescription(), 0, 127, '...') : '',
'data-category' => '',
'data-footprint' => '',
'data-image' => $preview_url,
] : [];
})
]);
}
public function mapDataToForms($data, \Traversable $forms): void
{
$form = current(iterator_to_array($forms, false));
$form->setData($data);
}
public function mapFormsToData(\Traversable $forms, &$data): void
{
$form = current(iterator_to_array($forms, false));
$data = $form->getData();
}
}

View file

@ -50,7 +50,7 @@ class PartSelectType extends AbstractType implements DataMapperInterface
$options = $form->get('autocomplete')->getConfig()->getOptions();
if (!isset($data['autocomplete']) || '' === $data['autocomplete']) {
if (!isset($data['autocomplete']) || '' === $data['autocomplete'] || empty($data['autocomplete'])) {
$options['choices'] = [];
} else {
//Extract the ID from the submitted data
@ -84,7 +84,6 @@ class PartSelectType extends AbstractType implements DataMapperInterface
'data-autocomplete' => $this->urlGenerator->generate('typeahead_parts', ['query' => '__QUERY__']),
//Disable browser autocomplete
'autocomplete' => 'off',
],
]);
@ -103,7 +102,7 @@ class PartSelectType extends AbstractType implements DataMapperInterface
}
return $part instanceof Part ? [
'data-description' => mb_strimwidth($part->getDescription(), 0, 127, '...'),
'data-description' => $part->getDescription() ? mb_strimwidth($part->getDescription(), 0, 127, '...') : '',
'data-category' => $part->getCategory() instanceof Category ? $part->getCategory()->getName() : '',
'data-footprint' => $part->getFootprint() instanceof Footprint ? $part->getFootprint()->getName() : '',
'data-image' => $preview_url,