From 6b2071b5c996cda20c6b90f413833323f446a20e Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Wed, 20 Oct 2021 20:32:46 -0400 Subject: [PATCH] Edit flow works; notes WIP --- src/MyPrayerJournal/Server/Handlers.fs | 124 ++++++++---------- src/MyPrayerJournal/Server/Views.fs | 79 ++++++++--- .../Server/wwwroot/script/mpj.js | 15 ++- 3 files changed, 135 insertions(+), 83 deletions(-) diff --git a/src/MyPrayerJournal/Server/Handlers.fs b/src/MyPrayerJournal/Server/Handlers.fs index d300f25..abcf7cd 100644 --- a/src/MyPrayerJournal/Server/Handlers.fs +++ b/src/MyPrayerJournal/Server/Handlers.fs @@ -36,7 +36,7 @@ module Error = log.LogError (EventId(), ex, "An unhandled exception has occurred while executing the request.") clearResponse >=> setStatusCode 500 - >=> setHttpHeader (sprintf "error|||%s: %s" (ex.GetType().Name) ex.Message) "X-Toast" + >=> setHttpHeader "X-Toast" (sprintf "error|||%s: %s" (ex.GetType().Name) ex.Message) >=> text ex.Message /// Handle unauthorized actions, redirecting to log on for GETs, otherwise returning a 401 Not Authorized reponse @@ -52,30 +52,6 @@ module Error = setStatusCode 404 >=> text "Not found" -/// Hold messages across redirects -module Messages = - - /// The messages being held - let mutable private messages : Map = Map.empty - - /// Locked update to prevent updates by multiple threads - let private upd8 = obj () - - /// Push a new message into the list - let push userId message url = lock upd8 (fun () -> - messages <- messages.Add (userId, (message, url))) - - /// Add a success message header to the response - let pushSuccess userId message url = - push userId (sprintf "success|||%s" message) url - - /// Pop the messages for the given user - let pop userId = lock upd8 (fun () -> - let msg = messages.TryFind userId - msg |> Option.iter (fun _ -> messages <- messages.Remove userId) - msg) - - /// Handler helpers [] module private Helpers = @@ -114,6 +90,10 @@ module private Helpers = fun next ctx -> (sprintf "%s://%s%s" ctx.Request.Scheme ctx.Request.Host.Value url |> setHttpHeader HeaderNames.Location >=> created) next ctx + + /// Return a 303 SEE OTHER response (forces a GET on the redirected URL) + let seeOther (url : string) = + setStatusCode 303 >=> setHttpHeader "Location" url /// The "now" time in JavaScript as Ticks let jsNow () = @@ -140,6 +120,29 @@ module private Helpers = return! ctx.WriteHtmlViewAsync view } + /// Hold messages across redirects + module Messages = + + /// The messages being held + let mutable private messages : Map = Map.empty + + /// Locked update to prevent updates by multiple threads + let private upd8 = obj () + + /// Push a new message into the list + let push ctx message url = lock upd8 (fun () -> + messages <- messages.Add (ctx |> (user >> Option.get), (message, url))) + + /// Add a success message header to the response + let pushSuccess ctx message url = + push ctx (sprintf "success|||%s" message) url + + /// Pop the messages for the given user + let pop userId = lock upd8 (fun () -> + let msg = messages.TryFind userId + msg |> Option.iter (fun _ -> messages <- messages.Remove userId) + msg) + /// Send a partial result if this is not a full page load let partialIfNotRefresh (pageTitle : string) content : HttpHandler = fun next ctx -> @@ -165,14 +168,14 @@ module private Helpers = module Models = /// An additional note - [] + [] type NoteEntry = { /// The notes being added notes : string } /// A prayer request - [] + [] type Request = { /// The ID of the request requestId : string @@ -191,7 +194,7 @@ module Models = } /// The time until which a request should not appear in the journal - [] + [] type SnoozeUntil = { /// The time at which the request should reappear until : int64 @@ -213,20 +216,6 @@ module Components = return! renderComponent [ Views.Journal.journalItems shown ] next ctx } - // GET /components/request/[req-id]/edit - let requestEdit requestId : HttpHandler = - requiresAuthentication Error.notAuthorized - >=> fun next ctx -> task { - match requestId with - | "new" -> - return! partialIfNotRefresh "Add Prayer Request" - (Views.Request.edit (JournalRequest.ofRequestLite Request.empty) "" false) next ctx - | _ -> - match! Data.tryJournalById (RequestId.ofString requestId) (userId ctx) (db ctx) with - | Some req -> return! partialIfNotRefresh "Edit Prayer Request" (Views.Request.edit req "" false) next ctx - | None -> return! Error.notFound next ctx - } - // GET /components/request-item/[req-id] let requestItem reqId : HttpHandler = requiresAuthentication Error.notAuthorized @@ -236,6 +225,14 @@ module Components = | None -> return! Error.notFound next ctx } + /// GET /components/request/[req-id]/notes + let notes requestId : HttpHandler = + requiresAuthentication Error.notAuthorized + >=> fun next ctx -> task { + let! notes = Data.notesById (RequestId.ofString requestId) (userId ctx) (db ctx) + return! renderComponent (Views.Request.notes notes) next ctx + } + /// / URL module Home = @@ -257,7 +254,8 @@ module Journal = |> Seq.tryFind (fun c -> c.Type = ClaimTypes.GivenName) |> Option.map (fun c -> c.Value) |> Option.defaultValue "Your" - return! partialIfNotRefresh (sprintf "%s Prayer Journal" usr) (Views.Journal.journal usr) next ctx + let title = usr |> match usr with "Your" -> sprintf "%s" | _ -> sprintf "%s's" + return! partialIfNotRefresh (sprintf "%s Prayer Journal" title) (Views.Journal.journal usr) next ctx } @@ -286,9 +284,9 @@ module Request = >=> fun next ctx -> task { let returnTo = match ctx.Request.Headers.Referer.[0] with - | it when it.EndsWith "/active" -> "active" + | it when it.EndsWith "/active" -> "active" | it when it.EndsWith "/snoozed" -> "snoozed" - | _ -> "journal" + | _ -> "journal" match requestId with | "new" -> return! partialIfNotRefresh "Add Prayer Request" @@ -381,14 +379,6 @@ module Request = | None -> return! Error.notFound next ctx } - /// GET /api/request/[req-id]/notes - let getNotes requestId : HttpHandler = - requiresAuthentication Error.notAuthorized - >=> fun next ctx -> task { - let! notes = Data.notesById (RequestId.ofString requestId) (userId ctx) (db ctx) - return! json notes next ctx - } - // PATCH /request/[req-id]/show let show requestId : HttpHandler = requiresAuthentication Error.notAuthorized @@ -465,12 +455,8 @@ module Request = } Data.addRequest req db do! db.saveChanges () - Messages.pushSuccess (ctx |> (user >> Option.get)) "Added prayer request" "/journal" - // TODO: this is not right - return! ( - redirectTo false "/journal" - // >=> createdAt (RequestId.toString req.id |> sprintf "/request/%s") - ) next ctx + Messages.pushSuccess ctx "Added prayer request" "/journal" + return! seeOther "/journal" next ctx } // PATCH /request @@ -485,20 +471,25 @@ module Request = // update recurrence if changed let (recur, interval) = parseRecurrence form match recur = req.recurType && interval = req.recurCount with - | true -> () + | true -> () | false -> do! Data.updateRecurrence req.requestId usrId recur interval db match recur with | Immediate -> do! Data.updateShowAfter req.requestId usrId (Ticks 0L) db - | _ -> () + | _ -> () // append history let upd8Text = form.requestText.Trim () let text = match upd8Text = req.text with true -> None | false -> Some upd8Text do! Data.addHistory req.requestId usrId { asOf = jsNow (); status = (Option.get >> RequestAction.ofString) form.status; text = text } db do! db.saveChanges () - return! (withSuccessMessage "Prayer request updated successfully" - >=> Components.requestItem (RequestId.toString req.requestId)) next ctx + let nextUrl = + match form.returnTo with + | "active" -> "/requests/active" + | "snoozed" -> "/requests/snoozed" + | _ (* "journal" *) -> "/journal" + Messages.pushSuccess ctx "Prayer request updated successfully" nextUrl + return! seeOther nextUrl next ctx | None -> return! Error.notFound next ctx } @@ -529,9 +520,9 @@ let routes = [ GET_HEAD [ route "/" Home.home ] subRoute "/components/" [ GET_HEAD [ - route "journal-items" Components.journalItems - routef "request/%s/edit" Components.requestEdit - routef "request/%s/item" Components.requestItem + route "journal-items" Components.journalItems + routef "request/%s/item" Components.requestItem + routef "request/%s/notes" Components.notes ] ] GET_HEAD [ route "/journal" Journal.journal ] @@ -568,7 +559,6 @@ let routes = subRoute "/api/" [ GET [ subRoute "request" [ - routef "/%s/notes" Request.getNotes routef "/%s" Request.get ] ] diff --git a/src/MyPrayerJournal/Server/Views.fs b/src/MyPrayerJournal/Server/Views.fs index 31a6a2c..9aacdb4 100644 --- a/src/MyPrayerJournal/Server/Views.fs +++ b/src/MyPrayerJournal/Server/Views.fs @@ -29,7 +29,7 @@ module private Helpers = /// Create a link that targets the `#top` element and pushes a URL to history let pageLink href attrs = attrs - |> List.append [ _href href; _hxBoost; _hxTarget "#top"; _hxPushUrl ] + |> List.append [ _href href; _hxBoost; _hxTarget "#top"; _hxSwap HxSwap.InnerHtml; _hxPushUrl ] |> a /// Create a Material icon @@ -269,27 +269,33 @@ module Journal = /// Display a card for this prayer request let journalCard req = + let reqId = RequestId.toString req.requestId + let spacer = span [] [ rawText " " ] div [ _class "col" ] [ div [ _class "card h-100" ] [ - div [ _class "card-header p-0 text-end"; _roleToolBar ] [ + div [ _class "card-header p-0 d-flex"; _roleToolBar ] [ + pageLink $"/request/{reqId}/edit" [ _class "btn btn-secondary"; _title "Edit Request" ] [ icon "edit" ] + spacer button [ - _class "btn btn-success" - _hxPatch $"/request/{RequestId.toString req.requestId}/prayed" - _title "Mark as Prayed" - ] [ icon "done" ] - // span - // md-button(@click.stop='showEdit()').md-icon-button.md-raised - // md-icon edit - // md-tooltip(md-direction='top' - // md-delay=1000) Edit Request - // md-button(@click.stop='showNotes()').md-icon-button.md-raised - // md-icon comment - // md-tooltip(md-direction='top' - // md-delay=1000) Add Notes + _type "button" + _class "btn btn-secondary" + _title "Add Notes" + _data "bs-toggle" "modal" + _data "bs-target" "#notesModal" + _data "request-id" reqId + ] [ icon "comment" ] + spacer // md-button(@click.stop='snooze()').md-icon-button.md-raised // md-icon schedule // md-tooltip(md-direction='top' // md-delay=1000) Snooze Request + div [ _class "flex-grow-1" ] [] + button [ + _type "button" + _class "btn btn-success w-25" + _hxPatch $"/request/{reqId}/prayed" + _title "Mark as Prayed" + ] [ icon "done" ] ] div [ _class "card-body" ] [ p [ _class "request-text" ] [ str req.text ] @@ -317,6 +323,40 @@ module Journal = _hxSwap HxSwap.OuterHtml _hxTrigger HxTrigger.Load ] [ rawText "Loading your prayer journal…" ] + div [ + _id "notesModal" + _class "modal fade" + _tabindex "-1" + _ariaLabelledBy "nodesModalLabel" + _ariaHidden "true" + ] [ + div [ _class "modal-dialog modal-dialog-scrollable" ] [ + div [ _class "modal-content" ] [ + div [ _class "modal-header" ] [ + h5 [ _class "modal-title"; _id "nodesModalLabel" ] [ str "Add Notes to Prayer Request" ] + button [ _type "button"; _class "btn-close"; _data "bs-dismiss" "modal"; _ariaLabel "Close" ] [] + ] + div [ _class "modal-body" ] [ + form [ _id "notesForm"; _method "POST"; _action ""; _hxBoost; _hxTarget "#top" ] [ + str "TODO" + button [ _type "submit"; _class "btn btn-primary" ] [ str "Add Notes" ] + ] + hr [] + div [ + _id "notesLoad" + _class "btn btn-secondary" + _hxGet "" + _hxSwap HxSwap.OuterHtml + _hxTarget "this" + ] [ str "Load Prior Notes" ] + ] + div [ _class "modal-footer" ] [ + button [ _type "button"; _class "btn btn-secondary"; _data "bs-dismiss" "modal" ] [ str "Close" ] + ] + ] + ] + ] + script [] [ str "setTimeout(function () { mpj.journal.setUp() }, 1000)" ] ] /// The journal items @@ -596,6 +636,15 @@ module Request = ] ] + /// Display a list of notes for a request + let notes notes = + let toItem (note : Note) = p [] [ small [ _class "text-muted" ] [ relativeDate note.asOf ]; br []; str note.notes ] + [ p [ _class "text-center" ] [ strong [] [ str "Prior Notes for This Request" ] ] + match notes with + | [] -> p [ _class "text-center text-muted" ] [ str "There are no prior notes for this request" ] + | _ -> yield! notes |> List.map toItem + ] + /// Layout views module Layout = diff --git a/src/MyPrayerJournal/Server/wwwroot/script/mpj.js b/src/MyPrayerJournal/Server/wwwroot/script/mpj.js index 56a67ee..cf0570b 100644 --- a/src/MyPrayerJournal/Server/wwwroot/script/mpj.js +++ b/src/MyPrayerJournal/Server/wwwroot/script/mpj.js @@ -49,9 +49,22 @@ const mpj = { */ toggleRecurrence ({ target }) { const isDisabled = target.value === "Immediate" - ;["recurCount","recurInterval"].forEach(it => document.getElementById(it).disabled = isDisabled) + ;["recurCount", "recurInterval"].forEach(it => document.getElementById(it).disabled = isDisabled) } }, + /** Script for the journal page */ + journal: { + /** + * Set up the journal page modals + */ + setUp () { + document.getElementById("notesModal").addEventListener("show.bs.modal", function (event) { + const reqId = event.relatedTarget.getAttribute("data-request-id") + document.getElementById("notesForm").setAttribute("action", `/request/${reqId}/note`) + document.getElementById("notesLoad").setAttribute("hx-get", `/components/request/${reqId}/notes`) + }) + } + } } htmx.on("htmx:afterOnLoad", function (evt) {