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,
|
isEditing: false,
|
||||||
editingOption: null,
|
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() {
|
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) {
|
if (sel.length === 1) {
|
||||||
this.query = sel[0].textContent.trim();
|
this.query = sel[0].textContent.trim();
|
||||||
this.isEditing = true;
|
this.isEditing = true;
|
||||||
|
|
@ -160,13 +166,11 @@ function ComboBox(cfg) {
|
||||||
if (!name) return;
|
if (!name) return;
|
||||||
|
|
||||||
if (this.isEditing && this.editingOption && this.editUrl) {
|
if (this.isEditing && this.editingOption && this.editUrl) {
|
||||||
// EDIT
|
|
||||||
const id = this.editingOption.value;
|
const id = this.editingOption.value;
|
||||||
await this._post(this.editUrl, { id, name });
|
const ok = await this._post(this.editUrl, { id, name });
|
||||||
this.editingOption.textContent = name;
|
if (ok) this.editingOption.textContent = name;
|
||||||
} else if (this.createUrl) {
|
} else if (this.createUrl) {
|
||||||
// CREATE
|
const data = await this._post(this.createUrl, { name }, true);
|
||||||
const data = await this._post(this.createUrl, { name });
|
|
||||||
const id = (data && data.id) ? data.id : ('temp-' + Math.random().toString(36).slice(2));
|
const id = (data && data.id) ? data.id : ('temp-' + Math.random().toString(36).slice(2));
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = id; opt.textContent = name;
|
opt.value = id; opt.textContent = name;
|
||||||
|
|
@ -180,13 +184,20 @@ function ComboBox(cfg) {
|
||||||
},
|
},
|
||||||
|
|
||||||
async removeSelected() {
|
async removeSelected() {
|
||||||
const opts = Array.from(this.$refs.list.selectedOptions);
|
const ids = [...this.selectedIds]; // <-- capture IDs before DOM changes
|
||||||
if (!opts.length) return;
|
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.query = '';
|
||||||
this.cancelEdit();
|
this.cancelEdit();
|
||||||
this._maybeRefresh();
|
this._maybeRefresh();
|
||||||
|
|
@ -200,7 +211,8 @@ function ComboBox(cfg) {
|
||||||
|
|
||||||
_maybeRefresh() { if (this.refreshUrl) this.$dispatch('combobox:refresh'); },
|
_maybeRefresh() { if (this.refreshUrl) this.$dispatch('combobox:refresh'); },
|
||||||
|
|
||||||
async _post(url, payload) {
|
async _post(url, payload, expectJson = false) {
|
||||||
|
try {
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
|
@ -208,6 +220,17 @@ function ComboBox(cfg) {
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const msg = await res.text().catch(() => 'Error');
|
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(
|
{{ combos.dynamic_combobox(
|
||||||
id='play',
|
id='play',
|
||||||
label='Breakfast!',
|
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 %}
|
{% endblock %}
|
||||||
|
|
@ -57,3 +57,52 @@ def list_items(model_name):
|
||||||
if want_html:
|
if want_html:
|
||||||
return render_template("fragments/_option_fragment.html", options=items)
|
return render_template("fragments/_option_fragment.html", options=items)
|
||||||
return jsonify({"items": 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