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)