Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions khata/web/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,12 @@ def save_trade_note(
if Q.trade_by_id(conn, user_id, trade_id) is None:
raise HTTPException(404)
note = Q.set_trade_note(conn, user_id, trade_id, body)
# Return only the saved-at stamp so HTMX doesn't re-render the editor
# DOM (which would cause EasyMDE to double-mount on every save).
return TEMPLATES.TemplateResponse(
request,
"partials/note_block.html",
{"note": note, "endpoint": f"/notes/trade/{trade_id}"},
"partials/note_meta.html",
{"note": note},
)

@app.post("/notes/day/{day}", response_class=HTMLResponse)
Expand All @@ -197,8 +199,8 @@ def save_daily_note(
note = Q.set_daily_note(conn, user_id, d, body)
return TEMPLATES.TemplateResponse(
request,
"partials/note_block.html",
{"note": note, "endpoint": f"/notes/day/{day}"},
"partials/note_meta.html",
{"note": note},
)

@app.post("/tags/trade/{trade_id}", response_class=HTMLResponse)
Expand Down
25 changes: 12 additions & 13 deletions khata/web/static/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,23 +60,22 @@
previewRender: (text) => easy.markdown(text),
});

// Keep the native textarea's value in sync so HTMX form submission works.
// Keep the native textarea's value in sync.
easy.codemirror.on("change", () => easy.codemirror.save());

// Keep a handle so form submit can flush before send.
window._khataEasyMDE = easy;

// HTMX replaces #note-block after POST → the old CodeMirror DOM gets
// unmounted. On htmx:afterSwap, re-wire whatever landed.
document.body.addEventListener("htmx:afterSwap", (ev) => {
if (ev.target && ev.target.id === "note-block") {
const t = document.querySelector("#note-block textarea.khata-editor");
if (t) {
delete t.dataset.khataMounted;
wire(t);
}
// Trigger an HTMX save when the CodeMirror area loses focus. The form's
// hx-trigger includes 'khata-save', so this submits without re-rendering
// the editor DOM (the server returns just the saved-at stamp).
const form = textarea.closest("form");
easy.codemirror.on("blur", () => {
easy.codemirror.save();
if (form && window.htmx) {
window.htmx.trigger(form, "khata-save");
}
});

// Keep a handle so any form-submit onsubmit hook can flush before send.
window._khataEasyMDE = easy;
}

function init() {
Expand Down
6 changes: 3 additions & 3 deletions khata/web/templates/partials/note_block.html
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<div class="note-block" id="note-block">
<form hx-post="{{ endpoint }}"
hx-target="#note-block"
hx-target="#note-meta-stamp"
hx-swap="outerHTML"
hx-trigger="submit, khata-save"
onsubmit="if (window._khataEasyMDE) window._khataEasyMDE.codemirror.save();">
<textarea name="body"
class="khata-editor"
data-upload="{{ upload_endpoint }}"
data-autosave="{{ endpoint }}"
placeholder="What happened? What did you learn? Paste or drop an image to embed it.">{{ note.body_md if note else '' }}</textarea>
<div class="note-meta">
<span class="subtle">{% if note and note.updated_at %}saved {{ note.updated_at[:19].replace('T', ' ') }} UTC{% else %}not saved yet{% endif %}</span>
{% include "partials/note_meta.html" %}
<button type="submit" class="btn btn-muted">save</button>
</div>
</form>
Expand Down
1 change: 1 addition & 0 deletions khata/web/templates/partials/note_meta.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<span id="note-meta-stamp" class="subtle">{% if note and note.updated_at %}saved {{ note.updated_at[:19].replace('T', ' ') }} UTC{% else %}not saved yet{% endif %}</span>
9 changes: 7 additions & 2 deletions tests/test_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,13 @@ def test_trade_404(client):


def test_note_save_and_reload(client):
# POST returns ONLY the saved-at stamp partial (not the full editor),
# so EasyMDE doesn't double-mount. The body itself is checked on reload.
r = client.post("/notes/trade/1", data={"body": "First thoughts on this trade"})
assert r.status_code == 200
assert "First thoughts" in r.text
assert 'id="note-meta-stamp"' in r.text
assert "saved" in r.text
assert "First thoughts" not in r.text # editor not re-rendered
# Reload the page and confirm note persisted
r2 = client.get("/trade/1")
assert "First thoughts" in r2.text
Expand Down Expand Up @@ -167,7 +171,8 @@ def test_tag_add_and_remove(client):
def test_daily_note_save(client):
r = client.post("/notes/day/2026-04-15", data={"body": "Revenge traded after the morning loss"})
assert r.status_code == 200
assert "Revenge traded" in r.text
assert 'id="note-meta-stamp"' in r.text
assert "Revenge traded" not in r.text # meta partial only

r2 = client.get("/day/2026-04-15")
assert "Revenge traded" in r2.text
Expand Down