From b67c4edd5ae1c241a1cfca961e44297bbb5dd6a4 Mon Sep 17 00:00:00 2001 From: Esa Kataja Date: Sat, 5 Sep 2026 21:34:18 +0300 Subject: [PATCH] Ask before deleting, and put the row actions on icons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Muokkaa and Poista become a pencil and a bin, which stops the catalog rows being two words wide. Both carry a Finnish aria-label and title, so nothing is lost by dropping the text. An icon is easier to hit by accident than a word, so neither delete happens immediately now. A tapped bin turns that row's actions into "Poista? Kyllä / Peruuta", and a logged meal asks "Poistetaanko merkintä?" before it goes. The meal is the more destructive of the two: a dish is only soft-deleted and its name still resolves in old entries, while the log row is dropped outright. Both confirmations are plain links and forms, so they work with the back button and need no client code. Also fixes a test that was passing for the wrong reason. The delete check only asserted a 303 and took the first dish id on the page, which stopped being the one it had just created when the catalog became grouped and alphabetical — so it was deleting an unrelated dish. It now finds that dish's own id, and a new refute helper asserts the dish is actually gone afterwards. --- cmd/foodster/handlers.go | 17 +++++++ cmd/foodster/static/app.css | 16 ++++++- cmd/foodster/views.templ | 88 +++++++++++++++++++++++++++---------- scripts/smoke.sh | 36 ++++++++++++++- 4 files changed, 131 insertions(+), 26 deletions(-) diff --git a/cmd/foodster/handlers.go b/cmd/foodster/handlers.go index 767b974..2d49eca 100644 --- a/cmd/foodster/handlers.go +++ b/cmd/foodster/handlers.go @@ -72,6 +72,10 @@ type logView struct { Sides []Side New mainForm // inline "add the dish you were looking for" History []HistoryRow + + // Deleting a logged meal drops the row outright, unlike a dish which is + // only soft-deleted, so it asks first. + Confirming bool } func (a *app) index(w http.ResponseWriter, r *http.Request) { @@ -83,6 +87,8 @@ func (a *app) index(w http.ResponseWriter, r *http.Request) { Checked: map[int64]bool{}, } + v.Confirming = r.URL.Query().Get("poista") != "" + entry, err := entryFor(a.db, date) if err != nil { log.Printf("entry for %s: %v", date.Format(dateLayout), err) @@ -270,6 +276,11 @@ type catalogView struct { Side sideForm Report *ImportReport Mains int // count, for the header + + // The row awaiting a delete confirmation, if any. A trash icon is easy to + // hit by accident, so the row asks before anything happens. + DeleteID int64 + DeleteKind string } func (a *app) catalog(w http.ResponseWriter, r *http.Request) { @@ -300,6 +311,12 @@ func (a *app) catalog(w http.ResponseWriter, r *http.Request) { } } } + if raw := r.URL.Query().Get("poista"); raw != "" { + if id, err := strconv.ParseInt(raw, 10, 64); err == nil { + v.DeleteID = id + v.DeleteKind = r.URL.Query().Get("tyyppi") + } + } a.renderCatalog(w, r, v) } diff --git a/cmd/foodster/static/app.css b/cmd/foodster/static/app.css index f563b63..794a866 100644 --- a/cmd/foodster/static/app.css +++ b/cmd/foodster/static/app.css @@ -427,12 +427,14 @@ html[data-theme="light"] .themetoggle .i-moon { display: none; } .rowtext { flex: 1; min-width: 0; } .rowtext .nm { font-size: 16px; font-weight: 600; letter-spacing: -0.02em; } .rowtext .sd { font-size: 12.5px; color: var(--muted); } -.rowactions { display: flex; align-items: center; gap: 4px; flex: none; } +.rowactions { display: flex; align-items: center; gap: 2px; flex: none; } .rowactions a, .rowactions button { + min-width: 40px; min-height: 40px; - padding: 0 10px; + padding: 0 8px; display: flex; align-items: center; + justify-content: center; background: none; border: 0; border-radius: 8px; @@ -442,9 +444,19 @@ html[data-theme="light"] .themetoggle .i-moon { display: none; } text-decoration: none; cursor: pointer; } +.rowactions svg { display: block; } .rowactions a:hover { color: var(--accent); } .rowactions .del { color: var(--liha); } +/* The row asks before a delete happens; an icon is easy to hit by accident. */ +.rowactions.confirming { + gap: 4px; + font-size: 13px; + color: var(--muted); +} +.rowactions.confirming > span { padding-right: 2px; } +.rowactions.confirming .del { color: var(--liha); font-weight: 700; } + .field input[type="text"] { width: 100%; min-height: var(--tap); diff --git a/cmd/foodster/views.templ b/cmd/foodster/views.templ index 7ec0db5..5944fde 100644 --- a/cmd/foodster/views.templ +++ b/cmd/foodster/views.templ @@ -360,20 +360,30 @@ templ loggedCard(v logView) { { v.Entry.Main.Name }

{ v.Entry.SidesLabel() }

-
- Muokkaa -
- - -
-
+ if v.Confirming { +

Poistetaanko merkintä?

+
+
+ + +
+ Peruuta +
+ } else { +
+ Muokkaa + Poista +
+ } } -// ---------------------------------------------------------------- Historia // ---------------------------------------------------------------- Ruuat templ catalogPage(v catalogView) { @page("Ruuat — Foodster", "/ruuat") { @@ -405,7 +415,7 @@ templ catalogPage(v catalogView) {
Ei lisukkeita
} - @rowActions("/ruuat?muokkaa="+strconv.FormatInt(d.ID, 10), d.ID, "paa") + @rowActions(v, "/ruuat?muokkaa="+strconv.FormatInt(d.ID, 10), d.ID, "paa") } } @@ -417,7 +427,7 @@ templ catalogPage(v catalogView) { for _, s := range v.Sides {
{ s.Name }
- @rowActions("/ruuat?muokkaa-lisuke="+strconv.FormatInt(s.ID, 10), s.ID, "lisuke") + @rowActions(v, "/ruuat?muokkaa-lisuke="+strconv.FormatInt(s.ID, 10), s.ID, "lisuke")
}
@@ -428,15 +438,49 @@ templ catalogPage(v catalogView) { } } -templ rowActions(editURL string, id int64, kind string) { -
- Muokkaa -
- - - -
-
+// rowActions is a pencil and a bin, until the bin is tapped: then the row +// asks. An icon is a smaller target to hit by accident than a word, and the +// dish disappears from every picker the moment it goes. +templ rowActions(v catalogView, editURL string, id int64, kind string) { + if v.DeleteID == id && v.DeleteKind == kind { +
+ Poista? +
+ + + +
+ Peruuta +
+ } else { + + } +} + +templ iconPencil() { + +} + +templ iconTrash() { + } templ mainForm_(f mainForm) { diff --git a/scripts/smoke.sh b/scripts/smoke.sh index 336c87e..721feef 100755 --- a/scripts/smoke.sh +++ b/scripts/smoke.sh @@ -43,6 +43,16 @@ check() { fi } +# refute +refute() { + if printf '%s' "$2" | grep -qF -- "$3"; then + echo " FAIL $1 (should not contain: $3)" + fail=1 + else + echo " ok $1" + fi +} + echo "smoke: http://$addr" check "unauthenticated request is refused" \ @@ -125,6 +135,11 @@ check "the saved day shows what was eaten" \ check "history is on the same page as the logger" \ "$(curl -s -u ":$pass" "http://$addr/")" "Aiemmin" +# Deleting a logged meal drops the row outright, so it asks first. +saved=$(curl -s -u ":$pass" "http://$addr/?pvm=2026-09-05&poista=1") +check "deleting a meal asks first" "$saved" "Poistetaanko merkintä?" +refute "and does not delete while asking" "$saved" "Ei merkintää" + check "deleting redirects back" \ "$(curl -s -o /dev/null -w '%{http_code}' -u ":$pass" \ -d "pvm=2026-09-05" "http://$addr/poista")" "303" @@ -196,14 +211,31 @@ check "adding a side redirects" \ check "the new side is listed" \ "$(curl -s -u ":$pass" "http://$addr/ruuat")" "Lohkoperunat" -uusi=$(printf '%s' "$catalog" | grep -o 'muokkaa=[0-9]*' | head -n1 | cut -d= -f2) +# The id of Uunikala specifically: the catalog is grouped and alphabetical, so +# the first id on the page belongs to some other dish entirely. +uusi=$(printf '%s' "$catalog" | grep -o 'Uunikala.*' | grep -o 'muokkaa=[0-9]*' | head -n1 | cut -d= -f2) +if [ -z "$uusi" ]; then + echo " FAIL could not find Uunikala's id in the catalog" + fail=1 + uusi=0 +fi check "the edit form is prefilled" \ "$(curl -s -u ":$pass" "http://$addr/ruuat?muokkaa=$uusi")" "Muokkaa pääruokaa" -check "deleting a main redirects" \ +# A bin icon is easy to hit by accident, so the row asks before anything goes. +check "the bin asks before deleting" \ + "$(curl -s -u ":$pass" "http://$addr/ruuat?poista=$uusi&tyyppi=paa")" "Poista?" + +check "the dish is still there while it asks" \ + "$(curl -s -u ":$pass" "http://$addr/ruuat?poista=$uusi&tyyppi=paa")" "Uunikala" + +check "confirming the delete redirects" \ "$(curl -s -o /dev/null -w '%{http_code}' -u ":$pass" \ -d "id=$uusi&tyyppi=paa" "http://$addr/ruuat/poista")" "303" +refute "the dish is gone once confirmed" \ + "$(curl -s -u ":$pass" "http://$addr/ruuat")" "Uunikala" + if [ "$fail" -ne 0 ]; then echo "smoke: FAILED" exit 1