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 135 additions and 83 deletions
Showing only changes of commit 6b2071b5c9 - Show all commits

View File

@ -36,7 +36,7 @@ module Error =
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 clearResponse
>=> setStatusCode 500 >=> setStatusCode 500
>=> setHttpHeader (sprintf "error|||%s: %s" (ex.GetType().Name) ex.Message) "X-Toast" >=> setHttpHeader "X-Toast" (sprintf "error|||%s: %s" (ex.GetType().Name) ex.Message)
>=> text ex.Message >=> 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
@ -52,30 +52,6 @@ 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 =
@ -114,6 +90,10 @@ module private Helpers =
fun next ctx -> fun next ctx ->
(sprintf "%s://%s%s" ctx.Request.Scheme ctx.Request.Host.Value url |> setHttpHeader HeaderNames.Location (sprintf "%s://%s%s" ctx.Request.Scheme ctx.Request.Host.Value url |> setHttpHeader HeaderNames.Location
>=> created) next ctx >=> created) next ctx
/// Return a 303 SEE OTHER response (forces a GET on the redirected URL)
let seeOther (url : string) =
setStatusCode 303 >=> setHttpHeader "Location" url
/// The "now" time in JavaScript as Ticks /// The "now" time in JavaScript as Ticks
let jsNow () = let jsNow () =
@ -140,6 +120,29 @@ module private Helpers =
return! ctx.WriteHtmlViewAsync view return! ctx.WriteHtmlViewAsync view
} }
/// 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 ctx message url = lock upd8 (fun () ->
messages <- messages.Add (ctx |> (user >> Option.get), (message, url)))
/// Add a success message header to the response
let pushSuccess ctx message url =
push ctx (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)
/// 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 ->
@ -165,14 +168,14 @@ module private Helpers =
module Models = module Models =
/// An additional note /// An additional note
[<CLIMutable>] [<CLIMutable; NoComparison; NoEquality>]
type NoteEntry = { type NoteEntry = {
/// The notes being added /// The notes being added
notes : string notes : string
} }
/// A prayer request /// A prayer request
[<CLIMutable>] [<CLIMutable; NoComparison; NoEquality>]
type Request = { type Request = {
/// The ID of the request /// The ID of the request
requestId : string requestId : string
@ -191,7 +194,7 @@ module Models =
} }
/// The time until which a request should not appear in the journal /// The time until which a request should not appear in the journal
[<CLIMutable>] [<CLIMutable; NoComparison; NoEquality>]
type SnoozeUntil = { type SnoozeUntil = {
/// The time at which the request should reappear /// The time at which the request should reappear
until : int64 until : int64
@ -213,20 +216,6 @@ module Components =
return! renderComponent [ Views.Journal.journalItems shown ] next ctx return! renderComponent [ Views.Journal.journalItems shown ] next ctx
} }
// GET /components/request/[req-id]/edit
let requestEdit requestId : HttpHandler =
requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task {
match requestId with
| "new" ->
return! partialIfNotRefresh "Add Prayer Request"
(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
| None -> return! Error.notFound next ctx
}
// 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
@ -236,6 +225,14 @@ module Components =
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
} }
/// GET /components/request/[req-id]/notes
let notes requestId : HttpHandler =
requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task {
let! notes = Data.notesById (RequestId.ofString requestId) (userId ctx) (db ctx)
return! renderComponent (Views.Request.notes notes) next ctx
}
/// / URL /// / URL
module Home = module Home =
@ -257,7 +254,8 @@ module Journal =
|> Seq.tryFind (fun c -> c.Type = ClaimTypes.GivenName) |> Seq.tryFind (fun c -> c.Type = ClaimTypes.GivenName)
|> Option.map (fun c -> c.Value) |> Option.map (fun c -> c.Value)
|> Option.defaultValue "Your" |> Option.defaultValue "Your"
return! partialIfNotRefresh (sprintf "%s Prayer Journal" usr) (Views.Journal.journal usr) next ctx let title = usr |> match usr with "Your" -> sprintf "%s" | _ -> sprintf "%s's"
return! partialIfNotRefresh (sprintf "%s Prayer Journal" title) (Views.Journal.journal usr) next ctx
} }
@ -286,9 +284,9 @@ module Request =
>=> fun next ctx -> task { >=> fun next ctx -> task {
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"
| it when it.EndsWith "/snoozed" -> "snoozed" | it when it.EndsWith "/snoozed" -> "snoozed"
| _ -> "journal" | _ -> "journal"
match requestId with match requestId with
| "new" -> | "new" ->
return! partialIfNotRefresh "Add Prayer Request" return! partialIfNotRefresh "Add Prayer Request"
@ -381,14 +379,6 @@ module Request =
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
} }
/// GET /api/request/[req-id]/notes
let getNotes requestId : HttpHandler =
requiresAuthentication Error.notAuthorized
>=> fun next ctx -> task {
let! notes = Data.notesById (RequestId.ofString requestId) (userId ctx) (db ctx)
return! json notes next ctx
}
// PATCH /request/[req-id]/show // PATCH /request/[req-id]/show
let show requestId : HttpHandler = let show requestId : HttpHandler =
requiresAuthentication Error.notAuthorized requiresAuthentication Error.notAuthorized
@ -465,12 +455,8 @@ 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" Messages.pushSuccess ctx "Added prayer request" "/journal"
// TODO: this is not right return! seeOther "/journal" next ctx
return! (
redirectTo false "/journal"
// >=> createdAt (RequestId.toString req.id |> sprintf "/request/%s")
) next ctx
} }
// PATCH /request // PATCH /request
@ -485,20 +471,25 @@ module Request =
// update recurrence if changed // update recurrence if changed
let (recur, interval) = parseRecurrence form let (recur, interval) = parseRecurrence form
match recur = req.recurType && interval = req.recurCount with match recur = req.recurType && interval = req.recurCount with
| true -> () | true -> ()
| false -> | false ->
do! Data.updateRecurrence req.requestId usrId recur interval db do! Data.updateRecurrence req.requestId usrId recur interval db
match recur with match recur with
| Immediate -> do! Data.updateShowAfter req.requestId usrId (Ticks 0L) db | Immediate -> do! Data.updateShowAfter req.requestId usrId (Ticks 0L) db
| _ -> () | _ -> ()
// append history // append history
let upd8Text = form.requestText.Trim () let upd8Text = form.requestText.Trim ()
let text = match upd8Text = req.text with true -> None | false -> Some upd8Text let text = match upd8Text = req.text with true -> None | false -> Some upd8Text
do! Data.addHistory req.requestId usrId do! Data.addHistory req.requestId usrId
{ asOf = jsNow (); status = (Option.get >> RequestAction.ofString) form.status; text = text } db { asOf = jsNow (); status = (Option.get >> RequestAction.ofString) form.status; text = text } db
do! db.saveChanges () do! db.saveChanges ()
return! (withSuccessMessage "Prayer request updated successfully" let nextUrl =
>=> Components.requestItem (RequestId.toString req.requestId)) next ctx match form.returnTo with
| "active" -> "/requests/active"
| "snoozed" -> "/requests/snoozed"
| _ (* "journal" *) -> "/journal"
Messages.pushSuccess ctx "Prayer request updated successfully" nextUrl
return! seeOther nextUrl next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
} }
@ -529,9 +520,9 @@ let routes =
[ GET_HEAD [ route "/" Home.home ] [ GET_HEAD [ route "/" Home.home ]
subRoute "/components/" [ subRoute "/components/" [
GET_HEAD [ GET_HEAD [
route "journal-items" Components.journalItems route "journal-items" Components.journalItems
routef "request/%s/edit" Components.requestEdit routef "request/%s/item" Components.requestItem
routef "request/%s/item" Components.requestItem routef "request/%s/notes" Components.notes
] ]
] ]
GET_HEAD [ route "/journal" Journal.journal ] GET_HEAD [ route "/journal" Journal.journal ]
@ -568,7 +559,6 @@ let routes =
subRoute "/api/" [ subRoute "/api/" [
GET [ GET [
subRoute "request" [ subRoute "request" [
routef "/%s/notes" Request.getNotes
routef "/%s" Request.get routef "/%s" Request.get
] ]
] ]

View File

@ -29,7 +29,7 @@ module private Helpers =
/// Create a link that targets the `#top` element and pushes a URL to history /// Create a link that targets the `#top` element and pushes a URL to history
let pageLink href attrs = let pageLink href attrs =
attrs attrs
|> List.append [ _href href; _hxBoost; _hxTarget "#top"; _hxPushUrl ] |> List.append [ _href href; _hxBoost; _hxTarget "#top"; _hxSwap HxSwap.InnerHtml; _hxPushUrl ]
|> a |> a
/// Create a Material icon /// Create a Material icon
@ -269,27 +269,33 @@ module Journal =
/// Display a card for this prayer request /// Display a card for this prayer request
let journalCard req = let journalCard req =
let reqId = RequestId.toString req.requestId
let spacer = span [] [ rawText "&nbsp;" ]
div [ _class "col" ] [ div [ _class "col" ] [
div [ _class "card h-100" ] [ div [ _class "card h-100" ] [
div [ _class "card-header p-0 text-end"; _roleToolBar ] [ div [ _class "card-header p-0 d-flex"; _roleToolBar ] [
pageLink $"/request/{reqId}/edit" [ _class "btn btn-secondary"; _title "Edit Request" ] [ icon "edit" ]
spacer
button [ button [
_class "btn btn-success" _type "button"
_hxPatch $"/request/{RequestId.toString req.requestId}/prayed" _class "btn btn-secondary"
_title "Mark as Prayed" _title "Add Notes"
] [ icon "done" ] _data "bs-toggle" "modal"
// span _data "bs-target" "#notesModal"
// md-button(@click.stop='showEdit()').md-icon-button.md-raised _data "request-id" reqId
// md-icon edit ] [ icon "comment" ]
// md-tooltip(md-direction='top' spacer
// md-delay=1000) Edit Request
// md-button(@click.stop='showNotes()').md-icon-button.md-raised
// md-icon comment
// md-tooltip(md-direction='top'
// md-delay=1000) Add Notes
// md-button(@click.stop='snooze()').md-icon-button.md-raised // md-button(@click.stop='snooze()').md-icon-button.md-raised
// md-icon schedule // md-icon schedule
// md-tooltip(md-direction='top' // md-tooltip(md-direction='top'
// md-delay=1000) Snooze Request // md-delay=1000) Snooze Request
div [ _class "flex-grow-1" ] []
button [
_type "button"
_class "btn btn-success w-25"
_hxPatch $"/request/{reqId}/prayed"
_title "Mark as Prayed"
] [ icon "done" ]
] ]
div [ _class "card-body" ] [ div [ _class "card-body" ] [
p [ _class "request-text" ] [ str req.text ] p [ _class "request-text" ] [ str req.text ]
@ -317,6 +323,40 @@ module Journal =
_hxSwap HxSwap.OuterHtml _hxSwap HxSwap.OuterHtml
_hxTrigger HxTrigger.Load _hxTrigger HxTrigger.Load
] [ rawText "Loading your prayer journal&hellip;" ] ] [ rawText "Loading your prayer journal&hellip;" ]
div [
_id "notesModal"
_class "modal fade"
_tabindex "-1"
_ariaLabelledBy "nodesModalLabel"
_ariaHidden "true"
] [
div [ _class "modal-dialog modal-dialog-scrollable" ] [
div [ _class "modal-content" ] [
div [ _class "modal-header" ] [
h5 [ _class "modal-title"; _id "nodesModalLabel" ] [ str "Add Notes to Prayer Request" ]
button [ _type "button"; _class "btn-close"; _data "bs-dismiss" "modal"; _ariaLabel "Close" ] []
]
div [ _class "modal-body" ] [
form [ _id "notesForm"; _method "POST"; _action ""; _hxBoost; _hxTarget "#top" ] [
str "TODO"
button [ _type "submit"; _class "btn btn-primary" ] [ str "Add Notes" ]
]
hr []
div [
_id "notesLoad"
_class "btn btn-secondary"
_hxGet ""
_hxSwap HxSwap.OuterHtml
_hxTarget "this"
] [ str "Load Prior Notes" ]
]
div [ _class "modal-footer" ] [
button [ _type "button"; _class "btn btn-secondary"; _data "bs-dismiss" "modal" ] [ str "Close" ]
]
]
]
]
script [] [ str "setTimeout(function () { mpj.journal.setUp() }, 1000)" ]
] ]
/// The journal items /// The journal items
@ -596,6 +636,15 @@ module Request =
] ]
] ]
/// 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 ]
[ 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
]
/// Layout views /// Layout views
module Layout = module Layout =

View File

@ -49,9 +49,22 @@ const mpj = {
*/ */
toggleRecurrence ({ target }) { toggleRecurrence ({ target }) {
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`)
})
}
}
} }
htmx.on("htmx:afterOnLoad", function (evt) { htmx.on("htmx:afterOnLoad", function (evt) {