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

View File

@ -116,20 +116,20 @@ module private Helpers =
/// Retrieve a request, including its history and notes, by its ID and user ID /// 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 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 return match box req with null -> None | _ when req.userId = userId -> Some req | _ -> None
} }
/// Add a history entry /// Add a history entry
let addHistory reqId userId hist db = task { let addHistory reqId userId hist db = backgroundTask {
match! tryFullRequestById reqId userId db with match! tryFullRequestById reqId userId db with
| Some req -> do! doUpdate db { req with history = hist :: req.history } | Some req -> do! doUpdate db { req with history = hist :: req.history }
| None -> invalidOp $"{RequestId.toString reqId} not found" | None -> invalidOp $"{RequestId.toString reqId} not found"
} }
/// Add a note /// Add a note
let addNote reqId userId note db = task { let addNote reqId userId note db = backgroundTask {
match! tryFullRequestById reqId userId db with match! tryFullRequestById reqId userId db with
| Some req -> do! doUpdate db { req with notes = note :: req.notes } | Some req -> do! doUpdate db { req with notes = note :: req.notes }
| None -> invalidOp $"{RequestId.toString reqId} not found" | None -> invalidOp $"{RequestId.toString reqId} not found"
@ -140,7 +140,7 @@ let addRequest (req : Request) (db : LiteDatabase) =
db.requests.Insert req |> ignore db.requests.Insert req |> ignore
/// Retrieve all answered requests for the given user /// 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 let! reqs = db.requests.Find (Query.EQ ("userId", UserId.toString userId)) |> toListAsync
return return
reqs reqs
@ -151,7 +151,7 @@ let answeredRequests userId (db : LiteDatabase) = task {
} }
/// Retrieve the user's current journal /// 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 let! jrnl = db.requests.Find (Query.EQ ("userId", UserId.toString userId)) |> toListAsync
return return
jrnl jrnl
@ -162,38 +162,38 @@ let journalByUserId userId (db : LiteDatabase) = task {
} }
/// Retrieve a request by its ID and user ID (without notes and history) /// 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 let! req = tryFullRequestById reqId userId db
return req |> Option.map (fun r -> { r with history = []; notes = [] }) return req |> Option.map (fun r -> { r with history = []; notes = [] })
} }
/// Retrieve notes for a request by its ID and user ID /// 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 [] match! tryFullRequestById reqId userId db with | Some req -> return req.notes | None -> return []
} }
/// Retrieve a journal request by its ID and user ID /// 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 let! req = tryFullRequestById reqId userId db
return req |> Option.map JournalRequest.ofRequestLite return req |> Option.map JournalRequest.ofRequestLite
} }
/// Update the recurrence for a request /// 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 match! tryFullRequestById reqId userId db with
| Some req -> do! doUpdate db { req with recurType = recurType; recurCount = recurCount } | Some req -> do! doUpdate db { req with recurType = recurType; recurCount = recurCount }
| None -> invalidOp $"{RequestId.toString reqId} not found" | None -> invalidOp $"{RequestId.toString reqId} not found"
} }
/// Update a snoozed request /// Update a snoozed request
let updateSnoozed reqId userId until db = task { let updateSnoozed reqId userId until db = backgroundTask {
match! tryFullRequestById reqId userId db with match! tryFullRequestById reqId userId db with
| Some req -> do! doUpdate db { req with snoozedUntil = until; showAfter = until } | Some req -> do! doUpdate db { req with snoozedUntil = until; showAfter = until }
| None -> invalidOp $"{RequestId.toString reqId} not found" | None -> invalidOp $"{RequestId.toString reqId} not found"
} }
/// Update the "show after" timestamp for a request /// 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 match! tryFullRequestById reqId userId db with
| Some req -> do! doUpdate db { req with showAfter = showAfter } | Some req -> do! doUpdate db { req with showAfter = showAfter }
| None -> invalidOp $"{RequestId.toString reqId} not found" | 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 /// Log on, optionally specifying a redirected URL once authentication is complete
let logOn url : HttpHandler = let logOn url : HttpHandler =
fun next ctx -> task { fun next ctx -> backgroundTask {
match url with match url with
| Some it -> | Some it ->
do! ctx.ChallengeAsync ("Auth0", AuthenticationProperties (RedirectUri = it)) do! ctx.ChallengeAsync ("Auth0", AuthenticationProperties (RedirectUri = it))
@ -102,7 +102,7 @@ module private Helpers =
/// Render a component result /// Render a component result
let renderComponent nodes : HttpHandler = let renderComponent nodes : HttpHandler =
noResponseCaching noResponseCaching
>=> fun next ctx -> task { >=> fun next ctx -> backgroundTask {
return! ctx.WriteHtmlStringAsync (ViewEngine.RenderView.AsString.htmlNodes nodes) return! ctx.WriteHtmlStringAsync (ViewEngine.RenderView.AsString.htmlNodes nodes)
} }
@ -117,7 +117,7 @@ module private Helpers =
/// Composable handler to write a view to the output /// Composable handler to write a view to the output
let writeView view : HttpHandler = let writeView view : HttpHandler =
fun next ctx -> task { fun next ctx -> backgroundTask {
return! ctx.WriteHtmlViewAsync view return! ctx.WriteHtmlViewAsync view
} }
@ -167,6 +167,10 @@ module private Helpers =
let withSuccessMessage : string -> HttpHandler = let withSuccessMessage : string -> HttpHandler =
sprintf "success|||%s" >> setHttpHeader "X-Toast" 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 /// Strongly-typed models for post requests
module Models = module Models =
@ -213,7 +217,7 @@ module Components =
// GET /components/journal-items // GET /components/journal-items
let journalItems : HttpHandler = let journalItems : HttpHandler =
requiresAuthentication Error.notAuthorized 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 shouldShow now r = now > Ticks.toLong r.snoozedUntil && now > Ticks.toLong r.showAfter
let! jrnl = Data.journalByUserId (userId ctx) (db ctx) let! jrnl = Data.journalByUserId (userId ctx) (db ctx)
let shown = jrnl |> List.filter (shouldShow ((jsNow >> Ticks.toLong) ())) let shown = jrnl |> List.filter (shouldShow ((jsNow >> Ticks.toLong) ()))
@ -223,7 +227,7 @@ module Components =
// GET /components/request-item/[req-id] // GET /components/request-item/[req-id]
let requestItem reqId : HttpHandler = let requestItem reqId : HttpHandler =
requiresAuthentication Error.notAuthorized requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task { >=> fun next ctx -> backgroundTask {
match! Data.tryJournalById (RequestId.ofString reqId) (userId ctx) (db ctx) with match! Data.tryJournalById (RequestId.ofString reqId) (userId ctx) (db ctx) with
| Some req -> return! renderComponent [ Views.Request.reqListItem req ] next ctx | Some req -> return! renderComponent [ Views.Request.reqListItem req ] next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
@ -237,7 +241,7 @@ module Components =
/// GET /components/request/[req-id]/notes /// GET /components/request/[req-id]/notes
let notes requestId : HttpHandler = let notes requestId : HttpHandler =
requiresAuthentication Error.notAuthorized requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task { >=> fun next ctx -> backgroundTask {
let! notes = Data.notesById (RequestId.ofString requestId) (userId ctx) (db ctx) let! notes = Data.notesById (RequestId.ofString requestId) (userId ctx) (db ctx)
return! renderComponent (Views.Request.notes notes) next ctx return! renderComponent (Views.Request.notes notes) next ctx
} }
@ -257,7 +261,7 @@ module Journal =
// GET /journal // GET /journal
let journal : HttpHandler = let journal : HttpHandler =
requiresAuthentication Error.notAuthorized requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task { >=> fun next ctx -> backgroundTask {
let usr = let usr =
ctx.User.Claims ctx.User.Claims
|> Seq.tryFind (fun c -> c.Type = ClaimTypes.GivenName) |> Seq.tryFind (fun c -> c.Type = ClaimTypes.GivenName)
@ -286,7 +290,7 @@ module Request =
// GET /request/[req-id]/edit // GET /request/[req-id]/edit
let edit requestId : HttpHandler = let edit requestId : HttpHandler =
requiresAuthentication Error.notAuthorized requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task { >=> fun next ctx -> backgroundTask {
let returnTo = let returnTo =
match ctx.Request.Headers.Referer.[0] with match ctx.Request.Headers.Referer.[0] with
| it when it.EndsWith "/active" -> "active" | it when it.EndsWith "/active" -> "active"
@ -305,7 +309,7 @@ module Request =
// PATCH /request/[req-id]/prayed // PATCH /request/[req-id]/prayed
let prayed requestId : HttpHandler = let prayed requestId : HttpHandler =
requiresAuthentication Error.notAuthorized requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task { >=> fun next ctx -> backgroundTask {
let db = db ctx let db = db ctx
let usrId = userId ctx let usrId = userId ctx
let reqId = RequestId.ofString requestId let reqId = RequestId.ofString requestId
@ -323,26 +327,26 @@ module Request =
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
} }
/// POST /api/request/[req-id]/note /// POST /request/[req-id]/note
let addNote requestId : HttpHandler = let addNote requestId : HttpHandler =
requiresAuthentication Error.notAuthorized requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task { >=> fun next ctx -> backgroundTask {
let db = db ctx let db = db ctx
let usrId = userId ctx let usrId = userId ctx
let reqId = RequestId.ofString requestId let reqId = RequestId.ofString requestId
match! Data.tryRequestById reqId usrId db with match! Data.tryRequestById reqId usrId db with
| Some _ -> | 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! Data.addNote reqId usrId { asOf = jsNow (); notes = notes.notes } db
do! db.saveChanges () do! db.saveChanges ()
return! created next ctx return! (withSuccessMessage "Added Notes" >=> hideModal "notes" >=> created) next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
} }
// GET /requests/active // GET /requests/active
let active : HttpHandler = let active : HttpHandler =
requiresAuthentication Error.notAuthorized requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task { >=> fun next ctx -> backgroundTask {
let! reqs = Data.journalByUserId (userId ctx) (db ctx) let! reqs = Data.journalByUserId (userId ctx) (db ctx)
return! partial "Active Requests" (Views.Request.active reqs) next ctx return! partial "Active Requests" (Views.Request.active reqs) next ctx
} }
@ -350,7 +354,7 @@ module Request =
// GET /requests/snoozed // GET /requests/snoozed
let snoozed : HttpHandler = let snoozed : HttpHandler =
requiresAuthentication Error.notAuthorized requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task { >=> fun next ctx -> backgroundTask {
let! reqs = Data.journalByUserId (userId ctx) (db ctx) let! reqs = Data.journalByUserId (userId ctx) (db ctx)
let now = (jsNow >> Ticks.toLong) () let now = (jsNow >> Ticks.toLong) ()
let snoozed = reqs |> List.filter (fun r -> Ticks.toLong r.snoozedUntil > now) let snoozed = reqs |> List.filter (fun r -> Ticks.toLong r.snoozedUntil > now)
@ -360,7 +364,7 @@ module Request =
// GET /requests/answered // GET /requests/answered
let answered : HttpHandler = let answered : HttpHandler =
requiresAuthentication Error.notAuthorized requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task { >=> fun next ctx -> backgroundTask {
let! reqs = Data.answeredRequests (userId ctx) (db ctx) let! reqs = Data.answeredRequests (userId ctx) (db ctx)
return! partial "Answered Requests" (Views.Request.answered reqs) next ctx return! partial "Answered Requests" (Views.Request.answered reqs) next ctx
} }
@ -368,7 +372,7 @@ module Request =
/// GET /api/request/[req-id] /// GET /api/request/[req-id]
let get requestId : HttpHandler = let get requestId : HttpHandler =
requiresAuthentication Error.notAuthorized requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task { >=> fun next ctx -> backgroundTask {
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! json req next ctx | Some req -> return! json req next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
@ -377,7 +381,7 @@ module Request =
// GET /request/[req-id]/full // GET /request/[req-id]/full
let getFull requestId : HttpHandler = let getFull requestId : HttpHandler =
requiresAuthentication Error.notAuthorized requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task { >=> fun next ctx -> backgroundTask {
match! Data.tryFullRequestById (RequestId.ofString requestId) (userId ctx) (db ctx) with match! Data.tryFullRequestById (RequestId.ofString requestId) (userId ctx) (db ctx) with
| Some req -> return! partial "Prayer Request" (Views.Request.full req) next ctx | Some req -> return! partial "Prayer Request" (Views.Request.full req) next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
@ -386,7 +390,7 @@ module Request =
// PATCH /request/[req-id]/show // PATCH /request/[req-id]/show
let show requestId : HttpHandler = let show requestId : HttpHandler =
requiresAuthentication Error.notAuthorized requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task { >=> fun next ctx -> backgroundTask {
let db = db ctx let db = db ctx
let usrId = userId ctx let usrId = userId ctx
let reqId = RequestId.ofString requestId let reqId = RequestId.ofString requestId
@ -401,7 +405,7 @@ module Request =
/// PATCH /api/request/[req-id]/snooze /// PATCH /api/request/[req-id]/snooze
let snooze requestId : HttpHandler = let snooze requestId : HttpHandler =
requiresAuthentication Error.notAuthorized requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task { >=> fun next ctx -> backgroundTask {
let db = db ctx let db = db ctx
let usrId = userId ctx let usrId = userId ctx
let reqId = RequestId.ofString requestId let reqId = RequestId.ofString requestId
@ -417,7 +421,7 @@ module Request =
// PATCH /request/[req-id]/cancel-snooze // PATCH /request/[req-id]/cancel-snooze
let cancelSnooze requestId : HttpHandler = let cancelSnooze requestId : HttpHandler =
requiresAuthentication Error.notAuthorized requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task { >=> fun next ctx -> backgroundTask {
let db = db ctx let db = db ctx
let usrId = userId ctx let usrId = userId ctx
let reqId = RequestId.ofString requestId let reqId = RequestId.ofString requestId
@ -437,7 +441,7 @@ module Request =
// POST /request // POST /request
let add : HttpHandler = let add : HttpHandler =
requiresAuthentication Error.notAuthorized requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task { >=> fun next ctx -> backgroundTask {
let! form = ctx.BindModelAsync<Models.Request> () let! form = ctx.BindModelAsync<Models.Request> ()
let db = db ctx let db = db ctx
let usrId = userId ctx let usrId = userId ctx
@ -466,7 +470,7 @@ module Request =
// PATCH /request // PATCH /request
let update : HttpHandler = let update : HttpHandler =
requiresAuthentication Error.notAuthorized requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task { >=> fun next ctx -> backgroundTask {
let! form = ctx.BindModelAsync<Models.Request> () let! form = ctx.BindModelAsync<Models.Request> ()
let db = db ctx let db = db ctx
let usrId = userId ctx let usrId = userId ctx
@ -552,7 +556,8 @@ let routes =
routef "/%s/show" Request.show routef "/%s/show" Request.show
] ]
POST [ POST [
route "" Request.add route "" Request.add
routef "/%s/note" Request.addNote
] ]
] ]
subRoute "/user/" [ subRoute "/user/" [
@ -572,10 +577,5 @@ let routes =
routef "/%s/snooze" Request.snooze 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-body"; _id "notesBody" ] [ ]
div [ _class "modal-footer" ] [ 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 /// The notes edit modal body
let notesEdit requestId = let notesEdit requestId =
let reqId = RequestId.toString requestId let reqId = RequestId.toString requestId
[ form [ _hxPost $"/request/{reqId}/note"; _hxTarget "#top" ] [ [ form [ _hxPost $"/request/{reqId}/note" ] [
str "TODO" div [ _class "form-floating pb-3" ] [
button [ _type "submit"; _class "btn btn-primary" ] [ str "Add Notes" ] 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" ] [ div [ _id "priorNotes" ] [
p [ _class "text-center pt-5" ] [ p [ _class "text-center pt-3" ] [
button [ button [
_type "button" _type "button"
_class "btn btn-secondary" _class "btn btn-secondary"

View File

@ -263,8 +263,7 @@ let edit (req : JournalRequest) returnTo isNew =
/// Display a list of notes for a request /// Display a list of notes for a request
let notes notes = let notes notes =
let toItem (note : Note) = p [] [ small [ _class "text-muted" ] [ relativeDate note.asOf ]; br []; str note.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 match notes with
| [] -> p [ _class "text-center text-muted" ] [ str "There are no prior notes for this request" ] | [] -> p [ _class "text-center text-muted" ] [ str "There are no prior notes for this request" ]
| _ -> yield! notes |> List.map toItem | _ -> yield! notes |> List.map toItem

View File

@ -66,19 +66,6 @@ const mpj = {
const isDisabled = target.value === "Immediate" 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`)
})
}
} }
} }
@ -88,5 +75,8 @@ htmx.on("htmx:afterOnLoad", function (evt) {
if (hdrs.indexOf("x-toast") >= 0) { if (hdrs.indexOf("x-toast") >= 0) {
mpj.showToast(evt.detail.xhr.getResponseHeader("x-toast")) 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()
}
}) })