Refactor ComboBoxWidget and settings form: remove unused handleComboAdd function, enhance form state serialization, and improve room option handling
This commit is contained in:
parent
31913eab47
commit
76d2799e05
3 changed files with 80 additions and 106 deletions
|
@ -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
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -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 %}
|
|
@ -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();
|
||||||
try {
|
const state = buildFormState();
|
||||||
const state = buildFormState();
|
|
||||||
|
|
||||||
|
if (!isSerializable(state)) {
|
||||||
|
console.warn("🚨 Payload is not serializable:", state);
|
||||||
|
alert("Invalid payload — check console.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
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' },
|
||||||
|
@ -268,7 +287,6 @@
|
||||||
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");
|
||||||
}
|
}
|
||||||
|
@ -285,21 +303,6 @@
|
||||||
|
|
||||||
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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue