New combobox placement for room editor.
This commit is contained in:
parent
1ccdf89739
commit
5a6125167c
7 changed files with 104 additions and 28 deletions
|
|
@ -6,7 +6,7 @@ if TYPE_CHECKING:
|
||||||
from .users import User
|
from .users import User
|
||||||
|
|
||||||
from sqlalchemy import ForeignKey, Identity, Integer, Unicode
|
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
|
from . import db
|
||||||
|
|
||||||
|
|
@ -26,6 +26,22 @@ class Room(ValidatableMixin, db.Model):
|
||||||
inventory: Mapped[List['Inventory']] = relationship('Inventory', back_populates='location')
|
inventory: Mapped[List['Inventory']] = relationship('Inventory', back_populates='location')
|
||||||
users: Mapped[List['User']] = relationship('User', 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):
|
def __init__(self, name: Optional[str] = None, area_id: Optional[int] = None, function_id: Optional[int] = None):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.area_id = area_id
|
self.area_id = area_id
|
||||||
|
|
@ -76,13 +92,13 @@ class Room(ValidatableMixin, db.Model):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
rid = room.get("id")
|
rid = room.get("id")
|
||||||
section_id = room.get("section_id")
|
area_id = room.get("area_id")
|
||||||
function_id = room.get("function_id")
|
function_id = room.get("function_id")
|
||||||
|
|
||||||
submitted_clean.append({
|
submitted_clean.append({
|
||||||
"id": rid,
|
"id": rid,
|
||||||
"name": name,
|
"name": name,
|
||||||
"section_id": section_id,
|
"area_id": area_id,
|
||||||
"function_id": function_id
|
"function_id": function_id
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -100,11 +116,11 @@ class Room(ValidatableMixin, db.Model):
|
||||||
rid = entry.get("id")
|
rid = entry.get("id")
|
||||||
name = entry["name"]
|
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")
|
resolved_function_id = resolve_fk(entry.get("function_id"), function_map, "function")
|
||||||
|
|
||||||
if not rid or str(rid).startswith("room-"):
|
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)
|
db.session.add(new_room)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
|
|
@ -120,8 +136,8 @@ class Room(ValidatableMixin, db.Model):
|
||||||
|
|
||||||
if room.name != name:
|
if room.name != name:
|
||||||
room.name = name
|
room.name = name
|
||||||
if room.area_id != resolved_section_id:
|
if room.area_id != resolved_area_id:
|
||||||
room.area_id = resolved_section_id
|
room.area_id = resolved_area_id
|
||||||
if room.function_id != resolved_function_id:
|
if room.function_id != resolved_function_id:
|
||||||
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
|
# Skip if a newly added room matches this one — likely duplicate
|
||||||
if any(
|
if any(
|
||||||
r["name"] == room.name and
|
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
|
resolve_fk(r["function_id"], function_map, "function") == room.function_id
|
||||||
for r in submitted_clean
|
for r in submitted_clean
|
||||||
if r.get("id") is None or str(r.get("id")).startswith("room-")
|
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}")
|
errors.append(f"{label} has an invalid ID: {raw_id}")
|
||||||
|
|
||||||
# These fields are FK IDs, so we're just checking for valid formats here.
|
# 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)
|
fk_val = item.get(fk_field)
|
||||||
|
|
||||||
if fk_val is None:
|
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}")
|
errors.append(f"{label} has invalid {fk_label} ID: {fk_val}")
|
||||||
|
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
Room.ui_eagerload = (
|
||||||
|
joinedload(Room.area),
|
||||||
|
joinedload(Room.room_function),
|
||||||
|
selectinload(Room.inventory),
|
||||||
|
selectinload(Room.users)
|
||||||
|
)
|
||||||
|
|
@ -32,13 +32,13 @@ def settings():
|
||||||
submitted_rooms = []
|
submitted_rooms = []
|
||||||
for room in state.get("rooms", []):
|
for room in state.get("rooms", []):
|
||||||
room = dict(room) # shallow copy
|
room = dict(room) # shallow copy
|
||||||
sid = room.get("section_id")
|
sid = room.get("area_id")
|
||||||
fid = room.get("function_id")
|
fid = room.get("function_id")
|
||||||
|
|
||||||
if sid is not None:
|
if sid is not None:
|
||||||
sid_key = str(sid)
|
sid_key = str(sid)
|
||||||
if sid_key in section_map:
|
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:
|
if fid is not None:
|
||||||
fid_key = str(fid)
|
fid_key = str(fid)
|
||||||
|
|
|
||||||
|
|
@ -172,10 +172,15 @@ function ComboBox(cfg) {
|
||||||
} else if (this.createUrl) {
|
} else if (this.createUrl) {
|
||||||
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 = 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.query = '';
|
this.query = '';
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ create_url = none, edit_url = none, delete_url = none, refresh_url = none
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
{% if refresh_url %}
|
{% 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 }}"
|
<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">
|
hx-trigger="revealed, combobox:refresh from:#{{ id }}-container" hx-target="#{{ id }}-list" hx-swap="innerHTML">
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@
|
||||||
const result = {
|
const result = {
|
||||||
name,
|
name,
|
||||||
...(id ? { id } : {}),
|
...(id ? { id } : {}),
|
||||||
section_id: sectionId,
|
area_id: sectionId,
|
||||||
function_id: functionId
|
function_id: functionId
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -167,13 +167,12 @@
|
||||||
<div class="col">
|
<div class="col">
|
||||||
{{ combos.dynamic_combobox(
|
{{ combos.dynamic_combobox(
|
||||||
id='function',
|
id='function',
|
||||||
options=functions,
|
|
||||||
label='Functions',
|
label='Functions',
|
||||||
placeholder='Add a new function',
|
placeholder='Add a new function',
|
||||||
create_url=url_for('ui.create_item', model_name='function'),
|
create_url=url_for('ui.create_item', model_name='room_function'),
|
||||||
edit_url=url_for('ui.update_item', model_name='function'),
|
edit_url=url_for('ui.update_item', model_name='room_function'),
|
||||||
refresh_url=url_for('ui.list_items', model_name='function'),
|
refresh_url=url_for('ui.list_items', model_name='room_function'),
|
||||||
delete_url=url_for('ui.delete_item', model_name='function')
|
delete_url=url_for('ui.delete_item', model_name='room_function')
|
||||||
) }}
|
) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -200,14 +199,15 @@
|
||||||
document.getElementById('room-input').value = '';
|
document.getElementById('room-input').value = '';
|
||||||
{% endset %}
|
{% endset %}
|
||||||
<div class="col">
|
<div class="col">
|
||||||
{{ combos.render_combobox(
|
{{ combos.dynamic_combobox(
|
||||||
id='room',
|
id='room',
|
||||||
options=rooms,
|
|
||||||
label='Rooms',
|
label='Rooms',
|
||||||
placeholder='Add a new room',
|
placeholder='Add a new room',
|
||||||
onAdd=room_editor,
|
data_attributes={'area_id': 'section-id', 'function_id': 'function-id'},
|
||||||
onEdit=room_editor,
|
create_url=url_for('ui.create_item', model_name='room'),
|
||||||
data_attributes={'area_id': 'section-id', 'function_id': 'function-id'}
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -268,6 +268,7 @@
|
||||||
<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 modal = document.getElementById('roomEditor');
|
||||||
const name = document.getElementById('roomName').value.trim();
|
const name = document.getElementById('roomName').value.trim();
|
||||||
const sectionVal = document.getElementById('roomSection').value;
|
const sectionVal = document.getElementById('roomSection').value;
|
||||||
|
|
@ -299,6 +300,29 @@
|
||||||
ComboBoxWidget.sortOptions(roomList);
|
ComboBoxWidget.sortOptions(roomList);
|
||||||
|
|
||||||
bootstrap.Modal.getInstance(modal).hide();
|
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 %}
|
{% endset %}
|
||||||
{{ buttons.render_button(
|
{{ buttons.render_button(
|
||||||
id='editorSave',
|
id='editorSave',
|
||||||
|
|
@ -351,4 +375,22 @@
|
||||||
cancelButton.addEventListener('click', () => {
|
cancelButton.addEventListener('click', () => {
|
||||||
bootstrap.Modal.getInstance(modal).hide();
|
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 %}
|
{% endblock %}
|
||||||
|
|
@ -43,10 +43,12 @@ def call(Model, name, *args, **kwargs):
|
||||||
@bp.get("/<model_name>/list")
|
@bp.get("/<model_name>/list")
|
||||||
def list_items(model_name):
|
def list_items(model_name):
|
||||||
Model = get_model_class(model_name)
|
Model = get_model_class(model_name)
|
||||||
text = (request.args.get("q") or "").strip() or None
|
text = (request.args.get("q") or "").strip() or None
|
||||||
limit = min(int(request.args.get("limit", 100)), 500)
|
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))
|
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) \
|
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)
|
or default_query(db.session, Model, text=text, limit=limit, offset=offset)
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,11 @@ def default_query(session, Model, *, text=None, limit=100, offset=0, filters=Non
|
||||||
if col is not None:
|
if col is not None:
|
||||||
stmt = stmt.order_by(col)
|
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()
|
return session.execute(stmt).scalars().all()
|
||||||
|
|
||||||
def default_create(session, Model, payload):
|
def default_create(session, Model, payload):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue