mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-05-18 17:31:35 +00:00
Unterstützung für IPN-Suche in der BOM-Konfiguration bei Part- und Assembly-Auswahl hinzufügen.
JavaScript BOM-Synchronisation (nur ein Part oder ein Assembly wählbar) ergänzen. laceholder für Name-Angabe setzen: Entweder Part- oder Assembly-Name.
This commit is contained in:
parent
ca6254cc53
commit
a126a8f7b6
10 changed files with 155 additions and 21 deletions
|
|
@ -10,12 +10,19 @@ export default class extends Controller {
|
||||||
|
|
||||||
connect() {
|
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 = {
|
let settings = {
|
||||||
allowEmptyOption: true,
|
allowEmptyOption: true,
|
||||||
plugins: ['dropdown_input', 'clear_button'],
|
plugins: ['dropdown_input', this.element.required ? null : 'clear_button'],
|
||||||
searchField: ["name", "description", "category", "footprint"],
|
searchField: ["name", "description", "category", "footprint", "ipn"],
|
||||||
valueField: "id",
|
valueField: "id",
|
||||||
labelField: "name",
|
labelField: "name",
|
||||||
|
dropdownParent: dropdownParent,
|
||||||
preload: "focus",
|
preload: "focus",
|
||||||
render: {
|
render: {
|
||||||
item: (data, escape) => {
|
item: (data, escape) => {
|
||||||
|
|
@ -42,8 +49,9 @@ export default class extends Controller {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
if (this.element.dataset.autocomplete) {
|
if (this.element.dataset.autocomplete || this.element.querySelector('[data-autocomplete]')) {
|
||||||
const base_url = this.element.dataset.autocomplete;
|
const autocompleteElement = this.element.dataset.autocomplete ? this.element : this.element.querySelector('[data-autocomplete]');
|
||||||
|
const base_url = autocompleteElement.dataset.autocomplete;
|
||||||
settings.valueField = "id";
|
settings.valueField = "id";
|
||||||
settings.load = (query, callback) => {
|
settings.load = (query, callback) => {
|
||||||
const url = base_url.replace('__QUERY__', encodeURIComponent(query));
|
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();
|
//this._tomSelect.clearOptions();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -67,4 +76,4 @@ export default class extends Controller {
|
||||||
//Destroy the TomSelect instance
|
//Destroy the TomSelect instance
|
||||||
this._tomSelect.destroy();
|
this._tomSelect.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
94
assets/controllers/elements/bom_name_sync_controller.js
Normal file
94
assets/controllers/elements/bom_name_sync_controller.js
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,7 +19,7 @@ export default class extends Controller {
|
||||||
let settings = {
|
let settings = {
|
||||||
allowEmptyOption: true,
|
allowEmptyOption: true,
|
||||||
plugins: ['dropdown_input', this.element.required ? null : 'clear_button'],
|
plugins: ['dropdown_input', this.element.required ? null : 'clear_button'],
|
||||||
searchField: ["name", "description", "category", "footprint"],
|
searchField: ["name", "description", "category", "footprint", "ipn"],
|
||||||
valueField: "id",
|
valueField: "id",
|
||||||
labelField: "name",
|
labelField: "name",
|
||||||
dropdownParent: dropdownParent,
|
dropdownParent: dropdownParent,
|
||||||
|
|
@ -53,8 +53,9 @@ export default class extends Controller {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
if (this.element.dataset.autocomplete) {
|
if (this.element.dataset.autocomplete || this.element.querySelector('[data-autocomplete]')) {
|
||||||
const base_url = this.element.dataset.autocomplete;
|
const autocompleteElement = this.element.dataset.autocomplete ? this.element : this.element.querySelector('[data-autocomplete]');
|
||||||
|
const base_url = autocompleteElement.dataset.autocomplete;
|
||||||
settings.valueField = "id";
|
settings.valueField = "id";
|
||||||
settings.load = (query, callback) => {
|
settings.load = (query, callback) => {
|
||||||
const url = base_url.replace('__QUERY__', encodeURIComponent(query));
|
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();
|
//this._tomSelect.clearOptions();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,7 @@ class TypeaheadController extends AbstractController
|
||||||
'type' => 'part',
|
'type' => 'part',
|
||||||
'id' => $part->getID(),
|
'id' => $part->getID(),
|
||||||
'name' => $part->getName(),
|
'name' => $part->getName(),
|
||||||
|
'ipn' => $part->getIpn(),
|
||||||
'category' => $part->getCategory() instanceof Category ? $part->getCategory()->getName() : 'Unknown',
|
'category' => $part->getCategory() instanceof Category ? $part->getCategory()->getName() : 'Unknown',
|
||||||
'footprint' => $part->getFootprint() instanceof Footprint ? $part->getFootprint()->getName() : '',
|
'footprint' => $part->getFootprint() instanceof Footprint ? $part->getFootprint()->getName() : '',
|
||||||
'description' => mb_strimwidth($part->getDescription(), 0, 127, '...'),
|
'description' => mb_strimwidth($part->getDescription(), 0, 127, '...'),
|
||||||
|
|
@ -210,6 +211,7 @@ class TypeaheadController extends AbstractController
|
||||||
'type' => 'assembly',
|
'type' => 'assembly',
|
||||||
'id' => $assembly->getID(),
|
'id' => $assembly->getID(),
|
||||||
'name' => $assembly->getName(),
|
'name' => $assembly->getName(),
|
||||||
|
'ipn' => $assembly->getIpn(),
|
||||||
'category' => '',
|
'category' => '',
|
||||||
'footprint' => '',
|
'footprint' => '',
|
||||||
'description' => mb_strimwidth($assembly->getDescription(), 0, 127, '...'),
|
'description' => mb_strimwidth($assembly->getDescription(), 0, 127, '...'),
|
||||||
|
|
@ -248,8 +250,10 @@ class TypeaheadController extends AbstractController
|
||||||
|
|
||||||
/** @var Assembly $assembly */
|
/** @var Assembly $assembly */
|
||||||
$result[] = [
|
$result[] = [
|
||||||
|
'type' => 'assembly',
|
||||||
'id' => $assembly->getID(),
|
'id' => $assembly->getID(),
|
||||||
'name' => $assembly->getName(),
|
'name' => $assembly->getName(),
|
||||||
|
'ipn' => $assembly->getIpn(),
|
||||||
'category' => '',
|
'category' => '',
|
||||||
'footprint' => '',
|
'footprint' => '',
|
||||||
'description' => mb_strimwidth($assembly->getDescription(), 0, 127, '...'),
|
'description' => mb_strimwidth($assembly->getDescription(), 0, 127, '...'),
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,6 @@ class AssemblySelectType extends AbstractType implements DataMapperInterface
|
||||||
|
|
||||||
$resolver->setDefaults([
|
$resolver->setDefaults([
|
||||||
'attr' => [
|
'attr' => [
|
||||||
'data-controller' => 'elements--assembly-select',
|
|
||||||
'data-autocomplete' => $this->urlGenerator->generate('typeahead_assemblies', ['query' => '__QUERY__']),
|
'data-autocomplete' => $this->urlGenerator->generate('typeahead_assemblies', ['query' => '__QUERY__']),
|
||||||
'autocomplete' => 'off',
|
'autocomplete' => 'off',
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,6 @@ class PartSelectType extends AbstractType implements DataMapperInterface
|
||||||
|
|
||||||
$resolver->setDefaults([
|
$resolver->setDefaults([
|
||||||
'attr' => [
|
'attr' => [
|
||||||
'data-controller' => 'elements--part-select',
|
|
||||||
'data-autocomplete' => $this->urlGenerator->generate('typeahead_parts', ['query' => '__QUERY__']),
|
'data-autocomplete' => $this->urlGenerator->generate('typeahead_parts', ['query' => '__QUERY__']),
|
||||||
//Disable browser autocomplete
|
//Disable browser autocomplete
|
||||||
'autocomplete' => 'off',
|
'autocomplete' => 'off',
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,7 @@ class PartRepository extends NamedDBElementRepository
|
||||||
|
|
||||||
->where('ILIKE(part.name, :query) = TRUE')
|
->where('ILIKE(part.name, :query) = TRUE')
|
||||||
->orWhere('ILIKE(part.description, :query) = TRUE')
|
->orWhere('ILIKE(part.description, :query) = TRUE')
|
||||||
|
->orWhere('ILIKE(part.ipn, :query) = TRUE')
|
||||||
->orWhere('ILIKE(category.name, :query) = TRUE')
|
->orWhere('ILIKE(category.name, :query) = TRUE')
|
||||||
->orWhere('ILIKE(footprint.name, :query) = TRUE');
|
->orWhere('ILIKE(footprint.name, :query) = TRUE');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
{% set target_id = 'expand_row-' ~ form.vars.name %}
|
{% set target_id = 'expand_row-' ~ form.vars.name %}
|
||||||
|
|
||||||
{% import 'components/collection_type.macro.html.twig' as collection %}
|
{% import 'components/collection_type.macro.html.twig' as collection %}
|
||||||
<tr>
|
<tr {{ stimulus_controller('elements--bom-name-sync') }}>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#{{ target_id }}">
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#{{ target_id }}">
|
||||||
<i class="fa-solid fa-eye"></i>
|
<i class="fa-solid fa-eye"></i>
|
||||||
|
|
@ -42,11 +42,19 @@
|
||||||
{{ form_errors(form.quantity) }}
|
{{ form_errors(form.quantity) }}
|
||||||
</td>
|
</td>
|
||||||
<td style="min-width: 250px;">
|
<td style="min-width: 250px;">
|
||||||
{{ form_widget(form.part) }}
|
<div {{ stimulus_target('elements--bom-name-sync', 'part') }} data-action="change->elements--bom-name-sync#sync change@window->elements--bom-name-sync#sync">
|
||||||
|
{{ form_row(form.part) }}
|
||||||
|
</div>
|
||||||
{{ form_errors(form.part) }}
|
{{ form_errors(form.part) }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ form_widget(form.name) }}
|
{{ form_widget(form.name, {
|
||||||
|
attr: {
|
||||||
|
'data-elements--bom-name-sync-target': 'name',
|
||||||
|
'data-original-placeholder': form.name.vars.attr.placeholder|default(''),
|
||||||
|
'data-action': 'input->elements--bom-name-sync#sync'
|
||||||
|
}
|
||||||
|
}) }}
|
||||||
{{ form_errors(form.name) }}
|
{{ form_errors(form.name) }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
@ -77,4 +85,4 @@
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
{% set target_id = 'expand_row-' ~ form.vars.name %}
|
{% set target_id = 'expand_row-' ~ form.vars.name %}
|
||||||
|
|
||||||
{% import 'components/collection_type.macro.html.twig' as collection %}
|
{% import 'components/collection_type.macro.html.twig' as collection %}
|
||||||
<tr>
|
<tr {{ stimulus_controller('elements--bom-name-sync') }}>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#{{ target_id }}">
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#{{ target_id }}">
|
||||||
<i class="fa-solid fa-eye"></i>
|
<i class="fa-solid fa-eye"></i>
|
||||||
|
|
@ -42,14 +42,24 @@
|
||||||
{{ form_errors(form.quantity) }}
|
{{ form_errors(form.quantity) }}
|
||||||
</td>
|
</td>
|
||||||
<td style="min-width: 450px;">
|
<td style="min-width: 450px;">
|
||||||
{{ form_row(form.part) }}
|
<div {{ stimulus_target('elements--bom-name-sync', 'part') }} data-action="change->elements--bom-name-sync#sync change@window->elements--bom-name-sync#sync">
|
||||||
|
{{ form_row(form.part) }}
|
||||||
|
</div>
|
||||||
{{ form_errors(form.part) }}
|
{{ form_errors(form.part) }}
|
||||||
<div class="text-center mb-2" style="line-height: 1">∨</div>
|
<div class="text-center mb-2" style="line-height: 1">∨</div>
|
||||||
{{ form_widget(form.referencedAssembly) }}
|
<div {{ stimulus_target('elements--bom-name-sync', 'assembly') }} data-action="change->elements--bom-name-sync#sync change@window->elements--bom-name-sync#sync">
|
||||||
|
{{ form_row(form.referencedAssembly) }}
|
||||||
|
</div>
|
||||||
{{ form_errors(form.referencedAssembly) }}
|
{{ form_errors(form.referencedAssembly) }}
|
||||||
</td>
|
</td>
|
||||||
<td style="min-width: 450px;">
|
<td style="min-width: 450px;">
|
||||||
{{ form_row(form.name) }}
|
{{ form_row(form.name, {
|
||||||
|
attr: {
|
||||||
|
'data-elements--bom-name-sync-target': 'name',
|
||||||
|
'data-original-placeholder': form.name.vars.attr.placeholder|default(''),
|
||||||
|
'data-action': 'input->elements--bom-name-sync#sync'
|
||||||
|
}
|
||||||
|
}) }}
|
||||||
{{ form_errors(form.name) }}
|
{{ form_errors(form.name) }}
|
||||||
<div class="text-center mb-2" style="line-height: 1"></div>
|
<div class="text-center mb-2" style="line-height: 1"></div>
|
||||||
{{ form_row(form.designator) }}
|
{{ form_row(form.designator) }}
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,15 @@
|
||||||
{%- endblock choice_widget_collapsed -%}
|
{%- endblock choice_widget_collapsed -%}
|
||||||
|
|
||||||
{% block part_select_widget %}
|
{% block part_select_widget %}
|
||||||
{{ form_widget(form.autocomplete) }}
|
<div {{ stimulus_controller('elements--part-select') }} data-autocomplete="{{ attr['data-autocomplete']|default('') }}">
|
||||||
|
{{ form_widget(form.autocomplete) }}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block assembly_select_widget %}
|
||||||
|
<div {{ stimulus_controller('elements--assembly-select') }} data-autocomplete="{{ attr['data-autocomplete']|default('') }}">
|
||||||
|
{{ form_widget(form.autocomplete) }}
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block password_widget %}
|
{% block password_widget %}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue