diff --git a/inventory/routes/__init__.py b/inventory/routes/__init__.py
index 601df49..da18176 100644
--- a/inventory/routes/__init__.py
+++ b/inventory/routes/__init__.py
@@ -1,15 +1,28 @@
+import logging
+
from flask import Blueprint, g
-from sqlalchemy.sql import Select
from sqlalchemy.engine import ScalarResult
+from sqlalchemy.orm import joinedload
+from sqlalchemy.sql import Select
from typing import Iterable, Any, cast
main = Blueprint('main', __name__)
+log = logging.getLogger(__name__)
from . import inventory, user, worklog, settings, index, search, hooks
from .. import db
from ..ui.blueprint import get_model_class, call
from ..ui.defaults import default_query
+def _eager_from_fields(Model, fields: Iterable[str]):
+ rels = {f.split(".", 1)[0] for f in fields if "." in f}
+ opts = []
+ for r in rels:
+ rel_attr = getattr(Model, r, None)
+ if getattr(rel_attr, "property", None) is not None:
+ opts.append(joinedload(rel_attr))
+ return opts
+
def _cell_cache():
if not hasattr(g, '_cell_cache'):
g._cell_cache = {}
@@ -22,7 +35,12 @@ def _tmpl_cache(name: str):
def _project_row(obj: Any, fields: Iterable[str]) -> dict[str, Any]:
out = {"id": obj.id}
+ Model = type(obj)
+ allow = getattr(Model, "ui_value_allow", None)
for f in fields:
+ if allow and f not in allow:
+ out[f] = None
+ continue
if "." in f:
rel, attr = f.split(".", 1)
relobj = getattr(obj, rel, None)
@@ -45,7 +63,8 @@ def cell(model_name: str, id_: int, field: str, default: str = ""):
val = call(Model, 'ui_value', db.session, id_=id_, field=field)
if val is None:
val = default_value(db.session, Model, id_=id_, field=field)
- except Exception:
+ except Exception as e:
+ log.warning(f"cell() error for {model_name} {id_} {field}: {e}")
val = default
if val is None:
val = default
@@ -106,6 +125,16 @@ def table(model_name: str, fields: Iterable[str], *,
Model = get_model_class(model_name)
qkwargs = dict(text=(q or None), limit=int(limit), offset=int(offset),
sort=(sort or None), direction=(direction or "asc").lower())
+ extra_opts = _eager_from_fields(Model, fields)
+ if extra_opts:
+ if isinstance(rows_any, Select):
+ rows_any = rows_any.options(*extra_opts)
+ elif rows_any is None:
+ original = getattr(Model, 'ui_eagerload', ())
+ def dyn_opts():
+ base = original() if callable(original) else original
+ return tuple(base) + tuple(extra_opts)
+ setattr(Model, 'ui_eagerload', dyn_opts)
rows_any: Any = call(Model, "ui_query", db.session, **qkwargs)
if rows_any is None:
diff --git a/inventory/static/js/label.js b/inventory/static/js/label.js
new file mode 100644
index 0000000..be3649d
--- /dev/null
+++ b/inventory/static/js/label.js
@@ -0,0 +1,29 @@
+function Label(cfg) {
+ return {
+ id: cfg.id,
+ refreshUrl: cfg.refreshUrl,
+ fieldName: cfg.fieldName,
+ recordId: cfg.recordId,
+
+ init() {
+ if (this.refreshUrl) this.refresh();
+ },
+
+ buildRefreshUrl() {
+ if (!this.refreshUrl) return null;
+ const u = new URL(this.refreshUrl, window.location.origin);
+ u.search = new URLSearchParams({ field: this.fieldName, id: this.recordId }).toString();
+ return u.toString();
+ },
+
+ async refresh() {
+ const url = this.buildRefreshUrl();
+ if (!url) return;
+ const res = await fetch(url, { headers: { 'HX-Request': 'true' } });
+ const text = await res.text();
+ if (this.$refs.label) {
+ this.$refs.label.innerHTML = text;
+ }
+ }
+ };
+}
\ No newline at end of file
diff --git a/inventory/templates/fragments/_label_fragment.html b/inventory/templates/fragments/_label_fragment.html
new file mode 100644
index 0000000..699969e
--- /dev/null
+++ b/inventory/templates/fragments/_label_fragment.html
@@ -0,0 +1,8 @@
+{% macro render_label(id, text=none, refresh_url=none, field_name=none, record_id=none) %}
+{{ text if text else '' }}
+{% endmacro %}
\ No newline at end of file
diff --git a/inventory/templates/layout.html b/inventory/templates/layout.html
index 67feb6b..16f01d7 100644
--- a/inventory/templates/layout.html
+++ b/inventory/templates/layout.html
@@ -5,6 +5,7 @@
{% import "fragments/_editor_fragment.html" as editor %}
{% import "fragments/_icon_fragment.html" as icons %}
{% import "fragments/_image_fragment.html" as images %}
+{% import "fragments/_label_fragment.html" as labels %}
{% import "fragments/_link_fragment.html" as links %}
{% import "fragments/_table_fragment.html" as tables %}
{% import "fragments/_toolbar_fragment.html" as toolbars %}
@@ -70,6 +71,7 @@
+