diff --git a/assets/controllers/elements/assembly_select_controller.js b/assets/controllers/elements/assembly_select_controller.js index 98702d41..1ef117b8 100644 --- a/assets/controllers/elements/assembly_select_controller.js +++ b/assets/controllers/elements/assembly_select_controller.js @@ -10,12 +10,19 @@ export default class extends Controller { connect() { + //Check if tomselect is inside an modal and do not attach the dropdown to body in that case (as it breaks the modal) + let dropdownParent = "body"; + if (this.element.closest('.modal')) { + dropdownParent = null + } + let settings = { allowEmptyOption: true, - plugins: ['dropdown_input', 'clear_button'], - searchField: ["name", "description", "category", "footprint"], + plugins: ['dropdown_input', this.element.required ? null : 'clear_button'], + searchField: ["name", "description", "category", "footprint", "ipn"], valueField: "id", labelField: "name", + dropdownParent: dropdownParent, preload: "focus", render: { item: (data, escape) => { @@ -42,8 +49,9 @@ export default class extends Controller { }; - if (this.element.dataset.autocomplete) { - const base_url = this.element.dataset.autocomplete; + if (this.element.dataset.autocomplete || this.element.querySelector('[data-autocomplete]')) { + const autocompleteElement = this.element.dataset.autocomplete ? this.element : this.element.querySelector('[data-autocomplete]'); + const base_url = autocompleteElement.dataset.autocomplete; settings.valueField = "id"; settings.load = (query, callback) => { const url = base_url.replace('__QUERY__', encodeURIComponent(query)); @@ -57,7 +65,8 @@ export default class extends Controller { }; - this._tomSelect = new TomSelect(this.element, settings); + const targetElement = this.element instanceof HTMLInputElement || this.element instanceof HTMLSelectElement ? this.element : this.element.querySelector('select, input'); + this._tomSelect = new TomSelect(targetElement, settings); //this._tomSelect.clearOptions(); } } @@ -67,4 +76,4 @@ export default class extends Controller { //Destroy the TomSelect instance this._tomSelect.destroy(); } -} \ No newline at end of file +} diff --git a/assets/controllers/elements/bom_name_sync_controller.js b/assets/controllers/elements/bom_name_sync_controller.js new file mode 100644 index 00000000..aeffc607 --- /dev/null +++ b/assets/controllers/elements/bom_name_sync_controller.js @@ -0,0 +1,94 @@ +import {Controller} from "@hotwired/stimulus"; + +export default class extends Controller { + static targets = ["part", "assembly", "name"]; + + connect() { + this.updatePlaceholder(); + // Give TomSelect some time to initialize and set values + setTimeout(() => this.updatePlaceholder(), 100); + setTimeout(() => this.updatePlaceholder(), 500); + } + + updatePlaceholder() { + const partSelect = this.hasPartTarget ? this.partTarget.querySelector('select, input') : null; + const assemblySelect = this.hasAssemblyTarget ? this.assemblyTarget.querySelector('select, input') : null; + const nameInput = this.hasNameTarget ? this.nameTarget : null; + + if (!nameInput) return; + + let selectedName = ""; + + // Helper to get name from tomselect + const getNameFromTS = (el) => { + if (el && el.tomselect) { + const val = el.tomselect.getValue(); + if (val) { + const data = el.tomselect.options[val]; + if (data && data.name) return data.name; + } + } + // Fallback for raw select + if (el && el.value && el.options && el.selectedIndex >= 0) { + return el.options[el.selectedIndex].text; + } + return ""; + }; + + selectedName = getNameFromTS(partSelect); + + if (!selectedName) { + selectedName = getNameFromTS(assemblySelect); + } + + if (selectedName) { + nameInput.placeholder = selectedName; + if (nameInput.value === "") { + nameInput.style.opacity = "0.6"; + } else { + nameInput.style.opacity = "1"; + } + } else { + nameInput.placeholder = nameInput.dataset.originalPlaceholder || ""; + nameInput.style.opacity = "1"; + } + } + + // This method will be called via action when a change occurs + sync(event) { + // Handle mutual exclusion: if a part is selected, clear the assembly (and vice-versa) + // We identify which field was changed by looking at the event target + const changedElement = event.target; + const partSelect = this.hasPartTarget ? this.partTarget.querySelector('select, input') : null; + const assemblySelect = this.hasAssemblyTarget ? this.assemblyTarget.querySelector('select, input') : null; + + // If part was changed and has a value, clear assembly + if (partSelect && (changedElement === partSelect || partSelect.contains(changedElement))) { + const val = partSelect.tomselect ? partSelect.tomselect.getValue() : partSelect.value; + if (val && assemblySelect) { + if (assemblySelect.tomselect) { + assemblySelect.tomselect.clear(true); // true to silent event to avoid loops + } else { + assemblySelect.value = ""; + } + } + } + + // If assembly was changed and has a value, clear part + if (assemblySelect && (changedElement === assemblySelect || assemblySelect.contains(changedElement))) { + const val = assemblySelect.tomselect ? assemblySelect.tomselect.getValue() : assemblySelect.value; + if (val && partSelect) { + if (partSelect.tomselect) { + partSelect.tomselect.clear(true); // true to silent event to avoid loops + } else { + partSelect.value = ""; + } + } + } + + // Delay slightly to allow TomSelect to update its internal state if needed + setTimeout(() => { + this.updatePlaceholder(); + }, 100); + } +} diff --git a/assets/controllers/elements/part_select_controller.js b/assets/controllers/elements/part_select_controller.js index b69acbbc..05b7b083 100644 --- a/assets/controllers/elements/part_select_controller.js +++ b/assets/controllers/elements/part_select_controller.js @@ -19,7 +19,7 @@ export default class extends Controller { let settings = { allowEmptyOption: true, plugins: ['dropdown_input', this.element.required ? null : 'clear_button'], - searchField: ["name", "description", "category", "footprint"], + searchField: ["name", "description", "category", "footprint", "ipn"], valueField: "id", labelField: "name", dropdownParent: dropdownParent, @@ -53,8 +53,9 @@ export default class extends Controller { }; - if (this.element.dataset.autocomplete) { - const base_url = this.element.dataset.autocomplete; + if (this.element.dataset.autocomplete || this.element.querySelector('[data-autocomplete]')) { + const autocompleteElement = this.element.dataset.autocomplete ? this.element : this.element.querySelector('[data-autocomplete]'); + const base_url = autocompleteElement.dataset.autocomplete; settings.valueField = "id"; settings.load = (query, callback) => { const url = base_url.replace('__QUERY__', encodeURIComponent(query)); @@ -68,7 +69,8 @@ export default class extends Controller { }; - this._tomSelect = new TomSelect(this.element, settings); + const targetElement = this.element instanceof HTMLInputElement || this.element instanceof HTMLSelectElement ? this.element : this.element.querySelector('select, input'); + this._tomSelect = new TomSelect(targetElement, settings); //this._tomSelect.clearOptions(); } } diff --git a/src/Controller/TypeaheadController.php b/src/Controller/TypeaheadController.php index 1648ec73..3557268c 100644 --- a/src/Controller/TypeaheadController.php +++ b/src/Controller/TypeaheadController.php @@ -154,6 +154,7 @@ class TypeaheadController extends AbstractController 'type' => 'part', 'id' => $part->getID(), 'name' => $part->getName(), + 'ipn' => $part->getIpn(), 'category' => $part->getCategory() instanceof Category ? $part->getCategory()->getName() : 'Unknown', 'footprint' => $part->getFootprint() instanceof Footprint ? $part->getFootprint()->getName() : '', 'description' => mb_strimwidth($part->getDescription(), 0, 127, '...'), @@ -210,6 +211,7 @@ class TypeaheadController extends AbstractController 'type' => 'assembly', 'id' => $assembly->getID(), 'name' => $assembly->getName(), + 'ipn' => $assembly->getIpn(), 'category' => '', 'footprint' => '', 'description' => mb_strimwidth($assembly->getDescription(), 0, 127, '...'), @@ -248,8 +250,10 @@ class TypeaheadController extends AbstractController /** @var Assembly $assembly */ $result[] = [ + 'type' => 'assembly', 'id' => $assembly->getID(), 'name' => $assembly->getName(), + 'ipn' => $assembly->getIpn(), 'category' => '', 'footprint' => '', 'description' => mb_strimwidth($assembly->getDescription(), 0, 127, '...'), diff --git a/src/Form/Type/AssemblySelectType.php b/src/Form/Type/AssemblySelectType.php index 282161b1..0cf38caf 100644 --- a/src/Form/Type/AssemblySelectType.php +++ b/src/Form/Type/AssemblySelectType.php @@ -77,7 +77,6 @@ class AssemblySelectType extends AbstractType implements DataMapperInterface $resolver->setDefaults([ 'attr' => [ - 'data-controller' => 'elements--assembly-select', 'data-autocomplete' => $this->urlGenerator->generate('typeahead_assemblies', ['query' => '__QUERY__']), 'autocomplete' => 'off', ], diff --git a/src/Form/Type/PartSelectType.php b/src/Form/Type/PartSelectType.php index 8cdd6256..06326438 100644 --- a/src/Form/Type/PartSelectType.php +++ b/src/Form/Type/PartSelectType.php @@ -80,7 +80,6 @@ class PartSelectType extends AbstractType implements DataMapperInterface $resolver->setDefaults([ 'attr' => [ - 'data-controller' => 'elements--part-select', 'data-autocomplete' => $this->urlGenerator->generate('typeahead_parts', ['query' => '__QUERY__']), //Disable browser autocomplete 'autocomplete' => 'off', diff --git a/src/Repository/PartRepository.php b/src/Repository/PartRepository.php index 9d5fee5e..a523b7eb 100644 --- a/src/Repository/PartRepository.php +++ b/src/Repository/PartRepository.php @@ -101,6 +101,7 @@ class PartRepository extends NamedDBElementRepository ->where('ILIKE(part.name, :query) = TRUE') ->orWhere('ILIKE(part.description, :query) = TRUE') + ->orWhere('ILIKE(part.ipn, :query) = TRUE') ->orWhere('ILIKE(category.name, :query) = TRUE') ->orWhere('ILIKE(footprint.name, :query) = TRUE'); diff --git a/templates/form/collection_types_layout.html.twig b/templates/form/collection_types_layout.html.twig index 96b71bf0..7848f3fb 100644 --- a/templates/form/collection_types_layout.html.twig +++ b/templates/form/collection_types_layout.html.twig @@ -31,7 +31,7 @@ {% set target_id = 'expand_row-' ~ form.vars.name %} {% import 'components/collection_type.macro.html.twig' as collection %} - +