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
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
+66
-22
@@ -360,20 +360,30 @@ templ loggedCard(v logView) {
|
||||
{ v.Entry.Main.Name }
|
||||
</p>
|
||||
<p class="sd">{ v.Entry.SidesLabel() }</p>
|
||||
<div class="pair">
|
||||
<a
|
||||
class="btn"
|
||||
href={ templ.SafeURL(dayURL("/", v.Date, v.Today) + pickSeparator(v) + "muuta=1") }
|
||||
>Muokkaa</a>
|
||||
<form method="post" action="/poista">
|
||||
<input type="hidden" name="pvm" value={ isoDate(v.Date) }/>
|
||||
<button class="btn del" type="submit">Poista</button>
|
||||
</form>
|
||||
</div>
|
||||
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">
|
||||
<a
|
||||
class="btn"
|
||||
href={ templ.SafeURL(dayURL("/", v.Date, v.Today) + pickSeparator(v) + "muuta=1") }
|
||||
>Muokkaa</a>
|
||||
<a
|
||||
class="btn del"
|
||||
href={ templ.SafeURL(dayURL("/", v.Date, v.Today) + pickSeparator(v) + "poista=1") }
|
||||
>Poista</a>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Historia
|
||||
// ---------------------------------------------------------------- Ruuat
|
||||
templ catalogPage(v catalogView) {
|
||||
@page("Ruuat — Foodster", "/ruuat") {
|
||||
@@ -405,7 +415,7 @@ templ catalogPage(v catalogView) {
|
||||
<div class="sd">Ei lisukkeita</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>
|
||||
}
|
||||
}
|
||||
@@ -417,7 +427,7 @@ templ catalogPage(v catalogView) {
|
||||
for _, s := range v.Sides {
|
||||
<div class="row">
|
||||
<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>
|
||||
}
|
||||
<details class="card">
|
||||
@@ -428,15 +438,49 @@ templ catalogPage(v catalogView) {
|
||||
}
|
||||
}
|
||||
|
||||
templ rowActions(editURL string, id int64, kind string) {
|
||||
<div class="rowactions">
|
||||
<a href={ templ.SafeURL(editURL) } aria-label="Muokkaa">Muokkaa</a>
|
||||
<form method="post" action="/ruuat/poista">
|
||||
<input type="hidden" name="id" value={ strconv.FormatInt(id, 10) }/>
|
||||
<input type="hidden" name="tyyppi" value={ kind }/>
|
||||
<button type="submit" class="del" aria-label="Poista">Poista</button>
|
||||
</form>
|
||||
</div>
|
||||
// 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 {
|
||||
<div class="rowactions confirming">
|
||||
<span>Poista?</span>
|
||||
<form method="post" action="/ruuat/poista">
|
||||
<input type="hidden" name="id" value={ strconv.FormatInt(id, 10) }/>
|
||||
<input type="hidden" name="tyyppi" value={ kind }/>
|
||||
<button type="submit" class="del">Kyllä</button>
|
||||
</form>
|
||||
<a href="/ruuat">Peruuta</a>
|
||||
</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) {
|
||||
|
||||
Reference in New Issue
Block a user