Refactor ComboBoxWidget and settings form: remove unused handleComboAdd function, enhance form state serialization, and improve room option handling

This commit is contained in:
Yaro Kasear 2025-07-03 10:38:29 -05:00
parent 31913eab47
commit 76d2799e05
3 changed files with 80 additions and 106 deletions

View file

@ -74,27 +74,6 @@ const ComboBoxWidget = (() => {
sorted.forEach(option => selectElement.appendChild(option)); sorted.forEach(option => selectElement.appendChild(option));
} }
function handleComboAdd(inputId, listId, stateArray, label = 'entry') {
const input = document.getElementById(inputId);
const value = input.value.trim();
if (!value) {
alert(`Please enter a ${label}.`);
return;
}
const select = document.getElementById(listId);
const exists = Array.from(select.options).some(opt => opt.textContent === value);
if (exists) {
alert(`${label.charAt(0).toUpperCase() + label.slice(1)} "${value}" already exists.`);
return;
}
const option = createOption(value); // Already built to handle temp IDs
select.add(option);
formState[stateArray].push(value);
input.value = '';
}
function initComboBox(ns, config = {}) { function initComboBox(ns, config = {}) {
const input = document.querySelector(`#${ns}-input`); const input = document.querySelector(`#${ns}-input`);
const list = document.querySelector(`#${ns}-list`); const list = document.querySelector(`#${ns}-list`);
@ -168,9 +147,6 @@ const ComboBoxWidget = (() => {
list.appendChild(option); list.appendChild(option);
const key = config.stateArray ?? `${ns}s`; // fallback to pluralization const key = config.stateArray ?? `${ns}s`; // fallback to pluralization
if (Array.isArray(formState?.[key])) {
formState[key].push({ name: newItem });
}
if (config.sort !== false) { if (config.sort !== false) {
sortOptions(list); sortOptions(list);
@ -203,7 +179,6 @@ const ComboBoxWidget = (() => {
initComboBox, initComboBox,
createOption, createOption,
sortOptions, sortOptions,
handleComboAdd,
createTempId createTempId
}; };
})(); })();

View file

@ -1,44 +1,40 @@
{% import "fragments/_icon_fragment.html" as icons %} {% import "fragments/_icon_fragment.html" as icons %}
{% macro render_combobox(id, options, label=none, placeholder=none, onAdd=none, onRemove=none, onEdit=none, data_attributes=none) %} {% macro render_combobox(id, options, label=none, placeholder=none, onAdd=none, onRemove=none, onEdit=none,
<!-- Combobox Widget Fragment --> data_attributes=none) %}
<!-- Combobox Widget Fragment -->
{% if label %} {% if label %}
<label for="{{ id }}-input" class="form-label">{{ label }}</label> <label for="{{ id }}-input" class="form-label">{{ label }}</label>
{% endif %} {% endif %}
<div class="combo-box-widget" id="{{ id }}-container"> <div class="combo-box-widget" id="{{ id }}-container">
<div class="input-group"> <div class="input-group">
<input type="text" class="form-control rounded-bottom-0" id="{{ id }}-input"{% if placeholder %} placeholder="{{ placeholder }}"{% endif %}> <input type="text" class="form-control rounded-bottom-0" id="{{ id }}-input" {% if placeholder %}
<button type="button" class="btn btn-primary rounded-bottom-0" id="{{ id }}-add" disabled> placeholder="{{ placeholder }}" {% endif %}>
{{ icons.render_icon('plus-lg', 16, 'icon-state') }} <button type="button" class="btn btn-primary rounded-bottom-0" id="{{ id }}-add" disabled>
</button> {{ icons.render_icon('plus-lg', 16, 'icon-state') }}
<button type="button" class="btn btn-danger rounded-bottom-0" id="{{ id }}-remove" disabled> </button>
{{ icons.render_icon('dash-lg', 16) }} <button type="button" class="btn btn-danger rounded-bottom-0" id="{{ id }}-remove" disabled>
</button> {{ icons.render_icon('dash-lg', 16) }}
</div> </button>
<select class="form-select border-top-0 rounded-top-0" id="{{ id }}-list" name="{{ id }}" size="10" multiple>
{% for option in options %}
<option value="{{ option.id }}"
{% if data_attributes %}
{% for key, data_attr in data_attributes.items() %}
{% if option[key] is defined %}
data-{{ data_attr }}="{{ option[key] }}"
{% endif %}
{% endfor %}
{% endif %}>
{{ option.name }}
</option>
{% endfor %}
</select>
</div> </div>
<select class="form-select border-top-0 rounded-top-0" id="{{ id }}-list" name="{{ id }}" size="10" multiple>
{% for option in options %}
<option value="{{ option.id }}" {% if data_attributes %} {% for key, data_attr in data_attributes.items() %} {%
if option[key] is defined %} data-{{ data_attr }}="{{ option[key] }}" {% endif %} {% endfor %} {% endif %}>
{{ option.name }}
</option>
{% endfor %}
</select>
</div>
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
ComboBoxWidget.initComboBox("{{ id }}", { ComboBoxWidget.initComboBox("{{ id }}"{% if onAdd or onRemove or onEdit %}, {
{% if onAdd %}onAdd: function(newItem, list, createOption) { {{ onAdd | safe }} },{% endif %} {% if onAdd %}onAdd: function(newItem, list, createOption) { { { onAdd | safe } } }, {% endif %}
{% if onRemove %}onRemove: function(option) { {{ onRemove | safe }} },{% endif %} {% if onRemove %} onRemove: function(option) { { { onRemove | safe } } }, {% endif %}
{% if onEdit %}onEdit: function(option) { {{ onEdit | safe }} }{% endif %} {% if onEdit %} onEdit: function(option) { { { onEdit | safe } } } {% endif %}
}); }{% endif %});
}); });
</script> </script>
{% endmacro %} {% endmacro %}

View file

@ -140,39 +140,52 @@
{% endblock %} {% endblock %}
{% block script %} {% block script %}
const formState = { function isSerializable(obj) {
brands: {{ brands | tojson }}, try {
types: {{ types | tojson }}, JSON.stringify(obj);
sections: {{ sections | tojson }}, return true;
functions: {{ functions | tojson }}, } catch {
rooms: {{ rooms | tojson }}, return false;
}; }
}
function buildFormState() { function buildFormState() {
function extractOptions(id) { function extractOptions(id) {
const select = document.getElementById(`${id}-list`); const select = document.getElementById(`${id}-list`);
return Array.from(select.options).map(opt => ({ return Array.from(select.options).map(opt => {
name: opt.textContent.trim(), const name = opt.textContent.trim();
id: opt.value || undefined const rawId = opt.value?.trim();
})); return {
name,
...(rawId ? { id: /^\d+$/.test(rawId) ? parseInt(rawId, 10) : rawId } : {})
};
});
} }
function sanitizeFk(val) { function sanitizeFk(val) {
return val && val !== "null" && val !== "" ? val : null; if (val && val !== "null" && val !== "") {
return /^\d+$/.test(val) ? parseInt(val, 10) : val;
}
return null;
} }
const roomOptions = Array.from(document.getElementById('room-list').options); const roomOptions = Array.from(document.getElementById('room-list').options);
const rooms = roomOptions.map(opt => { const rooms = roomOptions.map(opt => {
const data = opt.dataset; const id = opt.value?.trim();
const name = opt.textContent.trim();
const sectionId = sanitizeFk(opt.dataset.sectionId);
const functionId = sanitizeFk(opt.dataset.functionId);
return { const result = {
id: opt.value || undefined, name,
name: opt.textContent.trim(), ...(id ? { id } : {}),
section_id: sanitizeFk(data.sectionId), ...(sectionId ? { section_id: sectionId } : {}),
function_id: sanitizeFk(data.functionId), ...(functionId ? { function_id: functionId } : {})
}; };
});
return result;
});
return { return {
brands: extractOptions("brand"), brands: extractOptions("brand"),
@ -223,9 +236,15 @@
form.addEventListener('submit', async (event) => { form.addEventListener('submit', async (event) => {
event.preventDefault(); event.preventDefault();
const state = buildFormState();
if (!isSerializable(state)) {
console.warn("🚨 Payload is not serializable:", state);
alert("Invalid payload — check console.");
return;
}
try { try {
const state = buildFormState();
const response = await fetch('/api/settings', { const response = await fetch('/api/settings', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@ -264,42 +283,26 @@
alert('Please enter a room name.'); alert('Please enter a room name.');
return; return;
} }
const roomList = document.getElementById('room-list'); const roomList = document.getElementById('room-list');
let existingOption = Array.from(roomList.options).find(opt => opt.value === idRaw); let existingOption = Array.from(roomList.options).find(opt => opt.value === idRaw);
// If it's a brand new ID, generate one (string-based!)
if (!idRaw) { if (!idRaw) {
idRaw = ComboBoxWidget.createTempId("room"); idRaw = ComboBoxWidget.createTempId("room");
} }
if (!existingOption) { if (!existingOption) {
existingOption = ComboBoxWidget.createOption(name, idRaw); existingOption = ComboBoxWidget.createOption(name, idRaw);
roomList.appendChild(existingOption); roomList.appendChild(existingOption);
} }
existingOption.textContent = name; existingOption.textContent = name;
existingOption.value = idRaw; existingOption.value = idRaw;
existingOption.dataset.sectionId = sectionVal || ""; existingOption.dataset.sectionId = sectionVal || "";
existingOption.dataset.functionId = funcVal || ""; existingOption.dataset.functionId = funcVal || "";
ComboBoxWidget.sortOptions(roomList); ComboBoxWidget.sortOptions(roomList);
// Update formState.rooms
const index = formState.rooms.findIndex(r => r.id === idRaw);
const payload = {
id: idRaw,
name,
section_id: sectionVal !== "" ? sectionVal : null,
function_id: funcVal !== "" ? funcVal : null
};
if (index >= 0) {
formState.rooms[index] = payload;
} else {
formState.rooms.push(payload);
}
bootstrap.Modal.getInstance(modal).hide(); bootstrap.Modal.getInstance(modal).hide();
}); });