Addint submit and toast behavior.
This commit is contained in:
parent
fc4d3ebfe6
commit
dbf0d6169a
6 changed files with 137 additions and 22 deletions
|
|
@ -1189,5 +1189,6 @@ def render_form(
|
||||||
values=values_map,
|
values=values_map,
|
||||||
render_field=render_field,
|
render_field=render_field,
|
||||||
submit_attrs=submit_attrs,
|
submit_attrs=submit_attrs,
|
||||||
submit_label=submit_label
|
submit_label=submit_label,
|
||||||
|
model_name=model_cls.__name__
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<form method="POST">
|
<form method="POST" id="{{ model_name|lower }}_form">
|
||||||
{% macro render_row(row) %}
|
{% macro render_row(row) %}
|
||||||
<!-- {{ row.name }} (row) -->
|
<!-- {{ row.name }} (row) -->
|
||||||
{% if row.fields or row.children or row.legend %}
|
{% if row.fields or row.children or row.legend %}
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,9 @@ def init_entry_routes(app):
|
||||||
fields["fields"] = ["id", "contact", "work_item", "start_time", "end_time", "complete"]
|
fields["fields"] = ["id", "contact", "work_item", "start_time", "end_time", "complete"]
|
||||||
fields_spec = [
|
fields_spec = [
|
||||||
{"name": "id", "label": "", "type": "display", "label_spec": "Work Item #{id}",
|
{"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",
|
{"name": "contact", "row": "ownership", "wrap": {"class": "col"}, "label": "Contact",
|
||||||
"label_spec": "{label}", "attrs": {"class": "form-control"}, "label_attrs": {"class": "form-label"}},
|
"label_spec": "{label}", "attrs": {"class": "form-control"}, "label_attrs": {"class": "form-label"}},
|
||||||
{"name": "work_item", "row": "ownership", "wrap": {"class": "col"}, "label": "Work Item",
|
{"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"},
|
"type": "template", "template": "update_list.html"},
|
||||||
]
|
]
|
||||||
layout = [
|
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": "ownership", "order": 10, "attrs": {"class": "row mb-2"}},
|
||||||
{"name": "timestamps", "order": 20, "attrs": {"class": "row d-flex align-items-center"}},
|
{"name": "timestamps", "order": 20, "attrs": {"class": "row d-flex align-items-center"}},
|
||||||
{"name": "updates", "order": 30, "attrs": {"class": "row"}},
|
{"name": "updates", "order": 30, "attrs": {"class": "row"}},
|
||||||
|
|
@ -132,7 +134,6 @@ def init_entry_routes(app):
|
||||||
updates_cls.is_deleted == False)
|
updates_cls.is_deleted == False)
|
||||||
.order_by(updates_cls.timestamp.asc()))
|
.order_by(updates_cls.timestamp.asc()))
|
||||||
all_updates = updates_q.all()
|
all_updates = updates_q.all()
|
||||||
print(all_updates)
|
|
||||||
|
|
||||||
for f in fields_spec:
|
for f in fields_spec:
|
||||||
if f.get("name") == "updates" and f.get("type") == "template":
|
if f.get("name") == "updates" and f.get("type") == "template":
|
||||||
|
|
@ -141,8 +142,6 @@ def init_entry_routes(app):
|
||||||
f["template_ctx"] = ctx
|
f["template_ctx"] = ctx
|
||||||
break
|
break
|
||||||
|
|
||||||
print(fields_spec)
|
|
||||||
|
|
||||||
form = render_form(
|
form = render_form(
|
||||||
cls,
|
cls,
|
||||||
obj.as_dict(),
|
obj.as_dict(),
|
||||||
|
|
@ -150,13 +149,34 @@ def init_entry_routes(app):
|
||||||
instance=obj,
|
instance=obj,
|
||||||
fields_spec=fields_spec,
|
fields_spec=fields_spec,
|
||||||
layout=layout,
|
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)
|
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)
|
app.register_blueprint(bp_entry)
|
||||||
|
|
|
||||||
|
|
@ -89,8 +89,6 @@ def inventory_spares():
|
||||||
)
|
)
|
||||||
rows = session.execute(stmt).all()
|
rows = session.execute(stmt).all()
|
||||||
|
|
||||||
print(rows)
|
|
||||||
|
|
||||||
items = []
|
items = []
|
||||||
for dev, dep, avail in rows:
|
for dev, dep, avail in rows:
|
||||||
dep = int(dep or 0)
|
dep = int(dep or 0)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>{% block title %}{{ title if title else "Inventory Manager" }}{% endblock %}</title>
|
<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>
|
<style>
|
||||||
{% block style %}
|
{% block style %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
@ -22,13 +23,16 @@
|
||||||
</a>
|
</a>
|
||||||
<ul class="navbar-nav me-auto">
|
<ul class="navbar-nav me-auto">
|
||||||
<li class="nav-item">
|
<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>
|
||||||
<li class="nav-item">
|
<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>
|
||||||
<li class="nav-item">
|
<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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
{% block header %}
|
{% block header %}
|
||||||
|
|
@ -52,18 +56,23 @@
|
||||||
{% block postmain %}
|
{% block postmain %}
|
||||||
{% endblock %}
|
{% 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">
|
<footer class="bg-body-tertiary border border-bottom-0 position-fixed bottom-0 w-100 pb-1">
|
||||||
<small>
|
<small>
|
||||||
<span class="align-middle">© 2025 Conrad Nelson •
|
<span class="align-middle">© 2025 Conrad Nelson •
|
||||||
<a href="/LICENSE" class="link-underline link-underline-opacity-0">AGPL-3.0-or-later</a> •
|
<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>
|
</span>
|
||||||
{% block footer %}
|
{% block footer %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</small>
|
</small>
|
||||||
</footer>
|
</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 %}
|
{% block scriptincludes %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -77,14 +86,41 @@
|
||||||
|
|
||||||
searchText.addEventListener('keypress', (event) => {
|
searchText.addEventListener('keypress', (event) => {
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
location.href=`{{ url_for('search.search') }}?q=${searchText.value}`;
|
location.href = `{{ url_for('search.search') }}?q=${searchText.value}`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
searchButton.addEventListener('click', () => {
|
searchButton.addEventListener('click', () => {
|
||||||
location.href=`{{ url_for('search.search') }}?q=${searchText.value}`;
|
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 %}
|
{% block script %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
60
inventory/templates/submit_button.html
Normal file
60
inventory/templates/submit_button.html
Normal 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>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue