Version 3 #67

Merged
danieljsummers merged 53 commits from version-3 into master 2021-10-26 23:39:59 +00:00
5 changed files with 105 additions and 26 deletions
Showing only changes of commit d002614e48 - Show all commits

View File

@ -12,5 +12,8 @@ wwwroot/*.js.map
wwwroot/css
wwwroot/js
## Local library files
wwwroot/script/htmx*.js
## Development settings
appsettings.Development.json

View File

@ -171,16 +171,17 @@ module JournalRequest =
/// Convert a request to the form used for the journal (precomputed values, no notes or history)
let ofRequestLite (req : Request) =
let hist = req.history |> List.sortByDescending (fun it -> Ticks.toLong it.asOf) |> List.head
let hist = req.history |> List.sortByDescending (fun it -> Ticks.toLong it.asOf) |> List.tryHead
{ requestId = req.id
userId = req.userId
text = (req.history
|> List.filter (fun it -> Option.isSome it.text)
|> List.sortByDescending (fun it -> Ticks.toLong it.asOf)
|> List.head).text
|> Option.get
asOf = hist.asOf
lastStatus = hist.status
text = req.history
|> List.filter (fun it -> Option.isSome it.text)
|> List.sortByDescending (fun it -> Ticks.toLong it.asOf)
|> List.tryHead
|> Option.map (fun h -> Option.get h.text)
|> Option.defaultValue ""
asOf = match hist with Some h -> h.asOf | None -> Ticks 0L
lastStatus = match hist with Some h -> h.status | None -> Created
snoozedUntil = req.snoozedUntil
showAfter = req.showAfter
recurType = req.recurType

View File

@ -34,7 +34,10 @@ module Error =
/// Handle errors
let error (ex : Exception) (log : ILogger) =
log.LogError (EventId(), ex, "An unhandled exception has occurred while executing the request.")
clearResponse >=> setStatusCode 500 >=> json ex.Message
clearResponse
>=> setStatusCode 500
>=> setHttpHeader (sprintf "error|||%s: %s" (ex.GetType().Name) ex.Message) "X-Toast"
>=> text ex.Message
/// Handle unauthorized actions, redirecting to log on for GETs, otherwise returning a 401 Not Authorized reponse
let notAuthorized : HttpHandler =
@ -49,6 +52,30 @@ module Error =
setStatusCode 404 >=> text "Not found"
/// Hold messages across redirects
module Messages =
/// The messages being held
let mutable private messages : Map<string, (string * string)> = 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
[<AutoOpen>]
module private Helpers =
@ -116,12 +143,18 @@ module private Helpers =
/// Send a partial result if this is not a full page load
let partialIfNotRefresh (pageTitle : string) content : HttpHandler =
fun next ctx ->
let isPartial = ctx.Request.IsHtmx && not ctx.Request.IsHtmxRefresh
let view =
pageContext ctx pageTitle content
|> match ctx.Request.IsHtmx && not ctx.Request.IsHtmxRefresh with
| true -> Views.Layout.partial
| false -> Views.Layout.view
writeView view next ctx
|> match isPartial with true -> Views.Layout.partial | false -> Views.Layout.view
(next, ctx)
||> match user ctx with
| Some u ->
match Messages.pop u with
| Some (msg, url) -> setHttpHeader "X-Toast" msg >=> withHxPush url >=> writeView view
| None -> writeView view
| None -> writeView view
/// Add a success message header to the response
let withSuccessMessage : string -> HttpHandler =
@ -143,6 +176,8 @@ module Models =
type Request = {
/// The ID of the request
requestId : string
/// Where to redirect after saving
returnTo : string
/// The text of the request
requestText : string
/// The additional status to record
@ -185,10 +220,10 @@ module Components =
match requestId with
| "new" ->
return! partialIfNotRefresh "Add Prayer Request"
(Views.Request.edit (JournalRequest.ofRequestLite Request.empty) false) next ctx
(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
| Some req -> return! partialIfNotRefresh "Edit Prayer Request" (Views.Request.edit req "" false) next ctx
| None -> return! Error.notFound next ctx
}
@ -245,6 +280,26 @@ module Ply = FSharp.Control.Tasks.Affine
/// /api/request and /request(s) URLs
module Request =
// GET /request/[req-id]/edit
let edit requestId : HttpHandler =
requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task {
let returnTo =
match ctx.Request.Headers.Referer.[0] with
| it when it.EndsWith "/active" -> "active"
| it when it.EndsWith "/snoozed" -> "snoozed"
| _ -> "journal"
match requestId with
| "new" ->
return! partialIfNotRefresh "Add Prayer Request"
(Views.Request.edit (JournalRequest.ofRequestLite Request.empty) returnTo true) next ctx
| _ ->
match! Data.tryJournalById (RequestId.ofString requestId) (userId ctx) (db ctx) with
| Some req ->
return! partialIfNotRefresh "Edit Prayer Request" (Views.Request.edit req returnTo false) next ctx
| None -> return! Error.notFound next ctx
}
// PATCH /request/[req-id]/prayed
let prayed requestId : HttpHandler =
requiresAuthentication Error.notAuthorized
@ -410,8 +465,12 @@ 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! (withHxRedirect "/journal" >=> createdAt (RequestId.toString req.id |> sprintf "/request/%s")) next ctx
return! (
redirectTo false "/journal"
// >=> createdAt (RequestId.toString req.id |> sprintf "/request/%s")
) next ctx
}
// PATCH /request
@ -484,6 +543,7 @@ let routes =
]
subRoute "/request" [
GET_HEAD [
routef "/%s/edit" Request.edit
routef "/%s/full" Request.getFull
route "s/active" Request.active
route "s/answered" Request.answered

View File

@ -141,14 +141,16 @@ module Configure =
/// Configure the web application
let application (app : WebApplication) =
match app.Environment.IsDevelopment () with
| true -> app.UseDeveloperExceptionPage ()
| false -> app.UseGiraffeErrorHandler Handlers.Error.error
|> ignore
app.UseStaticFiles()
// match app.Environment.IsDevelopment () with
// | true -> app.UseDeveloperExceptionPage ()
// | false -> app.UseGiraffeErrorHandler Handlers.Error.error
// |> ignore
app
.UseStaticFiles()
.UseCookiePolicy()
.UseRouting()
.UseAuthentication()
.UseGiraffeErrorHandler(Handlers.Error.error)
// .UseAuthorization()
.UseEndpoints (fun e ->
e.MapGiraffeEndpoints Handlers.routes

View File

@ -309,6 +309,9 @@ module Journal =
| _ -> rawText "&rsquo;s"
str " Prayer Journal"
]
p [ _class "pb-3 text-center" ] [
pageLink "/request/new/edit" [ _class "btn btn-primary "] [ icon "add_box"; str " Add a Prayer Request" ]
]
p [
_hxGet "/components/journal-items"
_hxSwap HxSwap.OuterHtml
@ -472,16 +475,26 @@ module Request =
]
/// View for the edit request component
let edit (req : JournalRequest) isNew =
let cancelUrl = req.requestId |> (RequestId.toString >> sprintf "/components/request/%s/item")
section [ _class "container list-group-item"; _hxTarget "this"; _hxSwap HxSwap.OuterHtml ] [
let edit (req : JournalRequest) returnTo isNew =
let cancelLink =
match returnTo with
| "active" -> "/requests/active"
| "snoozed" -> "/requests/snoozed"
| _ (* "journal" *) -> "/journal"
article [ _class "container" ] [
h5 [ _class "pb-3" ] [ (match isNew with true -> "Add" | false -> "Edit") |> strf "%s Prayer Request" ]
form [ "/request" |> match isNew with true -> _hxPost | false -> _hxPatch ] [
form [
_hxBoost
_hxTarget "#top"
_hxPushUrl
"/request" |> match isNew with true -> _hxPost | false -> _hxPatch
] [
input [
_type "hidden"
_name "requestId"
_value (match isNew with true -> "new" | false -> RequestId.toString req.requestId)
]
input [ _type "hidden"; _name "returnTo"; _value returnTo ]
div [ _class "form-floating pb-3" ] [
textarea [
_id "requestText"
@ -578,7 +591,7 @@ module Request =
]
div [ _class "text-end pt-3" ] [
button [ _class "btn btn-primary me-2"; _type "submit" ] [ icon "save"; str " Save" ]
a [ _class "btn btn-secondary ms-2"; _href cancelUrl; _hxGet cancelUrl ] [icon "arrow_back"; str " Cancel"]
pageLink cancelLink [ _class "btn btn-secondary ms-2" ] [ icon "arrow_back"; str " Cancel" ]
]
]
]