From d0068577b37d013cb585c7e9693a4c14820fa623 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 15 Aug 2022 22:29:06 -0400 Subject: [PATCH] Handle errors on htmx requests (#36) - Use grouping for sets of links (#38) --- src/PrayerTracker.UI/CommonFunctions.fs | 3 + src/PrayerTracker.UI/PrayerRequest.fs | 94 ++++++++++++------------- src/PrayerTracker.UI/SmallGroup.fs | 12 ++-- src/PrayerTracker/PrayerRequest.fs | 2 +- src/PrayerTracker/wwwroot/css/app.css | 2 +- src/PrayerTracker/wwwroot/js/app.js | 20 ++++++ 6 files changed, 77 insertions(+), 56 deletions(-) diff --git a/src/PrayerTracker.UI/CommonFunctions.fs b/src/PrayerTracker.UI/CommonFunctions.fs index 991c85f..dd192bc 100644 --- a/src/PrayerTracker.UI/CommonFunctions.fs +++ b/src/PrayerTracker.UI/CommonFunctions.fs @@ -150,6 +150,9 @@ let _inputField = _inputFieldWith [] /// The class that designates a checkbox / label pair let _checkboxField = _class "pt-checkbox-field" +/// A group of related fields, inputs, links, etc., displayed in a row +let _group = _class "pt-group" + /// Create an input field of the given type, with matching name and ID and the given value let inputField typ name value attrs = List.concat [ [ _type typ; _name name; _id name; if value <> "" then _value value ]; attrs ] |> input diff --git a/src/PrayerTracker.UI/PrayerRequest.fs b/src/PrayerTracker.UI/PrayerRequest.fs index 77bac90..4e2b9de 100644 --- a/src/PrayerTracker.UI/PrayerRequest.fs +++ b/src/PrayerTracker.UI/PrayerRequest.fs @@ -54,7 +54,7 @@ let edit (model : EditRequest) today ctx viewInfo = div [ _fieldRow ] [ div [ _inputField ] [ label [] [ locStr s["Expiration"] ] - span [ _class "pt-radio-group" ] [ + span [ _group ] [ for code, name in ReferenceList.expirationList s (not model.IsNew) do label [] [ radio (nameof model.Expiration) "" code model.Expiration; locStr name ] ] @@ -212,22 +212,22 @@ let maintain (model : MaintainRequests) (ctx : HttpContext) viewInfo = ] ]) |> List.ofSeq - [ div [ _class "pt-center-text" ] [ - br [] - a [ _href $"/prayer-request/{emptyGuid}/edit"; _title s["Add a New Request"].Value ] [ - icon "add_circle"; rawText "  "; locStr s["Add a New Request"] - ] - rawText "       " - a [ _href "/prayer-requests/view"; _title s["View Prayer Request List"].Value ] [ - icon "list"; rawText "  "; locStr s["View Prayer Request List"] - ] - match model.SearchTerm with - | Some _ -> - rawText "       " - a [ _href "/prayer-requests"; _title l["Clear Search Criteria"].Value ] [ - icon "highlight_off"; rawText "  "; raw l["Clear Search Criteria"] + [ br [] + div [ _fieldRow ] [ + span [ _group ] [ + a [ _href $"/prayer-request/{emptyGuid}/edit"; _title s["Add a New Request"].Value ] [ + icon "add_circle"; rawText "  "; locStr s["Add a New Request"] ] - | None -> () + a [ _href "/prayer-requests/view"; _title s["View Prayer Request List"].Value ] [ + icon "list"; rawText "  "; locStr s["View Prayer Request List"] + ] + match model.SearchTerm with + | Some _ -> + a [ _href "/prayer-requests"; _title l["Clear Search Criteria"].Value ] [ + icon "highlight_off"; rawText "  "; raw l["Clear Search Criteria"] + ] + | None -> () + ] ] form [ _action "/prayer-requests"; _method "get"; _class "pt-center-text pt-search-form"; Target.content ] [ inputField "text" "search" (defaultArg model.SearchTerm "") [ _placeholder l["Search requests..."].Value ] @@ -300,9 +300,9 @@ let print model version = br [] hr [] div [ _style $"font-size:70%%;font-family:{model.SmallGroup.Preferences.FontStack};" ] [ - img [ _src $"""/img/{s["footer_en"].Value}.png""" + img [ _src $"""/img/{s["footer_en"].Value}.png""" _style "vertical-align:text-bottom;" - _alt imgAlt + _alt imgAlt _title imgAlt ] space str version @@ -315,38 +315,36 @@ let print model version = let view model viewInfo = let s = I18N.localizer.Force () let pageTitle = $"""{s["Prayer Requests"].Value} • {model.SmallGroup.Name}""" - let spacer = rawText "       " let dtString = model.Date.ToString ("yyyy-MM-dd", CultureInfo.InvariantCulture) - [ div [ _class "pt-center-text" ] [ - br [] - a [ _class "pt-icon-link" - _href $"/prayer-requests/print/{dtString}" - _target "_blank" - _title s["View Printable"].Value ] [ - icon "print"; rawText "  "; locStr s["View Printable"] - ] - if model.CanEmail then - spacer - if model.Date.DayOfWeek <> IsoDayOfWeek.Sunday then - let rec findSunday (date : LocalDate) = - if date.DayOfWeek = IsoDayOfWeek.Sunday then date else findSunday (date.PlusDays 1) - let sunday = findSunday model.Date - a [ _class "pt-icon-link" - _href $"""/prayer-requests/view/{sunday.ToString ("yyyy-MM-dd", CultureInfo.InvariantCulture)}""" - _title s["List for Next Sunday"].Value ] [ - icon "update"; rawText "  "; locStr s["List for Next Sunday"] - ] - spacer - let emailPrompt = s["This will e-mail the current list to every member of your group, without further prompting. Are you sure this is what you are ready to do?"].Value - a [ _class "pt-icon-link" - _href $"/prayer-requests/email/{dtString}" - _title s["Send via E-mail"].Value - _onclick $"return PT.requests.view.promptBeforeEmail('{emailPrompt}')" ] [ - icon "mail_outline"; rawText "  "; locStr s["Send via E-mail"] + [ br [] + div [ _fieldRow ] [ + span [ _group ] [ + a [ _class "pt-icon-link" + _href $"/prayer-requests/print/{dtString}" + _target "_blank" + _title s["View Printable"].Value ] [ + icon "print"; rawText "  "; locStr s["View Printable"] ] - spacer - a [ _class "pt-icon-link"; _href "/prayer-requests"; _title s["Maintain Prayer Requests"].Value ] [ - icon "compare_arrows"; rawText "  "; locStr s["Maintain Prayer Requests"] + if model.CanEmail then + if model.Date.DayOfWeek <> IsoDayOfWeek.Sunday then + let rec findSunday (date : LocalDate) = + if date.DayOfWeek = IsoDayOfWeek.Sunday then date else findSunday (date.PlusDays 1) + let sunday = findSunday model.Date + a [ _class "pt-icon-link" + _href $"""/prayer-requests/view/{sunday.ToString ("yyyy-MM-dd", CultureInfo.InvariantCulture)}""" + _title s["List for Next Sunday"].Value ] [ + icon "update"; rawText "  "; locStr s["List for Next Sunday"] + ] + let emailPrompt = s["This will e-mail the current list to every member of your group, without further prompting. Are you sure this is what you are ready to do?"].Value + a [ _class "pt-icon-link" + _href $"/prayer-requests/email/{dtString}" + _title s["Send via E-mail"].Value + _onclick $"return PT.requests.view.promptBeforeEmail('{emailPrompt}')" ] [ + icon "mail_outline"; rawText "  "; locStr s["Send via E-mail"] + ] + a [ _class "pt-icon-link"; _href "/prayer-requests"; _title s["Maintain Prayer Requests"].Value ] [ + icon "compare_arrows"; rawText "  "; locStr s["Maintain Prayer Requests"] + ] ] ] br [] diff --git a/src/PrayerTracker.UI/SmallGroup.fs b/src/PrayerTracker.UI/SmallGroup.fs index 74acb79..03e2817 100644 --- a/src/PrayerTracker.UI/SmallGroup.fs +++ b/src/PrayerTracker.UI/SmallGroup.fs @@ -30,7 +30,7 @@ let announcement isAdmin ctx viewInfo = div [ _fieldRow ] [ div [ _inputField ] [ label [] [ locStr s["Send Announcement to"]; rawText ":" ] - div [ _class "pt-radio-group" ] [ + div [ _group ] [ label [] [ radio (nameof model.SendToClass) $"{nameof model.SendToClass}_Y" "Y" "Y" locStr s["This Group"] @@ -457,7 +457,7 @@ let preferences (model : EditPreferences) ctx viewInfo = div [ _fieldRow ] [ div [ _inputField ] [ label [] [ locStr s["Color of Heading Lines"] ] - span [ _class "pt-radio-group" ] [ + span [ _group ] [ span [] [ label [] [ radio (nameof model.LineColorType) $"{nameof model.LineColorType}_Name" "Name" @@ -488,7 +488,7 @@ let preferences (model : EditPreferences) ctx viewInfo = div [ _fieldRow ] [ div [ _inputField ] [ label [] [ locStr s["Color of Heading Text"] ] - span [ _class "pt-radio-group" ] [ + span [ _group ] [ span [] [ label [] [ radio (nameof model.HeadingColorType) $"{nameof model.HeadingColorType}_Name" "Name" @@ -522,14 +522,14 @@ let preferences (model : EditPreferences) ctx viewInfo = div [ _inputField ] [ label [ _for (nameof model.Fonts) ] [ locStr s["Fonts** for List"] ] let value = if model.IsNative then "True" else "False" - span [ _class "pt-radio-group" ] [ + span [ _group ] [ label [] [ radio (nameof model.IsNative) $"{nameof model.IsNative}_Y" "True" value locStr s["Native Fonts"] ] inputField "text" "nativeFontSpacer" "" [ _style "visibility:hidden" ] ] - span [ _class "pt-radio-group" ] [ + span [ _group ] [ label [] [ radio (nameof model.IsNative) $"{nameof model.IsNative}_N" "False" value locStr s["Named Fonts"] @@ -557,7 +557,7 @@ let preferences (model : EditPreferences) ctx viewInfo = div [ _fieldRow ] [ div [ _inputField ] [ label [] [ locStr s["Request List Visibility"] ] - span [ _class "pt-radio-group" ] [ + span [ _group ] [ let name = nameof model.Visibility let value = string model.Visibility label [] [ diff --git a/src/PrayerTracker/PrayerRequest.fs b/src/PrayerTracker/PrayerRequest.fs index 52a90f0..e577190 100644 --- a/src/PrayerTracker/PrayerRequest.fs +++ b/src/PrayerTracker/PrayerRequest.fs @@ -126,7 +126,7 @@ let expire reqId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task /// GET /prayer-requests/[group-id]/list let list groupId : HttpHandler = requireAccess [ AccessLevel.Public ] >=> fun next ctx -> task { - match! SmallGroups.tryByIdWithPreferences groupId ctx.Conn with + match! SmallGroups.tryByIdWithPreferences (SmallGroupId groupId) ctx.Conn with | Some group when group.Preferences.IsPublic -> let! reqs = PrayerRequests.forGroup diff --git a/src/PrayerTracker/wwwroot/css/app.css b/src/PrayerTracker/wwwroot/css/app.css index 080a54e..48bbda1 100644 --- a/src/PrayerTracker/wwwroot/css/app.css +++ b/src/PrayerTracker/wwwroot/css/app.css @@ -254,7 +254,7 @@ footer a:hover { text-transform: uppercase; color: #777; } -.pt-radio-group { +.pt-group { display: flex; flex-flow: row; gap: 1.5rem; diff --git a/src/PrayerTracker/wwwroot/js/app.js b/src/PrayerTracker/wwwroot/js/app.js index efd2e43..9b0de67 100644 --- a/src/PrayerTracker/wwwroot/js/app.js +++ b/src/PrayerTracker/wwwroot/js/app.js @@ -301,3 +301,23 @@ this.PT = { htmx.on("htmx:configRequest", function (e) { e.detail.headers["X-Target"] = e.detail.target }) +htmx.on("htmx:responseError", function (e) { + /** @type {XMLHttpRequest} */ + const xhr = e.detail.xhr + + const detail = document.createElement("div") + detail.className = "description" + detail.innerHTML = `(Status code ${xhr.status}: ${xhr.statusText})` + + const msg = document.createElement("div") + msg.className = "pt-msg error" + msg.innerHTML = `ERROR » ${xhr.responseText}
` + msg.appendChild(detail) + + const messages = document.createElement("div") + messages.className = "pt-messages" + messages.appendChild(msg) + + const title = document.getElementById("pt-page-title") + title.parentNode.insertBefore(messages, title.nextSibling) +})