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 63 additions and 61 deletions
Showing only changes of commit f971b2cbf4 - Show all commits
src/MyPrayerJournal/Server

View File

@ -116,20 +116,20 @@ module private Helpers =
/// Retrieve a request, including its history and notes, by its ID and user ID
let tryFullRequestById reqId userId (db : LiteDatabase) = task {
let tryFullRequestById reqId userId (db : LiteDatabase) = backgroundTask {
let! req = db.requests.Find (Query.EQ ("_id", RequestId.toString reqId)) |> firstAsync
return match box req with null -> None | _ when req.userId = userId -> Some req | _ -> None
}
/// Add a history entry
let addHistory reqId userId hist db = task {
let addHistory reqId userId hist db = backgroundTask {
match! tryFullRequestById reqId userId db with
| Some req -> do! doUpdate db { req with history = hist :: req.history }
| None -> invalidOp $"{RequestId.toString reqId} not found"
}
/// Add a note
let addNote reqId userId note db = task {
let addNote reqId userId note db = backgroundTask {
match! tryFullRequestById reqId userId db with
| Some req -> do! doUpdate db { req with notes = note :: req.notes }
| None -> invalidOp $"{RequestId.toString reqId} not found"
@ -140,7 +140,7 @@ let addRequest (req : Request) (db : LiteDatabase) =
db.requests.Insert req |> ignore
/// Retrieve all answered requests for the given user
let answeredRequests userId (db : LiteDatabase) = task {
let answeredRequests userId (db : LiteDatabase) = backgroundTask {
let! reqs = db.requests.Find (Query.EQ ("userId", UserId.toString userId)) |> toListAsync
return
reqs
@ -151,7 +151,7 @@ let answeredRequests userId (db : LiteDatabase) = task {
}
/// Retrieve the user's current journal
let journalByUserId userId (db : LiteDatabase) = task {
let journalByUserId userId (db : LiteDatabase) = backgroundTask {
let! jrnl = db.requests.Find (Query.EQ ("userId", UserId.toString userId)) |> toListAsync
return
jrnl
@ -162,38 +162,38 @@ let journalByUserId userId (db : LiteDatabase) = task {
}
/// Retrieve a request by its ID and user ID (without notes and history)
let tryRequestById reqId userId db = task {
let tryRequestById reqId userId db = backgroundTask {
let! req = tryFullRequestById reqId userId db
return req |> Option.map (fun r -> { r with history = []; notes = [] })
}
/// Retrieve notes for a request by its ID and user ID
let notesById reqId userId (db : LiteDatabase) = task {
let notesById reqId userId (db : LiteDatabase) = backgroundTask {
match! tryFullRequestById reqId userId db with | Some req -> return req.notes | None -> return []
}
/// Retrieve a journal request by its ID and user ID
let tryJournalById reqId userId (db : LiteDatabase) = task {
let tryJournalById reqId userId (db : LiteDatabase) = backgroundTask {
let! req = tryFullRequestById reqId userId db
return req |> Option.map JournalRequest.ofRequestLite
}
/// Update the recurrence for a request
let updateRecurrence reqId userId recurType recurCount db = task {
let updateRecurrence reqId userId recurType recurCount db = backgroundTask {
match! tryFullRequestById reqId userId db with
| Some req -> do! doUpdate db { req with recurType = recurType; recurCount = recurCount }
| None -> invalidOp $"{RequestId.toString reqId} not found"
}
/// Update a snoozed request
let updateSnoozed reqId userId until db = task {
let updateSnoozed reqId userId until db = backgroundTask {
match! tryFullRequestById reqId userId db with
| Some req -> do! doUpdate db { req with snoozedUntil = until; showAfter = until }
| None -> invalidOp $"{RequestId.toString reqId} not found"
}
/// Update the "show after" timestamp for a request
let updateShowAfter reqId userId showAfter db = task {
let updateShowAfter reqId userId showAfter db = backgroundTask {
match! tryFullRequestById reqId userId db with
| Some req -> do! doUpdate db { req with showAfter = showAfter }
| None -> invalidOp $"{RequestId.toString reqId} not found"

View File

@ -17,7 +17,7 @@ module private LogOnHelpers =
/// Log on, optionally specifying a redirected URL once authentication is complete
let logOn url : HttpHandler =
fun next ctx -> task {
fun next ctx -> backgroundTask {
match url with
| Some it ->
do! ctx.ChallengeAsync ("Auth0", AuthenticationProperties (RedirectUri = it))
@ -102,7 +102,7 @@ module private Helpers =
/// Render a component result
let renderComponent nodes : HttpHandler =
noResponseCaching
>=> fun next ctx -> task {
>=> fun next ctx -> backgroundTask {
return! ctx.WriteHtmlStringAsync (ViewEngine.RenderView.AsString.htmlNodes nodes)
}
@ -117,7 +117,7 @@ module private Helpers =
/// Composable handler to write a view to the output
let writeView view : HttpHandler =
fun next ctx -> task {
fun next ctx -> backgroundTask {
return! ctx.WriteHtmlViewAsync view
}
@ -167,6 +167,10 @@ module private Helpers =
let withSuccessMessage : string -> HttpHandler =
sprintf "success|||%s" >> setHttpHeader "X-Toast"
/// Hide a modal window when the response is sent
let hideModal (name : string) : HttpHandler =
setHttpHeader "X-Hide-Modal" name
/// Strongly-typed models for post requests
module Models =
@ -213,7 +217,7 @@ module Components =
// GET /components/journal-items
let journalItems : HttpHandler =
requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task {
>=> fun next ctx -> backgroundTask {
let shouldShow now r = now > Ticks.toLong r.snoozedUntil && now > Ticks.toLong r.showAfter
let! jrnl = Data.journalByUserId (userId ctx) (db ctx)
let shown = jrnl |> List.filter (shouldShow ((jsNow >> Ticks.toLong) ()))
@ -223,7 +227,7 @@ module Components =
// GET /components/request-item/[req-id]
let requestItem reqId : HttpHandler =
requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task {
>=> fun next ctx -> backgroundTask {
match! Data.tryJournalById (RequestId.ofString reqId) (userId ctx) (db ctx) with
| Some req -> return! renderComponent [ Views.Request.reqListItem req ] next ctx
| None -> return! Error.notFound next ctx
@ -237,7 +241,7 @@ module Components =
/// GET /components/request/[req-id]/notes
let notes requestId : HttpHandler =
requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task {
>=> fun next ctx -> backgroundTask {
let! notes = Data.notesById (RequestId.ofString requestId) (userId ctx) (db ctx)
return! renderComponent (Views.Request.notes notes) next ctx
}
@ -257,7 +261,7 @@ module Journal =
// GET /journal
let journal : HttpHandler =
requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task {
>=> fun next ctx -> backgroundTask {
let usr =
ctx.User.Claims
|> Seq.tryFind (fun c -> c.Type = ClaimTypes.GivenName)
@ -286,7 +290,7 @@ module Request =
// GET /request/[req-id]/edit
let edit requestId : HttpHandler =
requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task {
>=> fun next ctx -> backgroundTask {
let returnTo =
match ctx.Request.Headers.Referer.[0] with
| it when it.EndsWith "/active" -> "active"
@ -305,7 +309,7 @@ module Request =
// PATCH /request/[req-id]/prayed
let prayed requestId : HttpHandler =
requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task {
>=> fun next ctx -> backgroundTask {
let db = db ctx
let usrId = userId ctx
let reqId = RequestId.ofString requestId
@ -323,26 +327,26 @@ module Request =
| None -> return! Error.notFound next ctx
}
/// POST /api/request/[req-id]/note
/// POST /request/[req-id]/note
let addNote requestId : HttpHandler =
requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task {
>=> fun next ctx -> backgroundTask {
let db = db ctx
let usrId = userId ctx
let reqId = RequestId.ofString requestId
match! Data.tryRequestById reqId usrId db with
| Some _ ->
let! notes = ctx.BindJsonAsync<Models.NoteEntry> ()
let! notes = ctx.BindFormAsync<Models.NoteEntry> ()
do! Data.addNote reqId usrId { asOf = jsNow (); notes = notes.notes } db
do! db.saveChanges ()
return! created next ctx
return! (withSuccessMessage "Added Notes" >=> hideModal "notes" >=> created) next ctx
| None -> return! Error.notFound next ctx
}
// GET /requests/active
let active : HttpHandler =
requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task {
>=> fun next ctx -> backgroundTask {
let! reqs = Data.journalByUserId (userId ctx) (db ctx)
return! partial "Active Requests" (Views.Request.active reqs) next ctx
}
@ -350,7 +354,7 @@ module Request =
// GET /requests/snoozed
let snoozed : HttpHandler =
requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task {
>=> fun next ctx -> backgroundTask {
let! reqs = Data.journalByUserId (userId ctx) (db ctx)
let now = (jsNow >> Ticks.toLong) ()
let snoozed = reqs |> List.filter (fun r -> Ticks.toLong r.snoozedUntil > now)
@ -360,7 +364,7 @@ module Request =
// GET /requests/answered
let answered : HttpHandler =
requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task {
>=> fun next ctx -> backgroundTask {
let! reqs = Data.answeredRequests (userId ctx) (db ctx)
return! partial "Answered Requests" (Views.Request.answered reqs) next ctx
}
@ -368,7 +372,7 @@ module Request =
/// GET /api/request/[req-id]
let get requestId : HttpHandler =
requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task {
>=> fun next ctx -> backgroundTask {
match! Data.tryJournalById (RequestId.ofString requestId) (userId ctx) (db ctx) with
| Some req -> return! json req next ctx
| None -> return! Error.notFound next ctx
@ -377,7 +381,7 @@ module Request =
// GET /request/[req-id]/full
let getFull requestId : HttpHandler =
requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task {
>=> fun next ctx -> backgroundTask {
match! Data.tryFullRequestById (RequestId.ofString requestId) (userId ctx) (db ctx) with
| Some req -> return! partial "Prayer Request" (Views.Request.full req) next ctx
| None -> return! Error.notFound next ctx
@ -386,7 +390,7 @@ module Request =
// PATCH /request/[req-id]/show
let show requestId : HttpHandler =
requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task {
>=> fun next ctx -> backgroundTask {
let db = db ctx
let usrId = userId ctx
let reqId = RequestId.ofString requestId
@ -401,7 +405,7 @@ module Request =
/// PATCH /api/request/[req-id]/snooze
let snooze requestId : HttpHandler =
requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task {
>=> fun next ctx -> backgroundTask {
let db = db ctx
let usrId = userId ctx
let reqId = RequestId.ofString requestId
@ -417,7 +421,7 @@ module Request =
// PATCH /request/[req-id]/cancel-snooze
let cancelSnooze requestId : HttpHandler =
requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task {
>=> fun next ctx -> backgroundTask {
let db = db ctx
let usrId = userId ctx
let reqId = RequestId.ofString requestId
@ -437,7 +441,7 @@ module Request =
// POST /request
let add : HttpHandler =
requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task {
>=> fun next ctx -> backgroundTask {
let! form = ctx.BindModelAsync<Models.Request> ()
let db = db ctx
let usrId = userId ctx
@ -466,7 +470,7 @@ module Request =
// PATCH /request
let update : HttpHandler =
requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task {
>=> fun next ctx -> backgroundTask {
let! form = ctx.BindModelAsync<Models.Request> ()
let db = db ctx
let usrId = userId ctx
@ -552,7 +556,8 @@ let routes =
routef "/%s/show" Request.show
]
POST [
route "" Request.add
route "" Request.add
routef "/%s/note" Request.addNote
]
]
subRoute "/user/" [
@ -572,10 +577,5 @@ let routes =
routef "/%s/snooze" Request.snooze
]
]
POST [
subRoute "request" [
routef "/%s/note" Request.addNote
]
]
]
]

View File

@ -75,7 +75,9 @@ let journal user = article [ _class "container-fluid mt-3" ] [
]
div [ _class "modal-body"; _id "notesBody" ] [ ]
div [ _class "modal-footer" ] [
button [ _type "button"; _class "btn btn-secondary"; _data "bs-dismiss" "modal" ] [ str "Close" ]
button [ _type "button"; _id "notesDismiss"; _class "btn btn-secondary"; _data "bs-dismiss" "modal" ] [
str "Close"
]
]
]
]
@ -102,12 +104,23 @@ let journalItems items =
/// The notes edit modal body
let notesEdit requestId =
let reqId = RequestId.toString requestId
[ form [ _hxPost $"/request/{reqId}/note"; _hxTarget "#top" ] [
str "TODO"
button [ _type "submit"; _class "btn btn-primary" ] [ str "Add Notes" ]
[ form [ _hxPost $"/request/{reqId}/note" ] [
div [ _class "form-floating pb-3" ] [
textarea [
_id "notes"
_name "notes"
_class "form-control"
_style "min-height: 8rem;"
_placeholder "Notes"
_autofocus; _required
] [ ]
label [ _for "notes" ] [ str "Notes" ]
]
p [ _class "text-end" ] [ button [ _type "submit"; _class "btn btn-primary" ] [ str "Add Notes" ] ]
]
hr [ _style "margin: .5rem -1rem" ]
div [ _id "priorNotes" ] [
p [ _class "text-center pt-5" ] [
p [ _class "text-center pt-3" ] [
button [
_type "button"
_class "btn btn-secondary"

View File

@ -263,8 +263,7 @@ let edit (req : JournalRequest) returnTo isNew =
/// 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 ]
[ hr [ _style "margin: .5rem -1rem" ]
p [ _class "text-center" ] [ strong [] [ str "Prior Notes for This Request" ] ]
[ 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

View File

@ -66,19 +66,6 @@ const mpj = {
const isDisabled = target.value === "Immediate"
;["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`)
})
}
}
}
@ -88,5 +75,8 @@ htmx.on("htmx:afterOnLoad", function (evt) {
if (hdrs.indexOf("x-toast") >= 0) {
mpj.showToast(evt.detail.xhr.getResponseHeader("x-toast"))
}
// Hide a modal window if requested
if (hdrs.indexOf("x-hide-modal") >= 0) {
document.getElementById(evt.detail.xhr.getResponseHeader("x-hide-modal") + "Dismiss").click()
}
})