Version 3 #67
src/MyPrayerJournal/Server
3
src/MyPrayerJournal/Server/.gitignore
vendored
3
src/MyPrayerJournal/Server/.gitignore
vendored
@ -12,5 +12,8 @@ wwwroot/*.js.map
|
|||||||
wwwroot/css
|
wwwroot/css
|
||||||
wwwroot/js
|
wwwroot/js
|
||||||
|
|
||||||
|
## Local library files
|
||||||
|
wwwroot/script/htmx*.js
|
||||||
|
|
||||||
## Development settings
|
## Development settings
|
||||||
appsettings.Development.json
|
appsettings.Development.json
|
||||||
|
@ -171,16 +171,17 @@ module JournalRequest =
|
|||||||
|
|
||||||
/// Convert a request to the form used for the journal (precomputed values, no notes or history)
|
/// Convert a request to the form used for the journal (precomputed values, no notes or history)
|
||||||
let ofRequestLite (req : Request) =
|
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
|
{ requestId = req.id
|
||||||
userId = req.userId
|
userId = req.userId
|
||||||
text = (req.history
|
text = req.history
|
||||||
|> List.filter (fun it -> Option.isSome it.text)
|
|> List.filter (fun it -> Option.isSome it.text)
|
||||||
|> List.sortByDescending (fun it -> Ticks.toLong it.asOf)
|
|> List.sortByDescending (fun it -> Ticks.toLong it.asOf)
|
||||||
|> List.head).text
|
|> List.tryHead
|
||||||
|> Option.get
|
|> Option.map (fun h -> Option.get h.text)
|
||||||
asOf = hist.asOf
|
|> Option.defaultValue ""
|
||||||
lastStatus = hist.status
|
asOf = match hist with Some h -> h.asOf | None -> Ticks 0L
|
||||||
|
lastStatus = match hist with Some h -> h.status | None -> Created
|
||||||
snoozedUntil = req.snoozedUntil
|
snoozedUntil = req.snoozedUntil
|
||||||
showAfter = req.showAfter
|
showAfter = req.showAfter
|
||||||
recurType = req.recurType
|
recurType = req.recurType
|
||||||
|
@ -34,7 +34,10 @@ module Error =
|
|||||||
/// Handle errors
|
/// Handle errors
|
||||||
let error (ex : Exception) (log : ILogger) =
|
let error (ex : Exception) (log : ILogger) =
|
||||||
log.LogError (EventId(), ex, "An unhandled exception has occurred while executing the request.")
|
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
|
/// Handle unauthorized actions, redirecting to log on for GETs, otherwise returning a 401 Not Authorized reponse
|
||||||
let notAuthorized : HttpHandler =
|
let notAuthorized : HttpHandler =
|
||||||
@ -49,6 +52,30 @@ module Error =
|
|||||||
setStatusCode 404 >=> text "Not found"
|
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
|
/// Handler helpers
|
||||||
[<AutoOpen>]
|
[<AutoOpen>]
|
||||||
module private Helpers =
|
module private Helpers =
|
||||||
@ -116,12 +143,18 @@ module private Helpers =
|
|||||||
/// Send a partial result if this is not a full page load
|
/// Send a partial result if this is not a full page load
|
||||||
let partialIfNotRefresh (pageTitle : string) content : HttpHandler =
|
let partialIfNotRefresh (pageTitle : string) content : HttpHandler =
|
||||||
fun next ctx ->
|
fun next ctx ->
|
||||||
|
let isPartial = ctx.Request.IsHtmx && not ctx.Request.IsHtmxRefresh
|
||||||
let view =
|
let view =
|
||||||
pageContext ctx pageTitle content
|
pageContext ctx pageTitle content
|
||||||
|> match ctx.Request.IsHtmx && not ctx.Request.IsHtmxRefresh with
|
|> match isPartial with true -> Views.Layout.partial | false -> Views.Layout.view
|
||||||
| true -> Views.Layout.partial
|
(next, ctx)
|
||||||
| false -> Views.Layout.view
|
||> match user ctx with
|
||||||
writeView view next ctx
|
| 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
|
/// Add a success message header to the response
|
||||||
let withSuccessMessage : string -> HttpHandler =
|
let withSuccessMessage : string -> HttpHandler =
|
||||||
@ -143,6 +176,8 @@ module Models =
|
|||||||
type Request = {
|
type Request = {
|
||||||
/// The ID of the request
|
/// The ID of the request
|
||||||
requestId : string
|
requestId : string
|
||||||
|
/// Where to redirect after saving
|
||||||
|
returnTo : string
|
||||||
/// The text of the request
|
/// The text of the request
|
||||||
requestText : string
|
requestText : string
|
||||||
/// The additional status to record
|
/// The additional status to record
|
||||||
@ -185,10 +220,10 @@ module Components =
|
|||||||
match requestId with
|
match requestId with
|
||||||
| "new" ->
|
| "new" ->
|
||||||
return! partialIfNotRefresh "Add Prayer Request"
|
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
|
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
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -245,6 +280,26 @@ module Ply = FSharp.Control.Tasks.Affine
|
|||||||
/// /api/request and /request(s) URLs
|
/// /api/request and /request(s) URLs
|
||||||
module Request =
|
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
|
// PATCH /request/[req-id]/prayed
|
||||||
let prayed requestId : HttpHandler =
|
let prayed requestId : HttpHandler =
|
||||||
requiresAuthentication Error.notAuthorized
|
requiresAuthentication Error.notAuthorized
|
||||||
@ -410,8 +465,12 @@ module Request =
|
|||||||
}
|
}
|
||||||
Data.addRequest req db
|
Data.addRequest req db
|
||||||
do! db.saveChanges ()
|
do! db.saveChanges ()
|
||||||
|
Messages.pushSuccess (ctx |> (user >> Option.get)) "Added prayer request" "/journal"
|
||||||
// TODO: this is not right
|
// 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
|
// PATCH /request
|
||||||
@ -484,6 +543,7 @@ let routes =
|
|||||||
]
|
]
|
||||||
subRoute "/request" [
|
subRoute "/request" [
|
||||||
GET_HEAD [
|
GET_HEAD [
|
||||||
|
routef "/%s/edit" Request.edit
|
||||||
routef "/%s/full" Request.getFull
|
routef "/%s/full" Request.getFull
|
||||||
route "s/active" Request.active
|
route "s/active" Request.active
|
||||||
route "s/answered" Request.answered
|
route "s/answered" Request.answered
|
||||||
|
@ -141,14 +141,16 @@ module Configure =
|
|||||||
|
|
||||||
/// Configure the web application
|
/// Configure the web application
|
||||||
let application (app : WebApplication) =
|
let application (app : WebApplication) =
|
||||||
match app.Environment.IsDevelopment () with
|
// match app.Environment.IsDevelopment () with
|
||||||
| true -> app.UseDeveloperExceptionPage ()
|
// | true -> app.UseDeveloperExceptionPage ()
|
||||||
| false -> app.UseGiraffeErrorHandler Handlers.Error.error
|
// | false -> app.UseGiraffeErrorHandler Handlers.Error.error
|
||||||
|> ignore
|
// |> ignore
|
||||||
app.UseStaticFiles()
|
app
|
||||||
|
.UseStaticFiles()
|
||||||
.UseCookiePolicy()
|
.UseCookiePolicy()
|
||||||
.UseRouting()
|
.UseRouting()
|
||||||
.UseAuthentication()
|
.UseAuthentication()
|
||||||
|
.UseGiraffeErrorHandler(Handlers.Error.error)
|
||||||
// .UseAuthorization()
|
// .UseAuthorization()
|
||||||
.UseEndpoints (fun e ->
|
.UseEndpoints (fun e ->
|
||||||
e.MapGiraffeEndpoints Handlers.routes
|
e.MapGiraffeEndpoints Handlers.routes
|
||||||
|
@ -309,6 +309,9 @@ module Journal =
|
|||||||
| _ -> rawText "’s"
|
| _ -> rawText "’s"
|
||||||
str " Prayer Journal"
|
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 [
|
p [
|
||||||
_hxGet "/components/journal-items"
|
_hxGet "/components/journal-items"
|
||||||
_hxSwap HxSwap.OuterHtml
|
_hxSwap HxSwap.OuterHtml
|
||||||
@ -472,16 +475,26 @@ module Request =
|
|||||||
]
|
]
|
||||||
|
|
||||||
/// View for the edit request component
|
/// View for the edit request component
|
||||||
let edit (req : JournalRequest) isNew =
|
let edit (req : JournalRequest) returnTo isNew =
|
||||||
let cancelUrl = req.requestId |> (RequestId.toString >> sprintf "/components/request/%s/item")
|
let cancelLink =
|
||||||
section [ _class "container list-group-item"; _hxTarget "this"; _hxSwap HxSwap.OuterHtml ] [
|
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" ]
|
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 [
|
input [
|
||||||
_type "hidden"
|
_type "hidden"
|
||||||
_name "requestId"
|
_name "requestId"
|
||||||
_value (match isNew with true -> "new" | false -> RequestId.toString req.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" ] [
|
div [ _class "form-floating pb-3" ] [
|
||||||
textarea [
|
textarea [
|
||||||
_id "requestText"
|
_id "requestText"
|
||||||
@ -578,7 +591,7 @@ module Request =
|
|||||||
]
|
]
|
||||||
div [ _class "text-end pt-3" ] [
|
div [ _class "text-end pt-3" ] [
|
||||||
button [ _class "btn btn-primary me-2"; _type "submit" ] [ icon "save"; str " Save" ]
|
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" ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
Loading…
Reference in New Issue
Block a user