New combobox placement for room editor.

This commit is contained in:
Yaro Kasear 2025-08-14 10:18:22 -05:00
parent 1ccdf89739
commit 5a6125167c
7 changed files with 104 additions and 28 deletions

View file

@ -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)
)

View file

@ -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)

View file

@ -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 = '';

View file

@ -79,7 +79,7 @@ create_url = none, edit_url = none, delete_url = none, refresh_url = none
</select>
{% 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' %}
<div id="{{ id }}-htmx-refresh" class="d-none" hx-get="{{ url }}"
hx-trigger="revealed, combobox:refresh from:#{{ id }}-container" hx-target="#{{ id }}-list" hx-swap="innerHTML">
</div>

View file

@ -44,7 +44,7 @@
const result = {
name,
...(id ? { id } : {}),
section_id: sectionId,
area_id: sectionId,
function_id: functionId
};
@ -167,13 +167,12 @@
<div class="col">
{{ 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')
) }}
</div>
</div>
@ -200,14 +199,15 @@
document.getElementById('room-input').value = '';
{% endset %}
<div class="col">
{{ 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')
) }}
</div>
</div>
@ -268,6 +268,7 @@
<button type="button" class="btn btn-danger" data-bs-dismiss="modal" id="roomEditorCancelButton">{{
icons.render_icon('x-lg', 16) }}</button>
{% 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 %}

View file

@ -43,10 +43,12 @@ def call(Model, name, *args, **kwargs):
@bp.get("/<model_name>/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)

View file

@ -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):