diff --git a/inventory/static/js/combobox.js b/inventory/static/js/combobox.js index 03f4258..558b037 100644 --- a/inventory/static/js/combobox.js +++ b/inventory/static/js/combobox.js @@ -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) { return { id: cfg.id, @@ -139,16 +9,13 @@ function ComboBox(cfg) { query: '', isEditing: false, editingOption: null, - - // NEW: keep selection reactive selectedIds: [], - // Button disable uses reactive data now get hasSelection() { return this.selectedIds.length > 0 }, onListChange() { 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) { this.query = sel[0].textContent.trim(); @@ -173,13 +40,11 @@ function ComboBox(cfg) { const data = await this._post(this.createUrl, { name }, true); const id = (data && data.id) ? data.id : ('temp-' + Math.random().toString(36).slice(2)); - // add option optimistically const opt = document.createElement('option'); opt.value = id; opt.textContent = data?.name || name; this.$refs.list.appendChild(opt); this._sortOptions(); - // ✅ NEW: tell the world we created something this.$dispatch('combobox:item-created', { id, name: data?.name || name }); } @@ -189,7 +54,7 @@ function ComboBox(cfg) { }, async removeSelected() { - const ids = [...this.selectedIds]; // <-- capture IDs before DOM changes + const ids = [...this.selectedIds]; if (!ids.length) return; if (!confirm(`Delete ${ids.length} item(s)?`)) return; diff --git a/inventory/templates/fragments/_combobox_fragment.html b/inventory/templates/fragments/_combobox_fragment.html index 3cd7720..798bcec 100644 --- a/inventory/templates/fragments/_combobox_fragment.html +++ b/inventory/templates/fragments/_combobox_fragment.html @@ -1,45 +1,6 @@ {% 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) %} - - -{% if label %} - -{% endif %} -
- - -{% endmacro %} - -{% macro dynamic_combobox( +{% macro render_combobox( id, options, label = none, placeholder = none, data_attributes = none, create_url = none, edit_url = none, delete_url = none, refresh_url = none ) %} diff --git a/inventory/templates/playground.html b/inventory/templates/playground.html index 3b02008..a5c09d2 100644 --- a/inventory/templates/playground.html +++ b/inventory/templates/playground.html @@ -1,7 +1,7 @@ {% extends 'layout.html' %} {% block content %} - {{ combos.dynamic_combobox( + {{ combos.render_combobox( id='play', label='Breakfast!', create_url=url_for('ui.create_item', model_name='brand'), diff --git a/inventory/templates/settings.html b/inventory/templates/settings.html index 35ee1b4..f6717dc 100644 --- a/inventory/templates/settings.html +++ b/inventory/templates/settings.html @@ -3,108 +3,9 @@ {% block title %}{{ title }}{% endblock %} {% 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( id='settings', - left=breadcrumb_macro.render_breadcrumb(breadcrumbs=breadcrumbs), - right=buttons.render_button( - id='save', - icon='floppy', - logic=saveLogic, - style='outline-primary' - ) + left=breadcrumb_macro.render_breadcrumb(breadcrumbs=breadcrumbs) ) }} {% endblock %} @@ -128,7 +29,7 @@