Skip to content

Commit 771857e

Browse files
webdevinitionMarcel Diegelmannjbtronics
authored
Added feature for part IPN suggest with category prefixes (#1054)
* Erweiterungstätigkeiten zur IPN-Vorschlagsliste anhand von Präfixen aus den Kategorien * Umstellung Migrationen bzgl. Multi-Plattform-Support. Zunächst MySQL, SQLite Statements integrieren. * Postgre Statements integrieren * SQL-Formatierung in Migration verbessern * Erweitere IPN-Suggest um Bauteilbeschreibung. Die Implementierung berücksichtigt nun zusätzlich die Bauteilbeschreibung zu maximal 150 Zeichen Länge für die Generierung von IPN-Vorschlägen und Inkrementen. * Anpassungen aus Analyse vornehmen * IPN-Validierung für Parts überarbeiten * IPN-Vorschlagslogik um Konfiguration erweitert * Anpassungen aus phpstan Analyse * IPN-Vorschlagslogik erweitert und Bauteil-IPN vereindeutigt Die IPN-Logik wurde um eine Konfiguration zur automatischen Suffix-Anfügung und die Berücksichtigung von doppelten Beschreibungen bei Bedarf ergänzt. Zudem wurde das Datenmodell angepasst, um eine eindeutige Speicherung der IPN zu gewährleisten. * Regex-Konfigurationsmöglichkeit für IPN-Vorschläge einführen Die Einstellungen für die IPN-Vorschlagslogik wurden um eine Regex-Validierung und eine Hilfetext-Konfiguration erweitert. Tests und Änderungen an den Formularoptionen wurden implementiert. * Match range assert and form limits in suggestPartDigits * Keep existing behavior with autoAppend suffix by default * Show the regex hint in the browser validation notice. * Improved translations * Removed unnecessary service definition * Removed german comments --------- Co-authored-by: Marcel Diegelmann <[email protected]> Co-authored-by: Jan Böhmer <[email protected]>
1 parent 14a4f1f commit 771857e

34 files changed

+2791
-115
lines changed

assets/controllers/elements/ckeditor_controller.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,15 @@ export default class extends Controller {
106106
editor_div.classList.add(...new_classes.split(","));
107107
}
108108

109+
// Automatic synchronization of source input
110+
editor.model.document.on("change:data", () => {
111+
editor.updateSourceElement();
112+
113+
// Dispatch the input event for further treatment
114+
const event = new Event("input");
115+
this.element.dispatchEvent(event);
116+
});
117+
109118
//This return is important! Otherwise we get mysterious errors in the console
110119
//See: https://github.com/ckeditor/ckeditor5/issues/5897#issuecomment-628471302
111120
return editor;
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import { Controller } from "@hotwired/stimulus";
2+
import "../../css/components/autocomplete_bootstrap_theme.css";
3+
4+
export default class extends Controller {
5+
static targets = ["input"];
6+
static values = {
7+
partId: Number,
8+
partCategoryId: Number,
9+
partDescription: String,
10+
suggestions: Object,
11+
commonSectionHeader: String, // Dynamic header for common Prefixes
12+
partIncrementHeader: String, // Dynamic header for new possible part increment
13+
suggestUrl: String,
14+
};
15+
16+
connect() {
17+
this.configureAutocomplete();
18+
this.watchCategoryChanges();
19+
this.watchDescriptionChanges();
20+
}
21+
22+
templates = {
23+
commonSectionHeader({ title, html }) {
24+
return html`
25+
<section class="aa-Source">
26+
<div class="aa-SourceHeader">
27+
<span class="aa-SourceHeaderTitle">${title}</span>
28+
<div class="aa-SourceHeaderLine"></div>
29+
</div>
30+
</section>
31+
`;
32+
},
33+
partIncrementHeader({ title, html }) {
34+
return html`
35+
<section class="aa-Source">
36+
<div class="aa-SourceHeader">
37+
<span class="aa-SourceHeaderTitle">${title}</span>
38+
<div class="aa-SourceHeaderLine"></div>
39+
</div>
40+
</section>
41+
`;
42+
},
43+
list({ html }) {
44+
return html`
45+
<ul class="aa-List" role="listbox"></ul>
46+
`;
47+
},
48+
item({ suggestion, description, html }) {
49+
return html`
50+
<li class="aa-Item" role="option" data-suggestion="${suggestion}" aria-selected="false">
51+
<div class="aa-ItemWrapper">
52+
<div class="aa-ItemContent">
53+
<div class="aa-ItemIcon aa-ItemIcon--noBorder">
54+
<svg viewBox="0 0 24 24" fill="currentColor">
55+
<path d="M12 21c4.971 0 9-4.029 9-9s-4.029-9-9-9-9 4.029-9 9 4.029 9 9 9z"></path>
56+
</svg>
57+
</div>
58+
<div class="aa-ItemContentBody">
59+
<div class="aa-ItemContentTitle">${suggestion}</div>
60+
<div class="aa-ItemContentDescription">${description}</div>
61+
</div>
62+
</div>
63+
</div>
64+
</li>
65+
`;
66+
},
67+
};
68+
69+
configureAutocomplete() {
70+
const inputField = this.inputTarget;
71+
const commonPrefixes = this.suggestionsValue.commonPrefixes || [];
72+
const prefixesPartIncrement = this.suggestionsValue.prefixesPartIncrement || [];
73+
const commonHeader = this.commonSectionHeaderValue;
74+
const partIncrementHeader = this.partIncrementHeaderValue;
75+
76+
if (!inputField || (!commonPrefixes.length && !prefixesPartIncrement.length)) return;
77+
78+
// Check whether the panel should be created at the update
79+
if (this.isPanelInitialized) {
80+
const existingPanel = inputField.parentNode.querySelector(".aa-Panel");
81+
if (existingPanel) {
82+
// Only remove the panel in the update phase
83+
84+
existingPanel.remove();
85+
}
86+
}
87+
88+
// Create panel
89+
const panel = document.createElement("div");
90+
panel.classList.add("aa-Panel");
91+
panel.style.display = "none";
92+
93+
// Create panel layout
94+
const panelLayout = document.createElement("div");
95+
panelLayout.classList.add("aa-PanelLayout", "aa-Panel--scrollable");
96+
97+
// Section for prefixes part increment
98+
if (prefixesPartIncrement.length) {
99+
const partIncrementSection = document.createElement("section");
100+
partIncrementSection.classList.add("aa-Source");
101+
102+
const partIncrementHeaderHtml = this.templates.partIncrementHeader({
103+
title: partIncrementHeader,
104+
html: String.raw,
105+
});
106+
partIncrementSection.innerHTML += partIncrementHeaderHtml;
107+
108+
const partIncrementList = document.createElement("ul");
109+
partIncrementList.classList.add("aa-List");
110+
partIncrementList.setAttribute("role", "listbox");
111+
112+
prefixesPartIncrement.forEach((prefix) => {
113+
const itemHTML = this.templates.item({
114+
suggestion: prefix.title,
115+
description: prefix.description,
116+
html: String.raw,
117+
});
118+
partIncrementList.innerHTML += itemHTML;
119+
});
120+
121+
partIncrementSection.appendChild(partIncrementList);
122+
panelLayout.appendChild(partIncrementSection);
123+
}
124+
125+
// Section for common prefixes
126+
if (commonPrefixes.length) {
127+
const commonSection = document.createElement("section");
128+
commonSection.classList.add("aa-Source");
129+
130+
const commonSectionHeader = this.templates.commonSectionHeader({
131+
title: commonHeader,
132+
html: String.raw,
133+
});
134+
commonSection.innerHTML += commonSectionHeader;
135+
136+
const commonList = document.createElement("ul");
137+
commonList.classList.add("aa-List");
138+
commonList.setAttribute("role", "listbox");
139+
140+
commonPrefixes.forEach((prefix) => {
141+
const itemHTML = this.templates.item({
142+
suggestion: prefix.title,
143+
description: prefix.description,
144+
html: String.raw,
145+
});
146+
commonList.innerHTML += itemHTML;
147+
});
148+
149+
commonSection.appendChild(commonList);
150+
panelLayout.appendChild(commonSection);
151+
}
152+
153+
panel.appendChild(panelLayout);
154+
inputField.parentNode.appendChild(panel);
155+
156+
inputField.addEventListener("focus", () => {
157+
panel.style.display = "block";
158+
});
159+
160+
inputField.addEventListener("blur", () => {
161+
setTimeout(() => {
162+
panel.style.display = "none";
163+
}, 100);
164+
});
165+
166+
// Selection of an item
167+
panelLayout.addEventListener("mousedown", (event) => {
168+
const target = event.target.closest("li");
169+
170+
if (target) {
171+
inputField.value = target.dataset.suggestion;
172+
panel.style.display = "none";
173+
}
174+
});
175+
176+
this.isPanelInitialized = true;
177+
};
178+
179+
watchCategoryChanges() {
180+
const categoryField = document.querySelector('[data-ipn-suggestion="categoryField"]');
181+
const descriptionField = document.querySelector('[data-ipn-suggestion="descriptionField"]');
182+
this.previousCategoryId = Number(this.partCategoryIdValue);
183+
184+
if (categoryField) {
185+
categoryField.addEventListener("change", () => {
186+
const categoryId = Number(categoryField.value);
187+
const description = String(descriptionField?.value ?? '');
188+
189+
// Check whether the category has changed compared to the previous ID
190+
if (categoryId !== this.previousCategoryId) {
191+
this.fetchNewSuggestions(categoryId, description);
192+
this.previousCategoryId = categoryId;
193+
}
194+
});
195+
}
196+
}
197+
198+
watchDescriptionChanges() {
199+
const categoryField = document.querySelector('[data-ipn-suggestion="categoryField"]');
200+
const descriptionField = document.querySelector('[data-ipn-suggestion="descriptionField"]');
201+
this.previousDescription = String(this.partDescriptionValue);
202+
203+
if (descriptionField) {
204+
descriptionField.addEventListener("input", () => {
205+
const categoryId = Number(categoryField.value);
206+
const description = String(descriptionField?.value ?? '');
207+
208+
// Check whether the description has changed compared to the previous one
209+
if (description !== this.previousDescription) {
210+
this.fetchNewSuggestions(categoryId, description);
211+
this.previousDescription = description;
212+
}
213+
});
214+
}
215+
}
216+
217+
fetchNewSuggestions(categoryId, description) {
218+
const baseUrl = this.suggestUrlValue;
219+
const partId = this.partIdValue;
220+
const truncatedDescription = description.length > 150 ? description.substring(0, 150) : description;
221+
const encodedDescription = this.base64EncodeUtf8(truncatedDescription);
222+
const url = `${baseUrl}?partId=${partId}&categoryId=${categoryId}` + (description !== '' ? `&description=${encodedDescription}` : '');
223+
224+
fetch(url, {
225+
method: "GET",
226+
headers: {
227+
"Content-Type": "application/json",
228+
"Accept": "application/json",
229+
},
230+
})
231+
.then((response) => {
232+
if (!response.ok) {
233+
throw new Error(`Error when calling up the IPN-suggestions: ${response.status}`);
234+
}
235+
return response.json();
236+
})
237+
.then((data) => {
238+
this.suggestionsValue = data;
239+
this.configureAutocomplete();
240+
})
241+
.catch((error) => {
242+
console.error("Errors when loading the new IPN-suggestions:", error);
243+
});
244+
};
245+
246+
base64EncodeUtf8(text) {
247+
const utf8Bytes = new TextEncoder().encode(text);
248+
return btoa(String.fromCharCode(...utf8Bytes));
249+
};
250+
}

config/services.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,16 @@ services:
231231
tags:
232232
- { name: 'doctrine.fixtures.purger_factory', alias: 'reset_autoincrement_purger' }
233233

234+
App\Repository\PartRepository:
235+
arguments:
236+
$translator: '@translator'
237+
tags: ['doctrine.repository_service']
238+
239+
App\EventSubscriber\UserSystem\PartUniqueIpnSubscriber:
240+
tags:
241+
- { name: doctrine.event_listener, event: onFlush, connection: default }
242+
243+
234244
# We are needing this service inside a migration, where only the container is injected. So we need to define it as public, to access it from the container.
235245
App\Services\UserSystem\PermissionPresetsHelper:
236246
public: true

docs/configuration.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,16 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept
116116
value should be handled as confidential data and not shared publicly.
117117
* `SHOW_PART_IMAGE_OVERLAY`: Set to 0 to disable the part image overlay, which appears if you hover over an image in the
118118
part image gallery
119+
* `IPN_SUGGEST_REGEX`: A global regular expression, that part IPNs have to fullfill. Enforce your own format for your users.
120+
* `IPN_SUGGEST_REGEX_HELP`: Define your own user help text for the Regex format specification.
121+
* `IPN_AUTO_APPEND_SUFFIX`: When enabled, an incremental suffix will be added to the user input when entering an existing
122+
* IPN again upon saving.
123+
* `IPN_SUGGEST_PART_DIGITS`: Defines the fixed number of digits used as the increment at the end of an IPN (Internal Part Number).
124+
IPN prefixes, maintained within part categories and their hierarchy, form the foundation for suggesting complete IPNs.
125+
These suggestions become accessible during IPN input of a part. The constant specifies the digits used to calculate and assign
126+
unique increments for parts within a category hierarchy, ensuring consistency and uniqueness in IPN generation.
127+
* `IPN_USE_DUPLICATE_DESCRIPTION`: When enabled, the part’s description is used to find existing parts with the same
128+
description and to determine the next available IPN by incrementing their numeric suffix for the suggestion list.
119129

120130
### E-Mail settings (all env only)
121131

0 commit comments

Comments
 (0)