Generic CRUD works now!
This commit is contained in:
parent
462b86c583
commit
e149a8d117
4 changed files with 97 additions and 22 deletions
|
|
@ -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,7 +211,8 @@ function ComboBox(cfg) {
|
|||
|
||||
_maybeRefresh() { if (this.refreshUrl) this.$dispatch('combobox:refresh'); },
|
||||
|
||||
async _post(url, payload) {
|
||||
async _post(url, payload, expectJson = false) {
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
|
@ -208,6 +220,17 @@ function ComboBox(cfg) {
|
|||
});
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
@ -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("/<model_name>/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("/<model_name>/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("/<model_name>/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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue