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 %}
- {{ choice_translation_domain is same as(false) ? choice.label|raw : choice.label|trans({}, choice_translation_domain)|raw }}
- {% else %}
- {{ choice_translation_domain is same as(false) ? choice.label : choice.label|trans({}, choice_translation_domain) }}
- {% endif %}
- {%- endif -%}
- {% endfor %}
-{%- endblock choice_widget_options -%}
-
{% block si_unit_widget %}
{{ form_widget(form.value) }}