Version 3 #67

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

View File

@ -149,7 +149,7 @@ let answeredRequests userId (db : LiteDatabase) = backgroundTask {
|> Seq.sortByDescending (fun it -> Ticks.toLong it.asOf)
|> List.ofSeq
}
/// Retrieve the user's current journal
let journalByUserId userId (db : LiteDatabase) = backgroundTask {
let! jrnl = db.requests.Find (Query.EQ ("userId", UserId.toString userId)) |> toListAsync
@ -161,6 +161,12 @@ let journalByUserId userId (db : LiteDatabase) = backgroundTask {
|> List.ofSeq
}
/// Does the user have any snoozed requests?
let hasSnoozed userId now (db : LiteDatabase) = backgroundTask {
let! jrnl = journalByUserId userId db
return jrnl |> List.exists (fun r -> Ticks.toLong r.snoozedUntil > Ticks.toLong now)
}
/// Retrieve a request by its ID and user ID (without notes and history)
let tryRequestById reqId userId db = backgroundTask {
let! req = tryFullRequestById reqId userId db

View File

@ -95,9 +95,13 @@ module private Helpers =
let seeOther (url : string) =
noResponseCaching >=> setStatusCode 303 >=> setHttpHeader "Location" url
/// Convert a date/time to JS-style ticks
let toJs (date : DateTime) =
date.Subtract(DateTime (1970, 1, 1, 0, 0, 0)).TotalSeconds |> (int64 >> ( * ) 1_000L >> Ticks)
/// The "now" time in JavaScript as Ticks
let jsNow () =
DateTime.UtcNow.Subtract(DateTime (1970, 1, 1, 0, 0, 0)).TotalSeconds |> (int64 >> ( * ) 1_000L >> Ticks)
toJs DateTime.UtcNow
/// Render a component result
let renderComponent nodes : HttpHandler =
@ -106,13 +110,22 @@ module private Helpers =
return! ctx.WriteHtmlStringAsync (ViewEngine.RenderView.AsString.htmlNodes nodes)
}
open Views.Layout
/// Create a page rendering context
let pageContext (ctx : HttpContext) pageTitle content : Views.Layout.PageRenderContext =
{ isAuthenticated = (user >> Option.isSome) ctx
hasSnoozed = false
let pageContext (ctx : HttpContext) pageTitle content = backgroundTask {
let! hasSnoozed = backgroundTask {
match user ctx with
| Some _ -> return! Data.hasSnoozed (userId ctx) (jsNow ()) (db ctx)
| None -> return false
}
return {
isAuthenticated = (user >> Option.isSome) ctx
hasSnoozed = hasSnoozed
currentUrl = ctx.Request.Path.Value
pageTitle = pageTitle
content = content
}
}
/// Composable handler to write a view to the output
@ -146,18 +159,19 @@ module private Helpers =
/// Send a partial result if this is not a full page load (does not append no-cache headers)
let partialStatic (pageTitle : string) content : HttpHandler =
fun next ctx ->
let isPartial = ctx.Request.IsHtmx && not ctx.Request.IsHtmxRefresh
let view =
pageContext ctx pageTitle content
|> 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
fun next ctx -> backgroundTask {
let isPartial = ctx.Request.IsHtmx && not ctx.Request.IsHtmxRefresh
let! pageCtx = pageContext ctx pageTitle content
let view = (match isPartial with true -> partial | false -> view) pageCtx
return!
(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
}
/// Send an explicitly non-cached result, rendering as a partial if this is not a full page load
let partial pageTitle content =
@ -201,11 +215,11 @@ module Models =
recurInterval : string option
}
/// The time until which a request should not appear in the journal
/// The date until which a request should not appear in the journal
[<CLIMutable; NoComparison; NoEquality>]
type SnoozeUntil = {
/// The time at which the request should reappear
until : int64
/// The date (YYYY-MM-DD) at which the request should reappear
until : string
}
@ -233,18 +247,23 @@ module Components =
| None -> return! Error.notFound next ctx
}
/// GET /components/request/[req-id]/add-notes
// GET /components/request/[req-id]/add-notes
let addNotes requestId : HttpHandler =
requiresAuthentication Error.notAuthorized
>=> renderComponent (Views.Journal.notesEdit (RequestId.ofString requestId))
/// GET /components/request/[req-id]/notes
// GET /components/request/[req-id]/notes
let notes requestId : HttpHandler =
requiresAuthentication Error.notAuthorized
>=> fun next ctx -> backgroundTask {
let! notes = Data.notesById (RequestId.ofString requestId) (userId ctx) (db ctx)
return! renderComponent (Views.Request.notes notes) next ctx
}
// GET /components/request/[req-id]/snooze
let snooze requestId : HttpHandler =
requiresAuthentication Error.notAuthorized
>=> renderComponent [ RequestId.ofString requestId |> Views.Journal.snooze ]
/// / URL
@ -369,7 +388,7 @@ module Request =
return! partial "Answered Requests" (Views.Request.answered reqs) next ctx
}
/// GET /api/request/[req-id]
// GET /api/request/[req-id]
let get requestId : HttpHandler =
requiresAuthentication Error.notAuthorized
>=> fun next ctx -> backgroundTask {
@ -402,7 +421,7 @@ module Request =
| None -> return! Error.notFound next ctx
}
/// PATCH /api/request/[req-id]/snooze
// PATCH /request/[req-id]/snooze
let snooze requestId : HttpHandler =
requiresAuthentication Error.notAuthorized
>=> fun next ctx -> backgroundTask {
@ -411,10 +430,14 @@ module Request =
let reqId = RequestId.ofString requestId
match! Data.tryRequestById reqId usrId db with
| Some _ ->
let! until = ctx.BindJsonAsync<Models.SnoozeUntil> ()
do! Data.updateSnoozed reqId usrId (Ticks until.until) db
let! until = ctx.BindFormAsync<Models.SnoozeUntil> ()
let date = sprintf "%s 00:00:00" until.until |> DateTime.Parse
do! Data.updateSnoozed reqId usrId (toJs date) db
do! db.saveChanges ()
return! setStatusCode 204 next ctx
return!
(withSuccessMessage $"Request snoozed until {until.until}"
>=> hideModal "snooze"
>=> Components.journalItems) next ctx
| None -> return! Error.notFound next ctx
}
@ -532,6 +555,7 @@ let routes =
routef "request/%s/add-notes" Components.addNotes
routef "request/%s/item" Components.requestItem
routef "request/%s/notes" Components.notes
routef "request/%s/snooze" Components.snooze
]
]
GET_HEAD [ route "/journal" Journal.journal ]
@ -554,6 +578,7 @@ let routes =
routef "/%s/cancel-snooze" Request.cancelSnooze
routef "/%s/prayed" Request.prayed
routef "/%s/show" Request.show
routef "/%s/snooze" Request.snooze
]
POST [
route "" Request.add
@ -566,16 +591,4 @@ let routes =
route "log-on" User.logOn
]
]
subRoute "/api/" [
GET [
subRoute "request" [
routef "/%s" Request.get
]
]
PATCH [
subRoute "request" [
routef "/%s/snooze" Request.snooze
]
]
]
]

View File

@ -26,10 +26,16 @@ let journalCard req =
_hxSwap HxSwap.InnerHtml
] [ 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
button [
_type "button"
_class "btn btn-secondary"
_title "Snooze Request"
_data "bs-toggle" "modal"
_data "bs-target" "#snoozeModal"
_hxGet $"/components/request/{reqId}/snooze"
_hxTarget "#snoozeBody"
_hxSwap HxSwap.InnerHtml
] [ icon "schedule" ]
div [ _class "flex-grow-1" ] []
button [
_type "button"
@ -82,6 +88,28 @@ let journal user = article [ _class "container-fluid mt-3" ] [
]
]
]
div [
_id "snoozeModal"
_class "modal fade"
_tabindex "-1"
_ariaLabelledBy "snoozeModalLabel"
_ariaHidden "true"
] [
div [ _class "modal-dialog modal-sm" ] [
div [ _class "modal-content" ] [
div [ _class "modal-header" ] [
h5 [ _class "modal-title"; _id "snoozeModalLabel" ] [ str "Snooze Prayer Request" ]
button [ _type "button"; _class "btn-close"; _data "bs-dismiss" "modal"; _ariaLabel "Close" ] []
]
div [ _class "modal-body"; _id "snoozeBody" ] [ ]
div [ _class "modal-footer" ] [
button [ _type "button"; _id "snoozeDismiss"; _class "btn btn-secondary"; _data "bs-dismiss" "modal" ] [
str "Close"
]
]
]
]
]
]
/// The journal items
@ -96,6 +124,7 @@ let journalItems items =
items
|> List.map journalCard
|> section [
_id "journalItems"
_class "row row-cols-1 row-cols-md-2 row-cols-lg-3 row-cols-xl-4 g-3"
_hxTarget "this"
_hxSwap HxSwap.OuterHtml
@ -131,3 +160,18 @@ let notesEdit requestId =
]
]
]
/// The snooze edit form
let snooze requestId =
let today = System.DateTime.Today.ToString "yyyy-MM-dd"
form [
_hxPatch $"/request/{RequestId.toString requestId}/snooze"
_hxTarget "#journalItems"
_hxSwap HxSwap.OuterHtml
] [
div [ _class "form-floating pb-3" ] [
input [ _type "date"; _id "until"; _name "until"; _class "form-control"; _min today ]
label [ _for "until" ] [ str "Until" ]
]
p [ _class "text-end mb-0" ] [ button [ _type "submit"; _class "btn btn-primary" ] [ str "Snooze" ] ]
]