diff --git a/assets/controllers/elements/structural_entity_select_controller.js b/assets/controllers/elements/structural_entity_select_controller.js new file mode 100644 index 00000000..50c931f7 --- /dev/null +++ b/assets/controllers/elements/structural_entity_select_controller.js @@ -0,0 +1,87 @@ +/* + * 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 . + */ + +import "tom-select/dist/css/tom-select.bootstrap5.css"; +import '../../css/components/tom-select_extensions.css'; +import TomSelect from "tom-select"; +import {Controller} from "@hotwired/stimulus"; + + +export default class extends Controller { + _tomSelect; + + connect() { + + let settings = { + allowEmptyOption: true, + selectOnTab: true, + + searchField: [ + {field: "text", weight : 2}, + {field: "parent", weight : 0.5}, + ], + + render: { + item: this.renderItem, + option: this.renderOption, + } + }; + + this._tomSelect = new TomSelect(this.element, settings); + } + + renderItem(data, escape) { + let name = ""; + if (data.parent) { + name += escape(data.parent) + " → "; + } + name += "" + escape(data.text) + ""; + + return '
' + (data.image ? "" : "") + name + '
'; + } + + renderOption(data, escape) { + //Render empty option as full row + if (data.value === "") { + return '
 
'; + } + + + //Indent the option according to the level + const level_html = '   '.repeat(data.level); + + let filter_badge = ""; + if (data.filetype_filter) { + filter_badge = ' ' + escape(data.filetype_filter) + ''; + } + + let parent_badge = ""; + if (data.parent) { + parent_badge = ' ' + escape(data.parent) + ''; + } + + let image = ""; + if (data.image) { + image = ''; + } + + return '
' + level_html + escape(data.text) + image + parent_badge + filter_badge + '
'; + } + +} diff --git a/src/Form/Part/PartLotType.php b/src/Form/Part/PartLotType.php index 6147336f..f56756ab 100644 --- a/src/Form/Part/PartLotType.php +++ b/src/Form/Part/PartLotType.php @@ -60,11 +60,6 @@ class PartLotType extends AbstractType 'label' => 'part_lot.edit.location', 'required' => false, 'disable_not_selectable' => true, - 'attr' => [ - 'data-controller' => 'elements--selectpicker', - 'title' => 'selectpicker.nothing_selected', - 'data-live-search' => true, - ], ]); $builder->add('amount', SIUnitType::class, [ diff --git a/src/Form/Type/CurrencyEntityType.php b/src/Form/Type/CurrencyEntityType.php index 586a05ed..4059b3b4 100644 --- a/src/Form/Type/CurrencyEntityType.php +++ b/src/Form/Type/CurrencyEntityType.php @@ -25,6 +25,7 @@ namespace App\Form\Type; use App\Entity\Attachments\AttachmentType; use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\PriceInformations\Currency; +use App\Services\Attachments\AttachmentURLGenerator; use App\Services\Trees\NodesListBuilder; use Doctrine\ORM\EntityManagerInterface; use RuntimeException; @@ -39,9 +40,9 @@ class CurrencyEntityType extends StructuralEntityType { protected ?string $base_currency; - public function __construct(EntityManagerInterface $em, NodesListBuilder $builder, ?string $base_currency) + public function __construct(EntityManagerInterface $em, NodesListBuilder $builder, AttachmentURLGenerator $attachmentURLGenerator, ?string $base_currency) { - parent::__construct($em, $builder); + parent::__construct($em, $builder, $attachmentURLGenerator); $this->base_currency = $base_currency; } diff --git a/src/Form/Type/StructuralEntityType.php b/src/Form/Type/StructuralEntityType.php index 9b25c870..1eff9ad2 100644 --- a/src/Form/Type/StructuralEntityType.php +++ b/src/Form/Type/StructuralEntityType.php @@ -24,6 +24,7 @@ namespace App\Form\Type; use App\Entity\Attachments\AttachmentType; use App\Entity\Base\AbstractStructuralDBElement; +use App\Services\Attachments\AttachmentURLGenerator; use App\Services\Trees\NodesListBuilder; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Form\AbstractType; @@ -44,16 +45,18 @@ use Symfony\Component\OptionsResolver\OptionsResolver; class StructuralEntityType extends AbstractType { protected EntityManagerInterface $em; + protected AttachmentURLGenerator $attachmentURLGenerator; /** * @var NodesListBuilder */ protected $builder; - public function __construct(EntityManagerInterface $em, NodesListBuilder $builder) + public function __construct(EntityManagerInterface $em, NodesListBuilder $builder, AttachmentURLGenerator $attachmentURLGenerator) { $this->em = $em; $this->builder = $builder; + $this->attachmentURLGenerator = $attachmentURLGenerator; } public function buildForm(FormBuilderInterface $builder, array $options): void @@ -94,17 +97,14 @@ class StructuralEntityType extends AbstractType $resolver->setDefault('empty_message', null); - $resolver->setDefault('controller', 'elements--selectpicker'); + $resolver->setDefault('controller', 'elements--structural-entity-select'); $resolver->setDefault('attr', static function (Options $options) { $tmp = [ 'data-controller' => $options['controller'], - 'data-live-search' => true, - 'title' => 'selectpicker.nothing_selected', ]; if ($options['empty_message']) { - $tmp['data-none-Selected-Text'] = $options['empty_message']; - $tmp['title'] = $options['empty_message']; + $tmp['data-empty_message'] = $options['empty_message']; } return $tmp; @@ -119,15 +119,6 @@ class StructuralEntityType extends AbstractType return $this->builder->typeToNodesList($options['class'], null); } - public function buildView(FormView $view, FormInterface $form, array $options): void - { - //Allow HTML in labels. You must override the 'choice_widget_options' block, so that can work - //See extendedBootstrap4_layout.html.twig for that... - $view->vars['use_html_in_labels'] = true; - - parent::buildView($view, $form, $options); - } - public function getParent(): string { return ChoiceType::class; @@ -221,35 +212,10 @@ class StructuralEntityType extends AbstractType return $this->em->find($options['class'], $value->getID()); } - /** - * This generates the HTML code that will be rendered by selectpicker - * @return string - */ - protected function getChoiceContent(AbstractStructuralDBElement $choice, $key, $value, $options): string - { - $html = ""; - - //Add element name, use a class as whitespace which hides when not used in dropdown list - $html .= $this->getElementNameWithLevelWhitespace($choice, $options, ''); - - if ($options['show_fullpath_in_subtext'] && null !== $choice->getParent()) { - $html .= ' ' . trim(htmlspecialchars($choice->getParent()->getFullPath())) . ''; - } - - if ($choice instanceof AttachmentType && !empty($choice->getFiletypeFilter())) { - $html .= ' ' . trim(htmlspecialchars($choice->getFiletypeFilter())) . ''; - } - - return $html; - } - - protected function generateChoiceAttr(AbstractStructuralDBElement $choice, $key, $value, $options): array { $tmp = []; - - //Disable attribute if the choice is marked as not selectable if ($options['disable_not_selectable'] && $choice->isNotSelectable()) { $tmp += ['disabled' => 'disabled']; @@ -259,8 +225,22 @@ class StructuralEntityType extends AbstractType $tmp += ['data-filetype_filter' => $choice->getFiletypeFilter()]; } - //Add the HTML content that will be shown finally in the selectpicker - $tmp += ['data-content' => $this->getChoiceContent($choice, $key, $value, $options)]; + $level = $choice->getLevel(); + /** @var AbstractStructuralDBElement|null $parent */ + $parent = $options['subentities_of']; + if (null !== $parent) { + $level -= $parent->getLevel() - 1; + } + + $tmp += [ + 'data-level' => $level, + 'data-parent' => $choice->getParent() ? $choice->getParent()->getFullPath() : null, + 'data-image' => $choice->getMasterPictureAttachment() ? $this->attachmentURLGenerator->getThumbnailURL($choice->getMasterPictureAttachment(), 'thumbnail_xs') : null, + ]; + + if ($choice instanceof AttachmentType && !empty($choice->getFiletypeFilter())) { + $tmp += ['data-filetype_filter' => $choice->getFiletypeFilter()]; + } return $tmp; } @@ -285,7 +265,6 @@ class StructuralEntityType extends AbstractType protected function generateChoiceLabels(AbstractStructuralDBElement $choice, $key, $value, $options): string { - //Just for compatibility reasons for the case selectpicker should not work. The real value is generated in the getChoiceContent() method - return $this->getElementNameWithLevelWhitespace($choice, $options, " "); + return $choice->getName(); } } diff --git a/src/Services/Attachments/AttachmentURLGenerator.php b/src/Services/Attachments/AttachmentURLGenerator.php index 72ca1d24..c66d76fd 100644 --- a/src/Services/Attachments/AttachmentURLGenerator.php +++ b/src/Services/Attachments/AttachmentURLGenerator.php @@ -101,13 +101,13 @@ class AttachmentURLGenerator /** * Returns a URL under which the attachment file can be viewed. + * @return string|null The URL or null if the attachment file is not existing */ - public function getViewURL(Attachment $attachment): string + public function getViewURL(Attachment $attachment): ?string { $absolute_path = $this->attachmentHelper->toAbsoluteFilePath($attachment); if (null === $absolute_path) { - throw new RuntimeException('The given attachment is external or has no valid file, so no URL can get generated for it! - Use Attachment::getURL() to get the external URL!'); + return null; } $asset_path = $this->absolutePathToAssetPath($absolute_path); @@ -122,8 +122,9 @@ class AttachmentURLGenerator /** * Returns a URL to an thumbnail of the attachment file. + * @return string|null The URL or null if the attachment file is not existing */ - public function getThumbnailURL(Attachment $attachment, string $filter_name = 'thumbnail_sm'): string + public function getThumbnailURL(Attachment $attachment, string $filter_name = 'thumbnail_sm'): ?string { if (!$attachment->isPicture()) { throw new InvalidArgumentException('Thumbnail creation only works for picture attachments!'); @@ -135,7 +136,7 @@ class AttachmentURLGenerator $absolute_path = $this->attachmentHelper->toAbsoluteFilePath($attachment); if (null === $absolute_path) { - throw new RuntimeException('The given attachment is external or has no valid file, so no URL can get generated for it!'); + return null; } $asset_path = $this->absolutePathToAssetPath($absolute_path); diff --git a/templates/Form/extendedBootstrap4_layout.html.twig b/templates/Form/extendedBootstrap4_layout.html.twig index a409d8d4..60f1dc2e 100644 --- a/templates/Form/extendedBootstrap4_layout.html.twig +++ b/templates/Form/extendedBootstrap4_layout.html.twig @@ -20,23 +20,6 @@ col-sm-9 {%- endblock form_group_class %} -{%- block choice_widget_options -%} - {% for group_label, choice in options %} - {%- if choice is iterable -%} - - {% set options = choice %} - {{- block('choice_widget_options') -}} - - {%- else -%} - {% if use_html_in_labels is defined and use_html_in_labels %} - - {% else %} - - {% endif %} - {%- endif -%} - {% endfor %} -{%- endblock choice_widget_options -%} - {% block si_unit_widget %}
{{ form_widget(form.value) }}