From e149a8d1175548e67b1c0955afac3bb3291431eb Mon Sep 17 00:00:00 2001 From: Yaro Kasear Date: Wed, 13 Aug 2025 13:48:11 -0500 Subject: [PATCH] Generic CRUD works now! --- inventory/static/js/combobox.js | 63 +++++++++++++------ .../fragments/_combobox_fragment.html | 2 +- inventory/templates/playground.html | 5 +- inventory/ui/blueprint.py | 49 +++++++++++++++ 4 files changed, 97 insertions(+), 22 deletions(-) diff --git a/inventory/static/js/combobox.js b/inventory/static/js/combobox.js index f825b8b..e8bb099 100644 --- a/inventory/static/js/combobox.js +++ b/inventory/static/js/combobox.js @@ -140,10 +140,16 @@ function ComboBox(cfg) { isEditing: false, editingOption: null, - get hasSelection() { return this.$refs.list?.selectedOptions.length > 0 }, + // NEW: keep selection reactive + selectedIds: [], + + // Button disable uses reactive data now + get hasSelection() { return this.selectedIds.length > 0 }, onListChange() { - const sel = this.$refs.list.selectedOptions; + const sel = Array.from(this.$refs.list.selectedOptions); + this.selectedIds = sel.map(o => o.value); // <-- reactive update + if (sel.length === 1) { this.query = sel[0].textContent.trim(); this.isEditing = true; @@ -160,13 +166,11 @@ function ComboBox(cfg) { if (!name) return; if (this.isEditing && this.editingOption && this.editUrl) { - // EDIT const id = this.editingOption.value; - await this._post(this.editUrl, { id, name }); - this.editingOption.textContent = name; + const ok = await this._post(this.editUrl, { id, name }); + if (ok) this.editingOption.textContent = name; } else if (this.createUrl) { - // CREATE - const data = await this._post(this.createUrl, { name }); + const data = await this._post(this.createUrl, { name }, true); const id = (data && data.id) ? data.id : ('temp-' + Math.random().toString(36).slice(2)); const opt = document.createElement('option'); opt.value = id; opt.textContent = name; @@ -180,13 +184,20 @@ function ComboBox(cfg) { }, async removeSelected() { - const opts = Array.from(this.$refs.list.selectedOptions); - if (!opts.length) return; + const ids = [...this.selectedIds]; // <-- capture IDs before DOM changes + if (!ids.length) return; + if (!confirm(`Delete ${ids.length} item(s)?`)) return; - if (!confirm(`Delete ${opts.length} item(s)?`)) return; + let ok = true; + if (this.deleteUrl) ok = !!(await this._post(this.deleteUrl, { ids })); + if (!ok) return; - const ids = opts.map(o => o.remove()); + // Remove matching options from DOM + const all = Array.from(this.$refs.list.options); + all.forEach(o => { if (ids.includes(o.value)) o.remove(); }); + // Clear selection reactively + this.selectedIds = []; this.query = ''; this.cancelEdit(); this._maybeRefresh(); @@ -200,15 +211,27 @@ function ComboBox(cfg) { _maybeRefresh() { if (this.refreshUrl) this.$dispatch('combobox:refresh'); }, - async _post(url, payload) { - const res = await fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }); - if (!res.ok) { - const msg = await res.text().catch(() => 'Error'); + async _post(url, payload, expectJson = false) { + try { + const res = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + if (!res.ok) { + const msg = await res.text().catch(() => 'Error'); + alert(msg); + return false; + } + if (expectJson) { + const ct = res.headers.get('content-type') || ''; + if (ct.includes('application/json')) return await res.json(); + } + return true; + } catch (e) { + alert('Network error'); + return false; } } } -} \ No newline at end of file +} diff --git a/inventory/templates/fragments/_combobox_fragment.html b/inventory/templates/fragments/_combobox_fragment.html index 5c2e7db..4b367bc 100644 --- a/inventory/templates/fragments/_combobox_fragment.html +++ b/inventory/templates/fragments/_combobox_fragment.html @@ -49,7 +49,7 @@ create_url = none, edit_url = none, delete_url = none, refresh_url = none
diff --git a/inventory/templates/playground.html b/inventory/templates/playground.html index d0b8764..3b02008 100644 --- a/inventory/templates/playground.html +++ b/inventory/templates/playground.html @@ -4,6 +4,9 @@ {{ combos.dynamic_combobox( id='play', label='Breakfast!', - refresh_url=url_for('ui.list_items', model_name='brand') + create_url=url_for('ui.create_item', model_name='brand'), + edit_url=url_for('ui.update_item', model_name='brand'), + refresh_url=url_for('ui.list_items', model_name='brand'), + delete_url=url_for('ui.delete_item', model_name='brand') ) }} {% endblock %} \ No newline at end of file diff --git a/inventory/ui/blueprint.py b/inventory/ui/blueprint.py index 51781c0..8ea5c26 100644 --- a/inventory/ui/blueprint.py +++ b/inventory/ui/blueprint.py @@ -57,3 +57,52 @@ def list_items(model_name): if want_html: return render_template("fragments/_option_fragment.html", options=items) return jsonify({"items": items}) + +@bp.post("//create") +def create_item(model_name): + Model = get_model_class(model_name) + payload = request.get_json(silent=True) or {} + if not payload: + return jsonify({"error": "Payload required"}), 422 + try: + obj = call(Model, 'ui_create', db.session, payload=payload) \ + or default_create(db.session, Model, payload) + except IntegrityError: + db.session.rollback() + return jsonify({"error": "Duplicate"}), 409 + data = call(Model, 'ui_serialize', obj) or default_serialize(Model, obj) + want_html = (request.args.get('view') == 'option') or ("HX-Request" in request.headers) + if want_html: + return "Yo." + return jsonify(data), 201 + +@bp.post("//update") +def update_item(model_name): + Model = get_model_class(model_name) + payload = request.get_json(silent=True) or {} + try: + id_ = int(payload.get("id")) + except Exception: + return jsonify({"error": "Invalid id"}), 422 + obj = call(Model, 'ui_update', db.session, id_=id_, payload=payload) \ + or default_update(db.session, Model, id_, payload) + if not obj: + return jsonify({"error": "Note found"}), 404 + return ("", 204) + +@bp.post("//delete") +def delete_item(model_name): + Model = get_model_class(model_name) + payload = request.get_json(silent=True) or {} + ids = payload.get("ids") or [] + try: + ids = [int(x) for x in ids] + except Exception: + return jsonify({"error": "Invalid ids"}), 422 + try: + deleted = call(Model, 'ui_delete', db.session, ids=ids) \ + or default_delete(db.session, Model, ids) + except IntegrityError as e: + db.session.rollback() + return jsonify({"error": "Constraint", "detail": str(e)}), 409 + return jsonify({"deleted": deleted}), 200