Ask before deleting, and put the row actions on icons
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.
This commit is contained in:
@@ -72,6 +72,10 @@ type logView struct {
|
|||||||
Sides []Side
|
Sides []Side
|
||||||
New mainForm // inline "add the dish you were looking for"
|
New mainForm // inline "add the dish you were looking for"
|
||||||
History []HistoryRow
|
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) {
|
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{},
|
Checked: map[int64]bool{},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
v.Confirming = r.URL.Query().Get("poista") != ""
|
||||||
|
|
||||||
entry, err := entryFor(a.db, date)
|
entry, err := entryFor(a.db, date)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("entry for %s: %v", date.Format(dateLayout), err)
|
log.Printf("entry for %s: %v", date.Format(dateLayout), err)
|
||||||
@@ -270,6 +276,11 @@ type catalogView struct {
|
|||||||
Side sideForm
|
Side sideForm
|
||||||
Report *ImportReport
|
Report *ImportReport
|
||||||
Mains int // count, for the header
|
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) {
|
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)
|
a.renderCatalog(w, r, v)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -427,12 +427,14 @@ html[data-theme="light"] .themetoggle .i-moon { display: none; }
|
|||||||
.rowtext { flex: 1; min-width: 0; }
|
.rowtext { flex: 1; min-width: 0; }
|
||||||
.rowtext .nm { font-size: 16px; font-weight: 600; letter-spacing: -0.02em; }
|
.rowtext .nm { font-size: 16px; font-weight: 600; letter-spacing: -0.02em; }
|
||||||
.rowtext .sd { font-size: 12.5px; color: var(--muted); }
|
.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 {
|
.rowactions a, .rowactions button {
|
||||||
|
min-width: 40px;
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
padding: 0 10px;
|
padding: 0 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
background: none;
|
background: none;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -442,9 +444,19 @@ html[data-theme="light"] .themetoggle .i-moon { display: none; }
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.rowactions svg { display: block; }
|
||||||
.rowactions a:hover { color: var(--accent); }
|
.rowactions a:hover { color: var(--accent); }
|
||||||
.rowactions .del { color: var(--liha); }
|
.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"] {
|
.field input[type="text"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: var(--tap);
|
min-height: var(--tap);
|
||||||
|
|||||||
+55
-11
@@ -360,20 +360,30 @@ templ loggedCard(v logView) {
|
|||||||
{ v.Entry.Main.Name }
|
{ v.Entry.Main.Name }
|
||||||
</p>
|
</p>
|
||||||
<p class="sd">{ v.Entry.SidesLabel() }</p>
|
<p class="sd">{ v.Entry.SidesLabel() }</p>
|
||||||
|
if v.Confirming {
|
||||||
|
<p class="q">Poistetaanko merkintä?</p>
|
||||||
|
<div class="pair">
|
||||||
|
<form method="post" action="/poista">
|
||||||
|
<input type="hidden" name="pvm" value={ isoDate(v.Date) }/>
|
||||||
|
<button class="btn del" type="submit">Kyllä, poista</button>
|
||||||
|
</form>
|
||||||
|
<a class="btn" href={ templ.SafeURL(dayURL("/", v.Date, v.Today)) }>Peruuta</a>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
<div class="pair">
|
<div class="pair">
|
||||||
<a
|
<a
|
||||||
class="btn"
|
class="btn"
|
||||||
href={ templ.SafeURL(dayURL("/", v.Date, v.Today) + pickSeparator(v) + "muuta=1") }
|
href={ templ.SafeURL(dayURL("/", v.Date, v.Today) + pickSeparator(v) + "muuta=1") }
|
||||||
>Muokkaa</a>
|
>Muokkaa</a>
|
||||||
<form method="post" action="/poista">
|
<a
|
||||||
<input type="hidden" name="pvm" value={ isoDate(v.Date) }/>
|
class="btn del"
|
||||||
<button class="btn del" type="submit">Poista</button>
|
href={ templ.SafeURL(dayURL("/", v.Date, v.Today) + pickSeparator(v) + "poista=1") }
|
||||||
</form>
|
>Poista</a>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------- Historia
|
|
||||||
// ---------------------------------------------------------------- Ruuat
|
// ---------------------------------------------------------------- Ruuat
|
||||||
templ catalogPage(v catalogView) {
|
templ catalogPage(v catalogView) {
|
||||||
@page("Ruuat — Foodster", "/ruuat") {
|
@page("Ruuat — Foodster", "/ruuat") {
|
||||||
@@ -405,7 +415,7 @@ templ catalogPage(v catalogView) {
|
|||||||
<div class="sd">Ei lisukkeita</div>
|
<div class="sd">Ei lisukkeita</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@rowActions("/ruuat?muokkaa="+strconv.FormatInt(d.ID, 10), d.ID, "paa")
|
@rowActions(v, "/ruuat?muokkaa="+strconv.FormatInt(d.ID, 10), d.ID, "paa")
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -417,7 +427,7 @@ templ catalogPage(v catalogView) {
|
|||||||
for _, s := range v.Sides {
|
for _, s := range v.Sides {
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="rowtext"><div class="nm">{ s.Name }</div></div>
|
<div class="rowtext"><div class="nm">{ s.Name }</div></div>
|
||||||
@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")
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<details class="card">
|
<details class="card">
|
||||||
@@ -428,15 +438,49 @@ templ catalogPage(v catalogView) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
templ rowActions(editURL string, id int64, kind string) {
|
// rowActions is a pencil and a bin, until the bin is tapped: then the row
|
||||||
<div class="rowactions">
|
// asks. An icon is a smaller target to hit by accident than a word, and the
|
||||||
<a href={ templ.SafeURL(editURL) } aria-label="Muokkaa">Muokkaa</a>
|
// 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 {
|
||||||
|
<div class="rowactions confirming">
|
||||||
|
<span>Poista?</span>
|
||||||
<form method="post" action="/ruuat/poista">
|
<form method="post" action="/ruuat/poista">
|
||||||
<input type="hidden" name="id" value={ strconv.FormatInt(id, 10) }/>
|
<input type="hidden" name="id" value={ strconv.FormatInt(id, 10) }/>
|
||||||
<input type="hidden" name="tyyppi" value={ kind }/>
|
<input type="hidden" name="tyyppi" value={ kind }/>
|
||||||
<button type="submit" class="del" aria-label="Poista">Poista</button>
|
<button type="submit" class="del">Kyllä</button>
|
||||||
</form>
|
</form>
|
||||||
|
<a href="/ruuat">Peruuta</a>
|
||||||
</div>
|
</div>
|
||||||
|
} else {
|
||||||
|
<div class="rowactions">
|
||||||
|
<a href={ templ.SafeURL(editURL) } aria-label="Muokkaa" title="Muokkaa">
|
||||||
|
@iconPencil()
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="del"
|
||||||
|
href={ templ.SafeURL("/ruuat?poista=" + strconv.FormatInt(id, 10) + "&tyyppi=" + kind) }
|
||||||
|
aria-label="Poista"
|
||||||
|
title="Poista"
|
||||||
|
>
|
||||||
|
@iconTrash()
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ iconPencil() {
|
||||||
|
<svg viewBox="0 0 16 16" width="18" height="18" aria-hidden="true" focusable="false" fill="currentColor">
|
||||||
|
<path d="M11.1 1.6a1.7 1.7 0 0 1 2.4 0l0.9 0.9a1.7 1.7 0 0 1 0 2.4l-0.8 0.8-3.3-3.3z"></path>
|
||||||
|
<path d="M9.4 3.3l3.3 3.3-6.6 6.6-4 0.7 0.7-4z"></path>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ iconTrash() {
|
||||||
|
<svg viewBox="0 0 16 16" width="18" height="18" aria-hidden="true" focusable="false" fill="currentColor">
|
||||||
|
<path d="M6.2 1.3h3.6a1 1 0 0 1 1 1v0.6h3.1v1.7H2.1V2.9h3.1v-.6a1 1 0 0 1 1-1z"></path>
|
||||||
|
<path d="M3.3 6.2h9.4l-0.7 7.3a1.5 1.5 0 0 1-1.5 1.3H5.5a1.5 1.5 0 0 1-1.5-1.3z"></path>
|
||||||
|
</svg>
|
||||||
}
|
}
|
||||||
|
|
||||||
templ mainForm_(f mainForm) {
|
templ mainForm_(f mainForm) {
|
||||||
|
|||||||
+34
-2
@@ -43,6 +43,16 @@ check() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# refute <name> <haystack> <needle>
|
||||||
|
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"
|
echo "smoke: http://$addr"
|
||||||
|
|
||||||
check "unauthenticated request is refused" \
|
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" \
|
check "history is on the same page as the logger" \
|
||||||
"$(curl -s -u ":$pass" "http://$addr/")" "Aiemmin"
|
"$(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" \
|
check "deleting redirects back" \
|
||||||
"$(curl -s -o /dev/null -w '%{http_code}' -u ":$pass" \
|
"$(curl -s -o /dev/null -w '%{http_code}' -u ":$pass" \
|
||||||
-d "pvm=2026-09-05" "http://$addr/poista")" "303"
|
-d "pvm=2026-09-05" "http://$addr/poista")" "303"
|
||||||
@@ -196,14 +211,31 @@ check "adding a side redirects" \
|
|||||||
check "the new side is listed" \
|
check "the new side is listed" \
|
||||||
"$(curl -s -u ":$pass" "http://$addr/ruuat")" "Lohkoperunat"
|
"$(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" \
|
check "the edit form is prefilled" \
|
||||||
"$(curl -s -u ":$pass" "http://$addr/ruuat?muokkaa=$uusi")" "Muokkaa pääruokaa"
|
"$(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" \
|
"$(curl -s -o /dev/null -w '%{http_code}' -u ":$pass" \
|
||||||
-d "id=$uusi&tyyppi=paa" "http://$addr/ruuat/poista")" "303"
|
-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
|
if [ "$fail" -ne 0 ]; then
|
||||||
echo "smoke: FAILED"
|
echo "smoke: FAILED"
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
Reference in New Issue
Block a user