Addint submit and toast behavior.

This commit is contained in:
Yaro Kasear 2025-09-30 11:43:57 -05:00
parent fc4d3ebfe6
commit dbf0d6169a
6 changed files with 137 additions and 22 deletions

View file

@ -1189,5 +1189,6 @@ def render_form(
values=values_map,
render_field=render_field,
submit_attrs=submit_attrs,
submit_label=submit_label
submit_label=submit_label,
model_name=model_cls.__name__
)

View file

@ -1,4 +1,4 @@
<form method="POST">
<form method="POST" id="{{ model_name|lower }}_form">
{% macro render_row(row) %}
<!-- {{ row.name }} (row) -->
{% if row.fields or row.children or row.legend %}

View file

@ -96,7 +96,9 @@ def init_entry_routes(app):
fields["fields"] = ["id", "contact", "work_item", "start_time", "end_time", "complete"]
fields_spec = [
{"name": "id", "label": "", "type": "display", "label_spec": "Work Item #{id}",
"attrs": {"class": "display-6 mb-3"}, "row": "label"},
"attrs": {"class": "display-6 mb-3"}, "row": "label", "wrap": {"class": "col"}},
{"name": "submit", "label": "", "row": "label", "type": "template", "template": "submit_button.html",
"wrap": {"class": "col text-end me-2"}, "attrs": {"data-model": model}},
{"name": "contact", "row": "ownership", "wrap": {"class": "col"}, "label": "Contact",
"label_spec": "{label}", "attrs": {"class": "form-control"}, "label_attrs": {"class": "form-label"}},
{"name": "work_item", "row": "ownership", "wrap": {"class": "col"}, "label": "Work Item",
@ -111,7 +113,7 @@ def init_entry_routes(app):
"type": "template", "template": "update_list.html"},
]
layout = [
{"name": "label", "order": 0},
{"name": "label", "order": 0, "attrs": {"class": "row align-items-center"}},
{"name": "ownership", "order": 10, "attrs": {"class": "row mb-2"}},
{"name": "timestamps", "order": 20, "attrs": {"class": "row d-flex align-items-center"}},
{"name": "updates", "order": 30, "attrs": {"class": "row"}},
@ -132,7 +134,6 @@ def init_entry_routes(app):
updates_cls.is_deleted == False)
.order_by(updates_cls.timestamp.asc()))
all_updates = updates_q.all()
print(all_updates)
for f in fields_spec:
if f.get("name") == "updates" and f.get("type") == "template":
@ -141,8 +142,6 @@ def init_entry_routes(app):
f["template_ctx"] = ctx
break
print(fields_spec)
form = render_form(
cls,
obj.as_dict(),
@ -150,13 +149,34 @@ def init_entry_routes(app):
instance=obj,
fields_spec=fields_spec,
layout=layout,
submit_attrs={"class": "btn btn-primary mt-3"},
submit_attrs={"class": "d-none", "disabled": True},
)
# sanity log
u = getattr(obj, "updates", None)
print("WORKLOG UPDATES loaded? ",
"None" if u is None else f"len={len(list(u))} ids={[n.id for n in list(u)]}")
return render_template("entry.html", form=form)
@bp_entry.post("/entry/<model>/<int:id>")
def update_entry(model, id):
try:
if model not in ["inventory", "user", "worklog"]:
raise TypeError("Invalid model.")
payload = request.get_json()
cls = crudkit.crud.get_model(model)
if model == "inventory":
pass
elif model == "user":
pass
elif model == "worklog":
pass
else:
raise TypeError("Invalid model.")
service = crudkit.crud.get_service(cls)
item = service.get(id)
print(item.as_dict(), payload)
return {"status": "success", "payload": payload}
except Exception as e:
return {"status": "failure", "error": str(e)}
app.register_blueprint(bp_entry)

View file

@ -89,8 +89,6 @@ def inventory_spares():
)
rows = session.execute(stmt).all()
print(rows)
items = []
for dev, dep, avail in rows:
dep = int(dep or 0)

View file

@ -5,7 +5,8 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{{ title if title else "Inventory Manager" }}{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
<style>
{% block style %}
{% endblock %}
@ -22,13 +23,16 @@
</a>
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a href="{{ url_for('listing.show_list', model='inventory') }}" class="nav-link link-success fw-semibold">Inventory</a>
<a href="{{ url_for('listing.show_list', model='inventory') }}"
class="nav-link link-success fw-semibold">Inventory</a>
</li>
<li class="nav-item">
<a href="{{ url_for('listing.show_list', model='worklog') }}" class="nav-link link-success fw-semibold">Work Log</a>
<a href="{{ url_for('listing.show_list', model='worklog') }}"
class="nav-link link-success fw-semibold">Work Log</a>
</li>
<li class="nav-item">
<a href="{{ url_for('listing.show_list', model='user') }}" class="nav-link link-success fw-semibold">Users</a>
<a href="{{ url_for('listing.show_list', model='user') }}"
class="nav-link link-success fw-semibold">Users</a>
</li>
</ul>
{% block header %}
@ -52,18 +56,23 @@
{% block postmain %}
{% endblock %}
<div class="toast-container position-fixed bottom-0 end p-3" id="toastContainer"></div>
<footer class="bg-body-tertiary border border-bottom-0 position-fixed bottom-0 w-100 pb-1">
<small>
<span class="align-middle">&copy; 2025 Conrad Nelson •
<a href="/LICENSE" class="link-underline link-underline-opacity-0">AGPL-3.0-or-later</a>
<a href="https://git.kasear.net/yaro/inventory" class="link-underline link-underline-opacity-0">Source Code</a>
<a href="https://git.kasear.net/yaro/inventory" class="link-underline link-underline-opacity-0">Source
Code</a>
</span>
{% block footer %}
{% endblock %}
</small>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"
integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI"
crossorigin="anonymous"></script>
{% block scriptincludes %}
{% endblock %}
<script>
@ -85,6 +94,33 @@
location.href = `{{ url_for('search.search') }}?q=${searchText.value}`;
});
toastNumber = 0;
window.toastMessage = function (message, title, type = 'info') {
const container = document.getElementById('toastContainer');
const now = new Date();
const timestamp = now.toLocaleString('en-US', { dateStyle: 'medium', timeStyle: 'short' });
const id = `toast${window.toastNumber++}`;
const template = `
<div class="toast text-bg-${type}" id="${id}" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header">
<strong class="me-auto">${title}</strong>
<small class="text-body-secondary">${timestamp}</small>
<button type="button" class="btn-close ms-2 mb-1" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">${message}</div>
</div>
`;
container.insertAdjacentHTML('beforeend', template);
const el = document.getElementById(id);
const toast = new bootstrap.Toast(el, { autohide: true, delay: 4000 });
toast.show();
};
{% block script %}
{% endblock %}
})

View file

@ -0,0 +1,60 @@
<button type="submit" class="btn btn-primary" id="submit">Save</button>
<script>
function formToJson(form) {
const fd = new FormData(form);
const out = {};
fd.forEach((value, key) => {
if (key in out) {
if (!Array.isArray(out[key])) out[key] = [out[key]];
out[key].push(value);
} else {
out[key] = value;
}
});
form.querySelectorAll('input[type="checkbox"], input[type="radio"]').forEach(el => {
if (!el.name) return;
if (el.type === 'radio') {
if (out[el.name] !== undefined) return;
const checked = form.querySelector(`input[type="radio"][name="${CSS.escape(el.name)}"]:checked`);
if (checked) out[el.name] = checked.value ?? true;
}
if (el.type === 'checkbox') {
const group = form.querySelectorAll(`input[type="checkbox"][name="${CSS.escape(el.name)}"]`);
if (group.length > 1) {
const checkedVals = Array.from(group)
.filter(i => i.checked)
.map(i => i.value ?? true);
out[el.name] = checkedVals;
} else {
out[el.name] = el.checked;
}
}
});
return out;
}
document.getElementById("{{ field['attrs']['data-model'] }}_form").addEventListener("submit", async e => {
e.preventDefault();
const json = formToJson(e.target);
response = await fetch("{{ url_for('entry.update_entry', id=field['template_ctx']['values']['id'], model=field['attrs']['data-model']) }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(json)
});
reply = await response.json();
if (reply['status'] === 'success') {
console.log("WELL DONE!")
} else {
console.log("YOU HAVE FAILED!")
}
});
</script>