Cleanup.
This commit is contained in:
parent
5a6125167c
commit
1ea6cd9588
4 changed files with 10 additions and 337 deletions
|
|
@ -1,133 +1,3 @@
|
||||||
const ComboBoxWidget = (() => {
|
|
||||||
let tempIdCounter = 1;
|
|
||||||
|
|
||||||
function createTempId(prefix = "temp") {
|
|
||||||
return `${prefix}-${tempIdCounter++}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createOption(text, value = null) {
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.textContent = text;
|
|
||||||
option.value = value ?? createTempId();
|
|
||||||
return option;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortOptions(selectElement) {
|
|
||||||
const sorted = Array.from(selectElement.options)
|
|
||||||
.sort((a, b) => a.text.localeCompare(b.text));
|
|
||||||
selectElement.innerHTML = '';
|
|
||||||
sorted.forEach(option => selectElement.appendChild(option));
|
|
||||||
}
|
|
||||||
|
|
||||||
function initComboBox(ns, config = {}) {
|
|
||||||
const input = document.querySelector(`#${ns}-input`);
|
|
||||||
const list = document.querySelector(`#${ns}-list`);
|
|
||||||
const addBtn = document.querySelector(`#${ns}-add`);
|
|
||||||
const removeBtn = document.querySelector(`#${ns}-remove`);
|
|
||||||
let currentlyEditing = null;
|
|
||||||
|
|
||||||
if (!input || !list || !addBtn || !removeBtn) {
|
|
||||||
console.warn(`ComboBoxWidget: Missing elements for namespace '${ns}'`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateAddButtonIcon() {
|
|
||||||
const iconEl = addBtn.querySelector('.icon-state');
|
|
||||||
|
|
||||||
const iconClass = currentlyEditing ? 'bi-pencil' : 'bi-plus-lg';
|
|
||||||
iconEl.classList.forEach(cls => {
|
|
||||||
if (cls.startsWith('bi-') && cls !== 'icon-state') {
|
|
||||||
iconEl.classList.remove(cls);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
iconEl.classList.add(iconClass);
|
|
||||||
}
|
|
||||||
|
|
||||||
input.addEventListener('input', () => {
|
|
||||||
addBtn.disabled = input.value.trim() === '';
|
|
||||||
updateAddButtonIcon();
|
|
||||||
});
|
|
||||||
|
|
||||||
list.addEventListener('change', () => {
|
|
||||||
const selected = list.selectedOptions;
|
|
||||||
removeBtn.disabled = selected.length === 0;
|
|
||||||
|
|
||||||
if (selected.length === 1) {
|
|
||||||
input.value = selected[0].textContent.trim();
|
|
||||||
currentlyEditing = selected[0];
|
|
||||||
addBtn.disabled = input.value.trim() === '';
|
|
||||||
} else {
|
|
||||||
input.value = '';
|
|
||||||
currentlyEditing = null;
|
|
||||||
addBtn.disabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateAddButtonIcon();
|
|
||||||
});
|
|
||||||
|
|
||||||
addBtn.addEventListener('click', () => {
|
|
||||||
const newItem = input.value.trim();
|
|
||||||
if (!newItem) return;
|
|
||||||
|
|
||||||
if (currentlyEditing) {
|
|
||||||
if (config.onEdit) {
|
|
||||||
config.onEdit(currentlyEditing, newItem);
|
|
||||||
} else {
|
|
||||||
currentlyEditing.textContent = newItem;
|
|
||||||
}
|
|
||||||
currentlyEditing = null;
|
|
||||||
} else {
|
|
||||||
if (config.onAdd) {
|
|
||||||
config.onAdd(newItem, list, createOption);
|
|
||||||
return; // Skip the default logic!
|
|
||||||
}
|
|
||||||
|
|
||||||
const exists = Array.from(list.options).some(opt => opt.textContent === newItem);
|
|
||||||
if (exists) {
|
|
||||||
alert(`"${newItem}" already exists.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const option = createOption(newItem);
|
|
||||||
list.appendChild(option);
|
|
||||||
|
|
||||||
const key = config.stateArray ?? `${ns}s`; // fallback to pluralization
|
|
||||||
|
|
||||||
if (config.sort !== false) {
|
|
||||||
sortOptions(list);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input.value = '';
|
|
||||||
addBtn.disabled = true;
|
|
||||||
removeBtn.disabled = true;
|
|
||||||
updateAddButtonIcon();
|
|
||||||
});
|
|
||||||
|
|
||||||
removeBtn.addEventListener('click', () => {
|
|
||||||
Array.from(list.selectedOptions).forEach(option => {
|
|
||||||
if (config.onRemove) {
|
|
||||||
config.onRemove(option);
|
|
||||||
}
|
|
||||||
option.remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
currentlyEditing = null;
|
|
||||||
input.value = '';
|
|
||||||
addBtn.disabled = true;
|
|
||||||
removeBtn.disabled = true;
|
|
||||||
updateAddButtonIcon();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
initComboBox,
|
|
||||||
createOption,
|
|
||||||
sortOptions,
|
|
||||||
createTempId
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
function ComboBox(cfg) {
|
function ComboBox(cfg) {
|
||||||
return {
|
return {
|
||||||
id: cfg.id,
|
id: cfg.id,
|
||||||
|
|
@ -139,16 +9,13 @@ function ComboBox(cfg) {
|
||||||
query: '',
|
query: '',
|
||||||
isEditing: false,
|
isEditing: false,
|
||||||
editingOption: null,
|
editingOption: null,
|
||||||
|
|
||||||
// NEW: keep selection reactive
|
|
||||||
selectedIds: [],
|
selectedIds: [],
|
||||||
|
|
||||||
// Button disable uses reactive data now
|
|
||||||
get hasSelection() { return this.selectedIds.length > 0 },
|
get hasSelection() { return this.selectedIds.length > 0 },
|
||||||
|
|
||||||
onListChange() {
|
onListChange() {
|
||||||
const sel = Array.from(this.$refs.list.selectedOptions);
|
const sel = Array.from(this.$refs.list.selectedOptions);
|
||||||
this.selectedIds = sel.map(o => o.value); // <-- reactive update
|
this.selectedIds = sel.map(o => o.value);
|
||||||
|
|
||||||
if (sel.length === 1) {
|
if (sel.length === 1) {
|
||||||
this.query = sel[0].textContent.trim();
|
this.query = sel[0].textContent.trim();
|
||||||
|
|
@ -173,13 +40,11 @@ function ComboBox(cfg) {
|
||||||
const data = await this._post(this.createUrl, { name }, true);
|
const data = await this._post(this.createUrl, { name }, true);
|
||||||
const id = (data && data.id) ? data.id : ('temp-' + Math.random().toString(36).slice(2));
|
const id = (data && data.id) ? data.id : ('temp-' + Math.random().toString(36).slice(2));
|
||||||
|
|
||||||
// add option optimistically
|
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = id; opt.textContent = data?.name || name;
|
opt.value = id; opt.textContent = data?.name || name;
|
||||||
this.$refs.list.appendChild(opt);
|
this.$refs.list.appendChild(opt);
|
||||||
this._sortOptions();
|
this._sortOptions();
|
||||||
|
|
||||||
// ✅ NEW: tell the world we created something
|
|
||||||
this.$dispatch('combobox:item-created', { id, name: data?.name || name });
|
this.$dispatch('combobox:item-created', { id, name: data?.name || name });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -189,7 +54,7 @@ function ComboBox(cfg) {
|
||||||
},
|
},
|
||||||
|
|
||||||
async removeSelected() {
|
async removeSelected() {
|
||||||
const ids = [...this.selectedIds]; // <-- capture IDs before DOM changes
|
const ids = [...this.selectedIds];
|
||||||
if (!ids.length) return;
|
if (!ids.length) return;
|
||||||
if (!confirm(`Delete ${ids.length} item(s)?`)) return;
|
if (!confirm(`Delete ${ids.length} item(s)?`)) return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,6 @@
|
||||||
{% 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,
|
{% macro render_combobox(
|
||||||
data_attributes=none) %}
|
|
||||||
<!-- Combobox Widget Fragment -->
|
|
||||||
|
|
||||||
{% if label %}
|
|
||||||
<label for="{{ id }}-input" class="form-label">{{ label }}</label>
|
|
||||||
{% endif %}
|
|
||||||
<div class="combo-box-widget" id="{{ id }}-container">
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" class="form-control rounded-bottom-0" id="{{ id }}-input" {% if placeholder %}
|
|
||||||
placeholder="{{ placeholder }}" {% endif %}>
|
|
||||||
<button type="button" class="btn btn-primary rounded-bottom-0" id="{{ id }}-add" disabled>
|
|
||||||
{{ icons.render_icon('plus-lg', 16, 'icon-state') }}
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-danger rounded-bottom-0" id="{{ id }}-remove" disabled>
|
|
||||||
{{ icons.render_icon('dash-lg', 16) }}
|
|
||||||
</button>
|
|
||||||
</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>
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
ComboBoxWidget.initComboBox("{{ id }}"{% if onAdd or onRemove or onEdit %}, {
|
|
||||||
{% if onAdd %}onAdd: function(newItem, list, createOption) { { { onAdd | safe } } }, {% endif %}
|
|
||||||
{% if onRemove %} onRemove: function(option) { { { onRemove | safe } } }, {% endif %}
|
|
||||||
{% if onEdit %} onEdit: function(option) { { { onEdit | safe } } } {% endif %}
|
|
||||||
}{% endif %});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro dynamic_combobox(
|
|
||||||
id, options, label = none, placeholder = none, data_attributes = none,
|
id, options, label = none, placeholder = none, data_attributes = none,
|
||||||
create_url = none, edit_url = none, delete_url = none, refresh_url = none
|
create_url = none, edit_url = none, delete_url = none, refresh_url = none
|
||||||
) %}
|
) %}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends 'layout.html' %}
|
{% extends 'layout.html' %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ combos.dynamic_combobox(
|
{{ combos.render_combobox(
|
||||||
id='play',
|
id='play',
|
||||||
label='Breakfast!',
|
label='Breakfast!',
|
||||||
create_url=url_for('ui.create_item', model_name='brand'),
|
create_url=url_for('ui.create_item', model_name='brand'),
|
||||||
|
|
|
||||||
|
|
@ -3,108 +3,9 @@
|
||||||
{% block title %}{{ title }}{% endblock %}
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
{% block precontent %}
|
{% block precontent %}
|
||||||
{% set saveLogic %}
|
|
||||||
function isSerializable(obj) {
|
|
||||||
try {
|
|
||||||
JSON.stringify(obj);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildFormState() {
|
|
||||||
function extractOptions(id) {
|
|
||||||
const select = document.getElementById(`${id}-list`);
|
|
||||||
return Array.from(select.options).map(opt => {
|
|
||||||
const name = opt.textContent.trim();
|
|
||||||
const rawId = opt.value?.trim();
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
...(rawId ? { id: /^\d+$/.test(rawId) ? parseInt(rawId, 10) : rawId } : {})
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function sanitizeFk(val) {
|
|
||||||
if (val && val !== "null" && val !== "" && val !== "None") {
|
|
||||||
return /^\d+$/.test(val) ? parseInt(val, 10) : val;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const roomOptions = Array.from(document.getElementById('room-list').options);
|
|
||||||
|
|
||||||
const rooms = roomOptions.map(opt => {
|
|
||||||
const id = opt.value?.trim();
|
|
||||||
const name = opt.textContent.trim();
|
|
||||||
const sectionId = sanitizeFk(opt.dataset.sectionId);
|
|
||||||
const functionId = sanitizeFk(opt.dataset.functionId);
|
|
||||||
|
|
||||||
const result = {
|
|
||||||
name,
|
|
||||||
...(id ? { id } : {}),
|
|
||||||
area_id: sectionId,
|
|
||||||
function_id: functionId
|
|
||||||
};
|
|
||||||
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
brands: extractOptions("brand"),
|
|
||||||
types: extractOptions("type"),
|
|
||||||
sections: extractOptions("section"),
|
|
||||||
functions: extractOptions("function"),
|
|
||||||
rooms
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
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', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(state)
|
|
||||||
});
|
|
||||||
|
|
||||||
const contentType = response.headers.get("content-type");
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
if (contentType && contentType.includes("application/json")) {
|
|
||||||
const data = await response.json();
|
|
||||||
throw new Error(data.errors?.join("\n") || "Unknown error");
|
|
||||||
} else {
|
|
||||||
const text = await response.text();
|
|
||||||
throw new Error("Unexpected response:\n" + text.slice(0, 200));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
Toast.renderToast({ message: 'Settings updated successfully.', type: 'success' });
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
Toast.renderToast({ message: `Failed to update settings, ${err}`, type: 'danger' });
|
|
||||||
}
|
|
||||||
{% endset %}
|
|
||||||
{{ toolbars.render_toolbar(
|
{{ toolbars.render_toolbar(
|
||||||
id='settings',
|
id='settings',
|
||||||
left=breadcrumb_macro.render_breadcrumb(breadcrumbs=breadcrumbs),
|
left=breadcrumb_macro.render_breadcrumb(breadcrumbs=breadcrumbs)
|
||||||
right=buttons.render_button(
|
|
||||||
id='save',
|
|
||||||
icon='floppy',
|
|
||||||
logic=saveLogic,
|
|
||||||
style='outline-primary'
|
|
||||||
)
|
|
||||||
) }}
|
) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
@ -128,7 +29,7 @@
|
||||||
<div class="tab-pane fade show active border border-top-0 p-3" id="inventory-tab-pane">
|
<div class="tab-pane fade show active border border-top-0 p-3" id="inventory-tab-pane">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
{{ combos.dynamic_combobox(
|
{{ combos.render_combobox(
|
||||||
id='brand',
|
id='brand',
|
||||||
label='Brands',
|
label='Brands',
|
||||||
placeholder='Add a new brand',
|
placeholder='Add a new brand',
|
||||||
|
|
@ -139,7 +40,7 @@
|
||||||
) }}
|
) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
{{ combos.dynamic_combobox(
|
{{ combos.render_combobox(
|
||||||
id='type',
|
id='type',
|
||||||
label='Inventory Types',
|
label='Inventory Types',
|
||||||
placeholder='Add a new type',
|
placeholder='Add a new type',
|
||||||
|
|
@ -154,7 +55,7 @@
|
||||||
<div class="tab-pane fade border border-top-0 p-3" id="location-tab-pane">
|
<div class="tab-pane fade border border-top-0 p-3" id="location-tab-pane">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
{{ combos.dynamic_combobox(
|
{{ combos.render_combobox(
|
||||||
id='section',
|
id='section',
|
||||||
label='Sections',
|
label='Sections',
|
||||||
placeholder='Add a new section',
|
placeholder='Add a new section',
|
||||||
|
|
@ -165,7 +66,7 @@
|
||||||
) }}
|
) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
{{ combos.dynamic_combobox(
|
{{ combos.render_combobox(
|
||||||
id='function',
|
id='function',
|
||||||
label='Functions',
|
label='Functions',
|
||||||
placeholder='Add a new function',
|
placeholder='Add a new function',
|
||||||
|
|
@ -177,29 +78,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{% set room_editor %}
|
|
||||||
const roomEditorModal = new bootstrap.Modal(document.getElementById('roomEditor'));
|
|
||||||
const input = document.getElementById('room-input');
|
|
||||||
const name = input.value.trim();
|
|
||||||
const existingOption = Array.from(document.getElementById('room-list').options)
|
|
||||||
.find(opt => opt.textContent.trim() === name);
|
|
||||||
|
|
||||||
const event = new CustomEvent('roomEditor:prepare', {
|
|
||||||
detail: {
|
|
||||||
id: existingOption?.value ?? '',
|
|
||||||
name: name,
|
|
||||||
sectionId: existingOption?.dataset.sectionId ?? '',
|
|
||||||
functionId: existingOption?.dataset.functionId ?? ''
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('roomEditor').dispatchEvent(event);
|
|
||||||
roomEditorModal.show();
|
|
||||||
|
|
||||||
document.getElementById('room-input').value = '';
|
|
||||||
{% endset %}
|
|
||||||
<div class="col">
|
<div class="col">
|
||||||
{{ combos.dynamic_combobox(
|
{{ combos.render_combobox(
|
||||||
id='room',
|
id='room',
|
||||||
label='Rooms',
|
label='Rooms',
|
||||||
placeholder='Add a new room',
|
placeholder='Add a new room',
|
||||||
|
|
@ -268,39 +148,6 @@
|
||||||
<button type="button" class="btn btn-danger" data-bs-dismiss="modal" id="roomEditorCancelButton">{{
|
<button type="button" class="btn btn-danger" data-bs-dismiss="modal" id="roomEditorCancelButton">{{
|
||||||
icons.render_icon('x-lg', 16) }}</button>
|
icons.render_icon('x-lg', 16) }}</button>
|
||||||
{% set editorSaveLogic %}
|
{% set editorSaveLogic %}
|
||||||
{#
|
|
||||||
const modal = document.getElementById('roomEditor');
|
|
||||||
const name = document.getElementById('roomName').value.trim();
|
|
||||||
const sectionVal = document.getElementById('roomSection').value;
|
|
||||||
const funcVal = document.getElementById('roomFunction').value;
|
|
||||||
let idRaw = document.getElementById('roomId').value;
|
|
||||||
|
|
||||||
if (!name) {
|
|
||||||
alert('Please enter a room name.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const roomList = document.getElementById('room-list');
|
|
||||||
let existingOption = Array.from(roomList.options).find(opt => opt.value === idRaw);
|
|
||||||
|
|
||||||
if (!idRaw) {
|
|
||||||
idRaw = ComboBoxWidget.createTempId("room");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!existingOption) {
|
|
||||||
existingOption = ComboBoxWidget.createOption(name, idRaw);
|
|
||||||
roomList.appendChild(existingOption);
|
|
||||||
}
|
|
||||||
|
|
||||||
existingOption.textContent = name;
|
|
||||||
existingOption.value = idRaw;
|
|
||||||
existingOption.dataset.sectionId = sectionVal || "";
|
|
||||||
existingOption.dataset.functionId = funcVal || "";
|
|
||||||
|
|
||||||
ComboBoxWidget.sortOptions(roomList);
|
|
||||||
|
|
||||||
bootstrap.Modal.getInstance(modal).hide();
|
|
||||||
#}
|
|
||||||
const modalEl = document.getElementById('roomEditor');
|
const modalEl = document.getElementById('roomEditor');
|
||||||
const idRaw = document.getElementById('roomId').value;
|
const idRaw = document.getElementById('roomId').value;
|
||||||
const name = document.getElementById('roomName').value.trim();
|
const name = document.getElementById('roomName').value.trim();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue