diff --git a/inventory/static/js/editor.js b/inventory/static/js/editor.js
index aa4002b..aec8204 100644
--- a/inventory/static/js/editor.js
+++ b/inventory/static/js/editor.js
@@ -9,9 +9,7 @@ function Editor(cfg) {
recordId: cfg.recordId,
init() {
- // initial render from whatever’s in the textarea
this.renderViewer();
- // then pull server value and re-render
if (this.refreshUrl) this.refresh();
},
@@ -35,7 +33,6 @@ function Editor(cfg) {
},
triggerRefresh() {
- // use this anywhere to force a re-pull
this.$refs.container?.dispatchEvent(new CustomEvent('editor:refresh', { bubbles: true }));
},
@@ -58,3 +55,21 @@ function Editor(cfg) {
}
};
}
+
+let tempIdCounter = 1;
+
+function createEditorWidget(template, id, timestamp, content = '') {
+ let html = template.innerHTML
+ .replace(/__ID__/g, id)
+ .replace(/__TIMESTAMP__/g, timestamp)
+ .replace(/__CONTENT__/g, content);
+
+ const wrapper = document.createElement("div");
+ wrapper.innerHTML = html;
+
+ return wrapper.firstElementChild;
+}
+
+function createTempId(prefix = "temp") {
+ return `${prefix}-${tempIdCounter++}`;
+}
diff --git a/inventory/templates/fragments/_list_fragment.html b/inventory/templates/fragments/_list_fragment.html
index 487d65e..c1add41 100644
--- a/inventory/templates/fragments/_list_fragment.html
+++ b/inventory/templates/fragments/_list_fragment.html
@@ -1,4 +1,4 @@
-{# templates/fragments/_list_fragment.html #}
+
{% for it in options %}
{% if rows %}
@@ -88,17 +88,6 @@
-
-
{% else %}
No data.
{% endif %}
diff --git a/inventory/ui/blueprint.py b/inventory/ui/blueprint.py
index 72a9234..9afe002 100644
--- a/inventory/ui/blueprint.py
+++ b/inventory/ui/blueprint.py
@@ -5,7 +5,7 @@ from sqlalchemy.sql import Select
from typing import Any, List, cast
from .defaults import (
- default_query, default_create, default_update, default_delete, default_serialize
+ default_query, default_create, default_update, default_delete, default_serialize, default_values, default_value
)
from .. import db
@@ -178,8 +178,6 @@ def get_value(model_name):
try:
val = call(Model, "ui_value", db.session, id_=id_, field=field)
if val is None:
- # None means “no override,” fall back to default
- from .defaults import default_value # or hoist to top if you like
val = default_value(db.session, Model, id_=id_, field=field)
except ValueError as e:
return jsonify({"error": str(e)}), 400
@@ -190,3 +188,25 @@ def get_value(model_name):
return (str(val) if val is not None else ""), 200, {"Content-Type": "text/plain; charset=utf-8"}
return jsonify({"id": id_, "field": field, "value": val})
+
+@bp.get("/values")
+def get_values(model_name):
+ Model = get_model_class(model_name)
+
+ raw = request.args.get("fields") or ""
+ parts = [p for p in raw.split(",") if p.strip()]
+ parts.extend(request.args.getlist("field"))
+
+ id_raw = request.args.get("id")
+ try:
+ id_ = int(id_raw)
+ except (TypeError, ValueError):
+ return jsonify({"error": "Invalid id"}), 422
+
+ try:
+ data = call(Model, "ui_values", db.session, id_=id_, fields=parts) \
+ or default_values(db.session, Model, id_=id_, fields=parts)
+ except ValueError as e:
+ return jsonify({"error": str(e)}), 400
+
+ return jsonify({"id": id_, "fields": parts, "values": data})
\ No newline at end of file
diff --git a/inventory/ui/defaults.py b/inventory/ui/defaults.py
index bce1dc2..0658ec6 100644
--- a/inventory/ui/defaults.py
+++ b/inventory/ui/defaults.py
@@ -1,5 +1,6 @@
from sqlalchemy import select, asc as sa_asc, desc as sa_desc
from sqlalchemy.inspection import inspect
+from sqlalchemy.orm import aliased
from sqlalchemy.sql import Select
from typing import Any, Optional, cast, Iterable
@@ -85,6 +86,64 @@ def default_query(
return list(session.execute(stmt).scalars().all())
+def _resolve_column(Model, path: str):
+ """Return (selectable, joins:list[tuple[parent, attr]]) for 'col' or 'rel.col'"""
+ if '.' not in path:
+ col = _mapped_column(Model, path)
+ if col is None:
+ raise ValueError(f"Column '{path}' is not a mapped column on {Model.__name__}")
+ return col, []
+ rel_name, rel_field = path.split('.', 1)
+ rel_attr = getattr(Model, rel_name, None)
+ if getattr(rel_attr, 'property', None) is None:
+ raise ValueError(f"Column '{path}' is not a valid relationship on {Model.__name__}")
+ Rel = rel_attr.property.mapper.class_
+ col = _mapped_column(Rel, rel_field)
+ if col is None:
+ raise ValueError(f"Column '{path}' is not a mapped column on {Rel.__name__}")
+ return col, [(Model, rel_name)]
+
+def default_values(session, Model, *, id_: int, fields: Iterable[str]) -> dict[str, Any]:
+ fields = [f.strip() for f in fields if f.strip()]
+ if not fields:
+ raise ValueError("No fields provided for default_values")
+
+ mapper = inspect(Model)
+ pk = mapper.primary_key[0]
+
+ selects = []
+ joins = []
+ for f in fields:
+ col, j = _resolve_column(Model, f)
+ selects.append(col.label(f.replace('.', '_')))
+ joins.extend(j)
+
+ seen = set()
+ stmt = select(*selects).where(pk == id_)
+ current = Model
+ for parent, attr_name in joins:
+ key = (parent, attr_name)
+ if key in seen:
+ continue
+ seen.add(key)
+ stmt = stmt.join(getattr(parent, attr_name))
+
+ row = session.execute(stmt).one_or_none()
+ if row is None:
+ return {}
+
+ allow = getattr(Model, "ui_value_allow", None)
+ if allow:
+ for f in fields:
+ if f not in allow:
+ raise ValueError(f"Field '{f}' not allowed")
+
+ data = {}
+ for f in fields:
+ key = f.replace('.', '_')
+ data[f] = getattr(row, key, None)
+ return data
+
def default_value(session, Model, *, id_: int, field: str) -> Any:
if '.' not in field:
col = _mapped_column(Model, field)