From 5a6125167ce45d0d7987033d04db1684d0ce5980 Mon Sep 17 00:00:00 2001 From: Yaro Kasear Date: Thu, 14 Aug 2025 10:18:22 -0500 Subject: [PATCH] New combobox placement for room editor. --- inventory/models/rooms.py | 41 +++++++++--- inventory/routes/settings.py | 4 +- inventory/static/js/combobox.js | 7 +- .../fragments/_combobox_fragment.html | 2 +- inventory/templates/settings.html | 64 +++++++++++++++---- inventory/ui/blueprint.py | 8 ++- inventory/ui/defaults.py | 6 +- 7 files changed, 104 insertions(+), 28 deletions(-) diff --git a/inventory/models/rooms.py b/inventory/models/rooms.py index b9ea03b..4081e87 100644 --- a/inventory/models/rooms.py +++ b/inventory/models/rooms.py @@ -6,7 +6,7 @@ if TYPE_CHECKING: from .users import User from sqlalchemy import ForeignKey, Identity, Integer, Unicode -from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm import Mapped, mapped_column, relationship, joinedload, selectinload from . import db @@ -26,6 +26,22 @@ class Room(ValidatableMixin, db.Model): inventory: Mapped[List['Inventory']] = relationship('Inventory', back_populates='location') users: Mapped[List['User']] = relationship('User', back_populates='location') + ui_label_attr = 'name' + ui_eagerload = tuple() + ui_extra_attrs = ('area_id', 'function_id') + + @classmethod + def ui_update(cls, session, id_, payload): + print(payload) + obj = session.get(cls, id_) + if not obj: + return None + obj.name = payload.get("name", obj.name) + obj.area_id = payload.get("area_id", obj.area_id) + obj.function_id = payload.get("function_id", obj.function_id) + session.commit() + return obj + def __init__(self, name: Optional[str] = None, area_id: Optional[int] = None, function_id: Optional[int] = None): self.name = name self.area_id = area_id @@ -76,13 +92,13 @@ class Room(ValidatableMixin, db.Model): continue rid = room.get("id") - section_id = room.get("section_id") + area_id = room.get("area_id") function_id = room.get("function_id") submitted_clean.append({ "id": rid, "name": name, - "section_id": section_id, + "area_id": area_id, "function_id": function_id }) @@ -100,11 +116,11 @@ class Room(ValidatableMixin, db.Model): rid = entry.get("id") name = entry["name"] - resolved_section_id = resolve_fk(entry.get("section_id"), section_map, "section") + resolved_area_id = resolve_fk(entry.get("area_id"), section_map, "section") resolved_function_id = resolve_fk(entry.get("function_id"), function_map, "function") if not rid or str(rid).startswith("room-"): - new_room = cls(name=name, area_id=resolved_section_id, function_id=resolved_function_id) + new_room = cls(name=name, area_id=resolved_area_id, function_id=resolved_function_id) db.session.add(new_room) else: try: @@ -120,8 +136,8 @@ class Room(ValidatableMixin, db.Model): if room.name != name: room.name = name - if room.area_id != resolved_section_id: - room.area_id = resolved_section_id + if room.area_id != resolved_area_id: + room.area_id = resolved_area_id if room.function_id != resolved_function_id: room.function_id = resolved_function_id @@ -133,7 +149,7 @@ class Room(ValidatableMixin, db.Model): # Skip if a newly added room matches this one — likely duplicate if any( r["name"] == room.name and - resolve_fk(r["section_id"], section_map, "section") == room.area_id and + resolve_fk(r["area_id"], section_map, "section") == room.area_id and resolve_fk(r["function_id"], function_map, "function") == room.function_id for r in submitted_clean if r.get("id") is None or str(r.get("id")).startswith("room-") @@ -167,7 +183,7 @@ class Room(ValidatableMixin, db.Model): errors.append(f"{label} has an invalid ID: {raw_id}") # These fields are FK IDs, so we're just checking for valid formats here. - for fk_field, fk_label in [("section_id", "Section"), ("function_id", "Function")]: + for fk_field, fk_label in [("area_id", "Section"), ("function_id", "Function")]: fk_val = item.get(fk_field) if fk_val is None: @@ -181,3 +197,10 @@ class Room(ValidatableMixin, db.Model): errors.append(f"{label} has invalid {fk_label} ID: {fk_val}") return errors + +Room.ui_eagerload = ( + joinedload(Room.area), + joinedload(Room.room_function), + selectinload(Room.inventory), + selectinload(Room.users) +) \ No newline at end of file diff --git a/inventory/routes/settings.py b/inventory/routes/settings.py index 1cd1433..00cc98c 100644 --- a/inventory/routes/settings.py +++ b/inventory/routes/settings.py @@ -32,13 +32,13 @@ def settings(): submitted_rooms = [] for room in state.get("rooms", []): room = dict(room) # shallow copy - sid = room.get("section_id") + sid = room.get("area_id") fid = room.get("function_id") if sid is not None: sid_key = str(sid) if sid_key in section_map: - room["section_id"] = section_map[sid_key] + room["area_id"] = section_map[sid_key] if fid is not None: fid_key = str(fid) diff --git a/inventory/static/js/combobox.js b/inventory/static/js/combobox.js index e8bb099..03f4258 100644 --- a/inventory/static/js/combobox.js +++ b/inventory/static/js/combobox.js @@ -172,10 +172,15 @@ function ComboBox(cfg) { } else if (this.createUrl) { 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 = name; + 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 }); } this.query = ''; diff --git a/inventory/templates/fragments/_combobox_fragment.html b/inventory/templates/fragments/_combobox_fragment.html index 45cb626..3cd7720 100644 --- a/inventory/templates/fragments/_combobox_fragment.html +++ b/inventory/templates/fragments/_combobox_fragment.html @@ -79,7 +79,7 @@ create_url = none, edit_url = none, delete_url = none, refresh_url = none {% if refresh_url %} - {% set url = refresh_url ~ ('&' if '?' in refresh_url else '?') ~ 'view=option' %} + {% set url = refresh_url ~ ('&' if '?' in refresh_url else '?') ~ 'view=option&limit=0' %}
diff --git a/inventory/templates/settings.html b/inventory/templates/settings.html index 46db84c..35ee1b4 100644 --- a/inventory/templates/settings.html +++ b/inventory/templates/settings.html @@ -44,7 +44,7 @@ const result = { name, ...(id ? { id } : {}), - section_id: sectionId, + area_id: sectionId, function_id: functionId }; @@ -167,13 +167,12 @@
{{ combos.dynamic_combobox( id='function', - options=functions, label='Functions', placeholder='Add a new function', - create_url=url_for('ui.create_item', model_name='function'), - edit_url=url_for('ui.update_item', model_name='function'), - refresh_url=url_for('ui.list_items', model_name='function'), - delete_url=url_for('ui.delete_item', model_name='function') + create_url=url_for('ui.create_item', model_name='room_function'), + edit_url=url_for('ui.update_item', model_name='room_function'), + refresh_url=url_for('ui.list_items', model_name='room_function'), + delete_url=url_for('ui.delete_item', model_name='room_function') ) }}
@@ -200,14 +199,15 @@ document.getElementById('room-input').value = ''; {% endset %}
- {{ combos.render_combobox( + {{ combos.dynamic_combobox( id='room', - options=rooms, label='Rooms', placeholder='Add a new room', - onAdd=room_editor, - onEdit=room_editor, - data_attributes={'area_id': 'section-id', 'function_id': 'function-id'} + data_attributes={'area_id': 'section-id', 'function_id': 'function-id'}, + create_url=url_for('ui.create_item', model_name='room'), + edit_url=url_for('ui.update_item', model_name='room'), + refresh_url=url_for('ui.list_items', model_name='room'), + delete_url=url_for('ui.delete_item', model_name='room') ) }}
@@ -268,6 +268,7 @@ {% set editorSaveLogic %} + {# const modal = document.getElementById('roomEditor'); const name = document.getElementById('roomName').value.trim(); const sectionVal = document.getElementById('roomSection').value; @@ -299,6 +300,29 @@ ComboBoxWidget.sortOptions(roomList); bootstrap.Modal.getInstance(modal).hide(); + #} + const modalEl = document.getElementById('roomEditor'); + const idRaw = document.getElementById('roomId').value; + const name = document.getElementById('roomName').value.trim(); + const sectionId = document.getElementById('roomSection').value || null; + const functionId = document.getElementById('roomFunction').value || null; + + if (!name) { alert('Please enter a room name.'); return; } + if (!idRaw) { alert('Missing room ID.'); return; } + + (async () => { + const res = await fetch('{{ url_for("ui.update_item", model_name="room") }}', { + method: 'POST', + headers: {'Content-Type':'application/json'}, + body: JSON.stringify({ id: parseInt(idRaw, 10), name, area_id: sectionId, function_id: functionId }) + }); + if (!res.ok) { + const txt = await res.text().catch(()=> 'Error'); alert(txt); return; + } + + htmx.trigger('#room-container', 'combobox:refresh'); + bootstrap.Modal.getInstance(modalEl).hide(); + })(); {% endset %} {{ buttons.render_button( id='editorSave', @@ -351,4 +375,22 @@ cancelButton.addEventListener('click', () => { bootstrap.Modal.getInstance(modal).hide(); }); + + (function () { + const container = document.getElementById('room-container'); + if (!container) return; + + container.addEventListener('combobox:item-created', (e) => { + if (container.id !== 'room-container') return; + + const { id, name } = e.detail || {}; + const prep = new CustomEvent('roomEditor:prepare', { + detail: { id, name, sectionId: '', functionId: '' } + }); + document.getElementById('roomEditor').dispatchEvent(prep); + + const roomEditorModal = new bootstrap.Modal(document.getElementById('roomEditor')); + roomEditorModal.show(); + }); + })(); {% endblock %} \ No newline at end of file diff --git a/inventory/ui/blueprint.py b/inventory/ui/blueprint.py index 8ea5c26..d06fbdd 100644 --- a/inventory/ui/blueprint.py +++ b/inventory/ui/blueprint.py @@ -43,10 +43,12 @@ def call(Model, name, *args, **kwargs): @bp.get("//list") def list_items(model_name): Model = get_model_class(model_name) - text = (request.args.get("q") or "").strip() or None - limit = min(int(request.args.get("limit", 100)), 500) + text = (request.args.get("q") or "").strip() or None + limit_param = request.args.get("limit") + limit = None if limit_param in (None, "", "0", "-1") else min(int(limit_param), 500) + # limit = min(int(request.args.get("limit", 100)), 500) offset = int(request.args.get("offset", 0)) - view = (request.args.get("view") or "json").strip() + view = (request.args.get("view") or "json").strip() rows = call(Model, "ui_query", db.session, text=text, limit=limit, offset=offset) \ or default_query(db.session, Model, text=text, limit=limit, offset=offset) diff --git a/inventory/ui/defaults.py b/inventory/ui/defaults.py index 5212156..e30b720 100644 --- a/inventory/ui/defaults.py +++ b/inventory/ui/defaults.py @@ -59,7 +59,11 @@ def default_query(session, Model, *, text=None, limit=100, offset=0, filters=Non if col is not None: stmt = stmt.order_by(col) - stmt = stmt.limit(limit).offset(offset) + # stmt = stmt.limit(limit).offset(offset) + if limit is not None: + stmt = stmt.limit(limit) + if offset: + stmt = stmt.offset(offset) return session.execute(stmt).scalars().all() def default_create(session, Model, payload):