Add htmx targets to forms (#36)

- Derive layout based on htmx headers
This commit is contained in:
Daniel J. Summers 2022-07-31 17:56:32 -04:00
parent 810b5d8258
commit 1547377527
15 changed files with 886 additions and 819 deletions

View File

@ -8,7 +8,7 @@ open PrayerTracker.ViewModels
let edit (m : EditChurch) ctx vi = let edit (m : EditChurch) ctx vi =
let pageTitle = if m.IsNew then "Add a New Church" else "Edit Church" let pageTitle = if m.IsNew then "Add a New Church" else "Edit Church"
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
[ form [ _action "/church/save"; _method "post"; _class "pt-center-columns" ] [ [ form [ _action "/church/save"; _method "post"; _class "pt-center-columns"; Target.content ] [
style [ _scoped ] [ style [ _scoped ] [
rawText "#name { width: 20rem; } #city { width: 10rem; } #st { width: 3rem; } #interfaceAddress { width: 30rem; }" rawText "#name { width: 20rem; } #city { width: 10rem; } #st { width: 3rem; } #interfaceAddress { width: 30rem; }"
] ]
@ -28,9 +28,9 @@ let edit (m : EditChurch) ctx vi =
input [ _type "text" input [ _type "text"
_name (nameof m.State) _name (nameof m.State)
_id "state" _id "state"
_required
_minlength "2"; _maxlength "2" _minlength "2"; _maxlength "2"
_value m.State ] _value m.State
_required ]
] ]
] ]
div [ _class "pt-field-row" ] [ div [ _class "pt-field-row" ] [
@ -90,8 +90,9 @@ let maintain (churches : Church list) (stats : Map<string, ChurchStats>) ctx vi
a [ _href $"/church/{chId}/edit"; _title s["Edit This Church"].Value ] [ icon "edit" ] a [ _href $"/church/{chId}/edit"; _title s["Edit This Church"].Value ] [ icon "edit" ]
a [ _href delAction a [ _href delAction
_title s["Delete This Church"].Value _title s["Delete This Church"].Value
_onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ] _onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ] [
[ icon "delete_forever" ] icon "delete_forever"
]
] ]
td [] [ str ch.name ] td [] [ str ch.name ]
td [] [ str ch.city; rawText ", "; str ch.st ] td [] [ str ch.city; rawText ", "; str ch.st ]
@ -104,8 +105,9 @@ let maintain (churches : Church list) (stats : Map<string, ChurchStats>) ctx vi
] ]
[ div [ _class "pt-center-text" ] [ [ div [ _class "pt-center-text" ] [
br [] br []
a [ _href $"/church/{emptyGuid}/edit"; _title s["Add a New Church"].Value ] a [ _href $"/church/{emptyGuid}/edit"; _title s["Add a New Church"].Value ] [
[ icon "add_circle"; rawText " &nbsp;"; locStr s["Add a New Church"] ] icon "add_circle"; rawText " &nbsp;"; locStr s["Add a New Church"]
]
br [] br []
br [] br []
] ]

View File

@ -113,6 +113,12 @@ let flatGuid (x : Guid) = x.ToString "N"
/// An empty GUID string (used for "add" actions) /// An empty GUID string (used for "add" actions)
let emptyGuid = flatGuid Guid.Empty let emptyGuid = flatGuid Guid.Empty
/// Create an HTML onsubmit event handler
let _onsubmit = attr "onsubmit"
/// A "rel='noopener'" attribute
let _relNoOpener = _rel "noopener"
/// The name this function used to have when the view engine was part of Giraffe /// The name this function used to have when the view engine was part of Giraffe
let renderHtmlNode = RenderView.AsString.htmlNode let renderHtmlNode = RenderView.AsString.htmlNode
@ -143,3 +149,15 @@ module TimeZones =
let name tzId (s : IStringLocalizer) = let name tzId (s : IStringLocalizer) =
try s[xref[tzId]] try s[xref[tzId]]
with :? KeyNotFoundException -> LocalizedString (tzId, tzId) with :? KeyNotFoundException -> LocalizedString (tzId, tzId)
open Giraffe.ViewEngine.Htmx
/// Known htmx targets
module Target =
/// htmx links target the body element
let body = _hxTarget "body"
/// htmx links target the #pt-body element
let content = _hxTarget "#pt-body"

View File

@ -50,7 +50,6 @@ let index vi =
let l = I18N.forView "Home/Index" let l = I18N.forView "Home/Index"
use sw = new StringWriter () use sw = new StringWriter ()
let raw = rawLocText sw let raw = rawLocText sw
[ p [] [ [ p [] [
raw l["Welcome to <strong>{0}</strong>!", s["PrayerTracker"]] raw l["Welcome to <strong>{0}</strong>!", s["PrayerTracker"]]
space space
@ -128,7 +127,6 @@ let privacyPolicy vi =
let l = I18N.forView "Home/PrivacyPolicy" let l = I18N.forView "Home/PrivacyPolicy"
use sw = new StringWriter () use sw = new StringWriter ()
let raw = rawLocText sw let raw = rawLocText sw
[ p [ _class "pt-right-text" ] [ small [] [ em [] [ raw l["(as of July 31, 2018)"] ] ] ] [ p [ _class "pt-right-text" ] [ small [] [ em [] [ raw l["(as of July 31, 2018)"] ] ] ]
p [] [ p [] [
raw l["The nature of the service is one where privacy is a must."] raw l["The nature of the service is one where privacy is a must."]
@ -150,7 +148,7 @@ let privacyPolicy vi =
rawText " &ndash; " rawText " &ndash; "
raw l["{0} stores the text of prayer requests.", s["PrayerTracker"]] raw l["{0} stores the text of prayer requests.", s["PrayerTracker"]]
space space
raw l["It also stores names and e-mail addreses of small group members, and plain-text passwords for small groups with password-protected lists."] raw l["It also stores names and e-mail addresses of small group members, and plain-text passwords for small groups with password-protected lists."]
] ]
] ]
h3 [] [ raw l["How Your Data Is Accessed / Secured"] ] h3 [] [ raw l["How Your Data Is Accessed / Secured"] ]

View File

@ -3,27 +3,13 @@ module PrayerTracker.Views.Layout
open Giraffe.ViewEngine open Giraffe.ViewEngine
open Giraffe.ViewEngine.Accessibility open Giraffe.ViewEngine.Accessibility
open Giraffe.ViewEngine.Htmx
open PrayerTracker
open PrayerTracker.ViewModels open PrayerTracker.ViewModels
open System
open System.Globalization open System.Globalization
/// Get the two-character language code for the current request /// Get the two-character language code for the current request
let langCode () = if CultureInfo.CurrentCulture.Name.StartsWith "es" then "es" else "en" let langCode () = if CultureInfo.CurrentCulture.Name.StartsWith "es" then "es" else "en"
/// Known htmx targets
module Target =
/// htmx links target the body element
let body = _hxTarget "body"
/// htmx links target the #pt-body element
let content = _hxTarget "#pt-body"
/// Navigation items /// Navigation items
module Navigation = module Navigation =
@ -31,11 +17,12 @@ module Navigation =
let top m = let top m =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let menuSpacer = rawText "&nbsp; " let menuSpacer = rawText "&nbsp; "
let _dropdown = _class "dropbtn"
let leftLinks = [ let leftLinks = [
match m.User with match m.User with
| Some u -> | Some u ->
li [ _class "dropdown" ] [ li [ _class "dropdown" ] [
a [ _class "dropbtn"; _ariaLabel s["Requests"].Value; _title s["Requests"].Value; _roleButton ] [ a [ _dropdown; _ariaLabel s["Requests"].Value; _title s["Requests"].Value; _roleButton ] [
icon "question_answer"; space; locStr s["Requests"]; space; icon "keyboard_arrow_down" icon "question_answer"; space; locStr s["Requests"]; space; icon "keyboard_arrow_down"
] ]
div [ _class "dropdown-content"; _roleMenuBar ] [ div [ _class "dropdown-content"; _roleMenuBar ] [
@ -48,7 +35,7 @@ module Navigation =
] ]
] ]
li [ _class "dropdown" ] [ li [ _class "dropdown" ] [
a [ _class "dropbtn"; _ariaLabel s["Group"].Value; _title s["Group"].Value; _roleButton ] [ a [ _dropdown; _ariaLabel s["Group"].Value; _title s["Group"].Value; _roleButton ] [
icon "group"; space; locStr s["Group"]; space; icon "keyboard_arrow_down" icon "group"; space; locStr s["Group"]; space; icon "keyboard_arrow_down"
] ]
div [ _class "dropdown-content"; _roleMenuBar ] [ div [ _class "dropdown-content"; _roleMenuBar ] [
@ -65,7 +52,7 @@ module Navigation =
] ]
if u.isAdmin then if u.isAdmin then
li [ _class "dropdown" ] [ li [ _class "dropdown" ] [
a [ _class "dropbtn" a [ _dropdown
_ariaLabel s["Administration"].Value _ariaLabel s["Administration"].Value
_title s["Administration"].Value _title s["Administration"].Value
_roleButton ] [ _roleButton ] [
@ -89,7 +76,7 @@ module Navigation =
] ]
| None -> | None ->
li [ _class "dropdown" ] [ li [ _class "dropdown" ] [
a [ _class "dropbtn"; _ariaLabel s["Log On"].Value; _title s["Log On"].Value; _roleButton ] [ a [ _dropdown; _ariaLabel s["Log On"].Value; _title s["Log On"].Value; _roleButton ] [
icon "security"; space; locStr s["Log On"]; space; icon "keyboard_arrow_down" icon "security"; space; locStr s["Log On"]; space; icon "keyboard_arrow_down"
] ]
div [ _class "dropdown-content"; _roleMenuBar ] [ div [ _class "dropdown-content"; _roleMenuBar ] [
@ -111,7 +98,7 @@ module Navigation =
_ariaLabel s["Help"].Value _ariaLabel s["Help"].Value
_title s["View Help"].Value _title s["View Help"].Value
_target "_blank" _target "_blank"
_rel "noopener" ] [ _relNoOpener ] [
icon "help"; space; locStr s["Help"] icon "help"; space; locStr s["Help"]
] ]
] ]
@ -130,10 +117,7 @@ module Navigation =
] ]
| None -> () | None -> ()
li [] [ li [] [
a [ _href "/log-off" a [ _href "/log-off"; _ariaLabel s["Log Off"].Value; _title s["Log Off"].Value; Target.body ] [
_ariaLabel s["Log Off"].Value
_title s["Log Off"].Value
_hxTarget "body" ] [
icon "power_settings_new"; space; locStr s["Log Off"] icon "power_settings_new"; space; locStr s["Log Off"]
] ]
] ]
@ -222,10 +206,11 @@ let private htmlHead m pageTitle =
yield! commonHead yield! commonHead
for cssFile in m.Style do for cssFile in m.Style do
link [ _rel "stylesheet"; _href $"/css/{cssFile}.css"; _type "text/css" ] link [ _rel "stylesheet"; _href $"/css/{cssFile}.css"; _type "text/css" ]
for jsFile in m.Script do
script [ _src $"/js/{jsFile}.js" ] []
] ]
open Giraffe.ViewEngine.Htmx
/// Render a link to the help page for the current page /// Render a link to the help page for the current page
let private helpLink link = let private helpLink link =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
@ -241,7 +226,7 @@ let private helpLink link =
/// Render the page title, and optionally a help link /// Render the page title, and optionally a help link
let private renderPageTitle m pageTitle = let private renderPageTitle m pageTitle =
h2 [ _id "pt-page-title" ] [ h2 [ _id "pt-page-title" ] [
match m.HelpLink with Some link -> Help.fullLink (langCode ()) link |> helpLink | None -> () match m.HelpLink with Some link -> PrayerTracker.Utils.Help.fullLink (langCode ()) link |> helpLink | None -> ()
locStr pageTitle locStr pageTitle
] ]
@ -268,6 +253,9 @@ let private messages m =
] ]
]) ])
open System
/// Render the <footer> at the bottom of the page /// Render the <footer> at the bottom of the page
let private htmlFooter m = let private htmlFooter m =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
@ -282,7 +270,7 @@ let private htmlFooter m =
a [ _href "https://github.com/bit-badger/PrayerTracker" a [ _href "https://github.com/bit-badger/PrayerTracker"
_title s["View source code and get technical support"].Value _title s["View source code and get technical support"].Value
_target "_blank" _target "_blank"
_rel "noopener" ] [ _relNoOpener ] [
locStr s["Source & Support"] locStr s["Source & Support"]
] ]
] ]
@ -300,32 +288,56 @@ let private htmlFooter m =
script [ _src "/js/app.js" ] [] script [ _src "/js/app.js" ] []
] ]
/// The standard layout for PrayerTracker /// The content portion of the PrayerTracker layout
let standard m pageTitle (content : XmlNode) = let private contentSection viewInfo title (content : XmlNode) = [
let s = I18N.localizer.Force () Navigation.identity viewInfo
let ttl = s[pageTitle] renderPageTitle viewInfo title
html [ _lang (langCode ()) ] [ yield! messages viewInfo
htmlHead m ttl
body [ _hxBoost ] [
Navigation.top m
div [ _id "pt-body" ] [
Navigation.identity m
renderPageTitle m ttl
yield! messages m
content content
htmlFooter m htmlFooter viewInfo
for jsFile in viewInfo.Script do
script [ _src $"/js/{jsFile}.js" ] []
]
/// The HTML head element for partial responses
let private partialHead pgTitle =
let s = I18N.localizer.Force ()
head [] [
meta [ _charset "UTF-8" ]
title [] [ locStr pgTitle; titleSep; locStr s["PrayerTracker"] ]
] ]
open Giraffe.Htmx.Common
/// The body of the PrayerTracker layout
let private pageLayout viewInfo title content =
body [ _hxBoost ] [
Navigation.top viewInfo
div [ _id "pt-body"; Target.content; _hxSwap $"{HxSwap.InnerHtml} show:window:top" ]
(contentSection viewInfo title content)
] ]
/// The standard layout(s) for PrayerTracker
let standard viewInfo pageTitle content =
let s = I18N.localizer.Force ()
let pgTitle = s[pageTitle]
html [ _lang (langCode ()) ] [
match viewInfo.Layout with
| FullPage ->
htmlHead viewInfo pgTitle
pageLayout viewInfo pgTitle content
| PartialPage ->
partialHead pgTitle
pageLayout viewInfo pgTitle content
| ContentOnly ->
partialHead pgTitle
body [] (contentSection viewInfo pgTitle content)
] ]
/// A layout with nothing but a title and content /// A layout with nothing but a title and content
let bare pageTitle content = let bare pageTitle content =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let ttl = s[pageTitle]
html [ _lang (langCode ()) ] [ html [ _lang (langCode ()) ] [
head [] [ partialHead s[pageTitle]
meta [ _charset "UTF-8" ]
title [] [ locStr ttl; titleSep; locStr s["PrayerTracker"] ]
]
body [] [ content ] body [] [ content ]
] ]

View File

@ -14,7 +14,7 @@ open PrayerTracker.ViewModels
let edit (m : EditRequest) today ctx vi = let edit (m : EditRequest) today ctx vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let pageTitle = if m.IsNew then "Add a New Request" else "Edit Request" let pageTitle = if m.IsNew then "Add a New Request" else "Edit Request"
[ form [ _action "/prayer-request/save"; _method "post"; _class "pt-center-columns" ] [ [ form [ _action "/prayer-request/save"; _method "post"; _class "pt-center-columns"; Target.content ] [
csrfToken ctx csrfToken ctx
input [ _type "hidden"; _name (nameof m.RequestId); _value (flatGuid m.RequestId) ] input [ _type "hidden"; _name (nameof m.RequestId); _value (flatGuid m.RequestId) ]
div [ _class "pt-field-row" ] [ div [ _class "pt-field-row" ] [
@ -28,8 +28,8 @@ let edit (m : EditRequest) today ctx vi =
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "requestor" ] [ locStr s["Requestor / Subject"] ] label [ _for "requestor" ] [ locStr s["Requestor / Subject"] ]
input [ _type "text" input [ _type "text"
_name (nameof m.Requestor)
_id "requestor" _id "requestor"
_name (nameof m.Requestor)
_value (defaultArg m.Requestor "") ] _value (defaultArg m.Requestor "") ]
] ]
if m.IsNew then if m.IsNew then
@ -41,7 +41,10 @@ let edit (m : EditRequest) today ctx vi =
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
div [ _class "pt-checkbox-field" ] [ div [ _class "pt-checkbox-field" ] [
br [] br []
input [ _type "checkbox"; _name (nameof m.SkipDateUpdate); _id "skipDateUpdate"; _value "True" ] input [ _type "checkbox"
_name (nameof m.SkipDateUpdate)
_id "skipDateUpdate"
_value "True" ]
label [ _for "skipDateUpdate" ] [ locStr s["Check to not update the date"] ] label [ _for "skipDateUpdate" ] [ locStr s["Check to not update the date"] ]
br [] br []
small [] [ em [] [ str (s["Typo Corrections"].Value.ToLower ()); rawText ", etc." ] ] small [] [ em [] [ str (s["Typo Corrections"].Value.ToLower ()); rawText ", etc." ] ]
@ -139,7 +142,9 @@ let lists (groups : SmallGroup list) vi =
if grp.preferences.isPublic then if grp.preferences.isPublic then
a [ _href $"/prayer-requests/{grpId}/list"; _title s["View"].Value ] [ icon "list" ] a [ _href $"/prayer-requests/{grpId}/list"; _title s["View"].Value ] [ icon "list" ]
else else
a [ _href $"/small-group/log-on/{grpId}"; _title s["Log On"].Value ] [ icon "verified_user" ] a [ _href $"/small-group/log-on/{grpId}"; _title s["Log On"].Value ] [
icon "verified_user"
]
|> List.singleton |> List.singleton
|> td [] |> td []
td [] [ str grp.church.name ] td [] [ str grp.church.name ]
@ -159,14 +164,13 @@ let maintain (m : MaintainRequests) (ctx : HttpContext) vi =
use sw = new StringWriter () use sw = new StringWriter ()
let raw = rawLocText sw let raw = rawLocText sw
let now = m.SmallGroup.localDateNow (ctx.GetService<IClock> ()) let now = m.SmallGroup.localDateNow (ctx.GetService<IClock> ())
let prefs = m.SmallGroup.preferences
let types = ReferenceList.requestTypeList s |> Map.ofList let types = ReferenceList.requestTypeList s |> Map.ofList
let updReq (req : PrayerRequest) = let updReq (req : PrayerRequest) =
if req.updateRequired now m.SmallGroup.preferences.daysToExpire m.SmallGroup.preferences.longTermUpdateWeeks then if req.updateRequired now prefs.daysToExpire prefs.longTermUpdateWeeks then "pt-request-update" else ""
"pt-request-update"
else ""
|> _class |> _class
let reqExp (req : PrayerRequest) = let reqExp (req : PrayerRequest) =
_class (if req.isExpired now m.SmallGroup.preferences.daysToExpire then "pt-request-expired" else "") _class (if req.isExpired now prefs.daysToExpire then "pt-request-expired" else "")
/// Iterate the sequence once, before we render, so we can get the count of it at the top of the table /// Iterate the sequence once, before we render, so we can get the count of it at the top of the table
let requests = let requests =
m.Requests m.Requests
@ -184,19 +188,24 @@ let maintain (m : MaintainRequests) (ctx : HttpContext) vi =
|> String.concat "" |> String.concat ""
tr [] [ tr [] [
td [] [ td [] [
a [ _href $"/prayer-request/{reqId}/edit"; _title l["Edit This Prayer Request"].Value ] a [ _href $"/prayer-request/{reqId}/edit"; _title l["Edit This Prayer Request"].Value ] [
[ icon "edit" ] icon "edit"
if req.isExpired now m.SmallGroup.preferences.daysToExpire then ]
if req.isExpired now prefs.daysToExpire then
a [ _href $"/prayer-request/{reqId}/restore" a [ _href $"/prayer-request/{reqId}/restore"
_title l["Restore This Inactive Request"].Value ] _title l["Restore This Inactive Request"].Value ] [
[ icon "visibility" ] icon "visibility"
]
else else
a [ _href $"/prayer-request/{reqId}/expire" a [ _href $"/prayer-request/{reqId}/expire"
_title l["Expire This Request Immediately"].Value ] _title l["Expire This Request Immediately"].Value ] [
[ icon "visibility_off" ] icon "visibility_off"
a [ _href delAction; _title l["Delete This Request"].Value; ]
_onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ] a [ _href delAction
[ icon "delete_forever" ] _title l["Delete This Request"].Value
_onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ] [
icon "delete_forever"
]
] ]
td [ updReq req ] [ td [ updReq req ] [
str (req.updatedDate.ToString(s["MMMM d, yyyy"].Value, Globalization.CultureInfo.CurrentUICulture)) str (req.updatedDate.ToString(s["MMMM d, yyyy"].Value, Globalization.CultureInfo.CurrentUICulture))
@ -212,19 +221,22 @@ let maintain (m : MaintainRequests) (ctx : HttpContext) vi =
|> List.ofSeq |> List.ofSeq
[ div [ _class "pt-center-text" ] [ [ div [ _class "pt-center-text" ] [
br [] br []
a [ _href $"/prayer-request/{emptyGuid}/edit"; _title s["Add a New Request"].Value ] a [ _href $"/prayer-request/{emptyGuid}/edit"; _title s["Add a New Request"].Value ] [
[ icon "add_circle"; rawText " &nbsp;"; locStr s["Add a New Request"] ] icon "add_circle"; rawText " &nbsp;"; locStr s["Add a New Request"]
]
rawText " &nbsp; &nbsp; &nbsp; " rawText " &nbsp; &nbsp; &nbsp; "
a [ _href "/prayer-requests/view"; _title s["View Prayer Request List"].Value ] a [ _href "/prayer-requests/view"; _title s["View Prayer Request List"].Value ] [
[ icon "list"; rawText " &nbsp;"; locStr s["View Prayer Request List"] ] icon "list"; rawText " &nbsp;"; locStr s["View Prayer Request List"]
]
match m.SearchTerm with match m.SearchTerm with
| Some _ -> | Some _ ->
rawText " &nbsp; &nbsp; &nbsp; " rawText " &nbsp; &nbsp; &nbsp; "
a [ _href "/prayer-requests"; _title l["Clear Search Criteria"].Value ] a [ _href "/prayer-requests"; _title l["Clear Search Criteria"].Value ] [
[ icon "highlight_off"; rawText " &nbsp;"; raw l["Clear Search Criteria"] ] icon "highlight_off"; rawText " &nbsp;"; raw l["Clear Search Criteria"]
]
| None -> () | None -> ()
] ]
form [ _action "/prayer-requests"; _method "get"; _class "pt-center-text pt-search-form" ] [ form [ _action "/prayer-requests"; _method "get"; _class "pt-center-text pt-search-form"; Target.content ] [
input [ _type "text" input [ _type "text"
_name "search" _name "search"
_placeholder l["Search requests..."].Value _placeholder l["Search requests..."].Value
@ -267,19 +279,20 @@ let maintain (m : MaintainRequests) (ctx : HttpContext) vi =
let search = [ match m.SearchTerm with Some s -> "search", s | None -> () ] let search = [ match m.SearchTerm with Some s -> "search", s | None -> () ]
let pg = defaultArg m.PageNbr 1 let pg = defaultArg m.PageNbr 1
let url = let url =
match m.OnlyActive with Some true | None -> "" | _ -> "/inactive" |> sprintf "/prayer-requests%s" match m.OnlyActive with Some true | None -> "" | _ -> "/inactive"
|> sprintf "/prayer-requests%s"
match pg with match pg with
| 1 -> () | 1 -> ()
| _ -> | _ ->
// button (_type "submit" :: attrs) [ icon ico; rawText " &nbsp;"; locStr text ] // button (_type "submit" :: attrs) [ icon ico; rawText " &nbsp;"; locStr text ]
let withPage = match pg with 2 -> search | _ -> ("page", string (pg - 1)) :: search let withPage = match pg with 2 -> search | _ -> ("page", string (pg - 1)) :: search
a [ _href (makeUrl url withPage) ] a [ _href (makeUrl url withPage) ] [ icon "keyboard_arrow_left"; space; raw l["Previous Page"] ]
[ icon "keyboard_arrow_left"; space; raw l["Previous Page"] ]
rawText " &nbsp; &nbsp; " rawText " &nbsp; &nbsp; "
match requests.Length = m.SmallGroup.preferences.pageSize with match requests.Length = m.SmallGroup.preferences.pageSize with
| true -> | true ->
a [ _href (makeUrl url (("page", string (pg + 1)) :: search)) ] a [ _href (makeUrl url (("page", string (pg + 1)) :: search)) ] [
[ raw l["Next Page"]; space; icon "keyboard_arrow_right" ] raw l["Next Page"]; space; icon "keyboard_arrow_right"
]
| false -> () | false -> ()
] ]
form [ _id "DeleteForm"; _action ""; _method "post" ] [ csrfToken ctx ] form [ _id "DeleteForm"; _action ""; _method "post" ] [ csrfToken ctx ]
@ -319,8 +332,10 @@ let view m vi =
br [] br []
a [ _class "pt-icon-link" a [ _class "pt-icon-link"
_href $"/prayer-requests/print/{dtString}" _href $"/prayer-requests/print/{dtString}"
_title s["View Printable"].Value _target "_blank"
] [ icon "print"; rawText " &nbsp;"; locStr s["View Printable"] ] _title s["View Printable"].Value ] [
icon "print"; rawText " &nbsp;"; locStr s["View Printable"]
]
if m.CanEmail then if m.CanEmail then
spacer spacer
if m.Date.DayOfWeek <> DayOfWeek.Sunday then if m.Date.DayOfWeek <> DayOfWeek.Sunday then

View File

@ -147,7 +147,7 @@
<data name="If you utilize the “{0}” box on sign in, a second cookie is stored, and transmitted to establish a session; this cookie is removed by clicking the “{1}” link." xml:space="preserve"> <data name="If you utilize the “{0}” box on sign in, a second cookie is stored, and transmitted to establish a session; this cookie is removed by clicking the “{1}” link." xml:space="preserve">
<value>Si utilizas el cuadro "{0}" al iniciar sesión, se almacena una segunda cookie y se transmite para establecer una sesión; esta cookie se elimina haciendo clic en el enlace "{1}".</value> <value>Si utilizas el cuadro "{0}" al iniciar sesión, se almacena una segunda cookie y se transmite para establecer una sesión; esta cookie se elimina haciendo clic en el enlace "{1}".</value>
</data> </data>
<data name="It also stores names and e-mail addreses of small group members, and plain-text passwords for small groups with password-protected lists." xml:space="preserve"> <data name="It also stores names and e-mail addresses of small group members, and plain-text passwords for small groups with password-protected lists." xml:space="preserve">
<value>También almacena nombres y direcciones de correo electrónico de miembros de grupos pequeños, y contraseñas de texto sin formato para grupos pequeños con listas protegidas por contraseña.</value> <value>También almacena nombres y direcciones de correo electrónico de miembros de grupos pequeños, y contraseñas de texto sin formato para grupos pequeños con listas protegidas por contraseña.</value>
</data> </data>
<data name="On the server, all data is stored in a controlled-access database." xml:space="preserve"> <data name="On the server, all data is stored in a controlled-access database." xml:space="preserve">

View File

@ -1,9 +1,7 @@
module PrayerTracker.Views.SmallGroup module PrayerTracker.Views.SmallGroup
open System.IO
open Giraffe.ViewEngine open Giraffe.ViewEngine
open Microsoft.Extensions.Localization open Microsoft.Extensions.Localization
open PrayerTracker
open PrayerTracker.Entities open PrayerTracker.Entities
open PrayerTracker.ViewModels open PrayerTracker.ViewModels
@ -12,7 +10,7 @@ let announcement isAdmin ctx vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let m = { SendToClass = ""; Text = ""; AddToRequestList = None; RequestType = None } let m = { SendToClass = ""; Text = ""; AddToRequestList = None; RequestType = None }
let reqTypes = ReferenceList.requestTypeList s let reqTypes = ReferenceList.requestTypeList s
[ form [ _action "/small-group/announcement/send"; _method "post"; _class "pt-center-columns" ] [ [ form [ _action "/small-group/announcement/send"; _method "post"; _class "pt-center-columns"; Target.content ] [
csrfToken ctx csrfToken ctx
div [ _class "pt-field-row" ] [ div [ _class "pt-field-row" ] [
div [ _class "pt-field pt-editor" ] [ div [ _class "pt-field pt-editor" ] [
@ -74,7 +72,7 @@ let announcementSent (m : Announcement) vi =
let edit (m : EditSmallGroup) (churches : Church list) ctx vi = let edit (m : EditSmallGroup) (churches : Church list) ctx vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let pageTitle = if m.IsNew then "Add a New Group" else "Edit Group" let pageTitle = if m.IsNew then "Add a New Group" else "Edit Group"
form [ _action "/small-group/save"; _method "post"; _class "pt-center-columns" ] [ form [ _action "/small-group/save"; _method "post"; _class "pt-center-columns"; Target.content ] [
csrfToken ctx csrfToken ctx
input [ _type "hidden"; _name (nameof m.SmallGroupId); _value (flatGuid m.SmallGroupId) ] input [ _type "hidden"; _name (nameof m.SmallGroupId); _value (flatGuid m.SmallGroupId) ]
div [ _class "pt-field-row" ] [ div [ _class "pt-field-row" ] [
@ -104,7 +102,7 @@ let edit (m : EditSmallGroup) (churches : Church list) ctx vi =
let editMember (m : EditMember) (types : (string * LocalizedString) seq) ctx vi = let editMember (m : EditMember) (types : (string * LocalizedString) seq) ctx vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let pageTitle = if m.IsNew then "Add a New Group Member" else "Edit Group Member" let pageTitle = if m.IsNew then "Add a New Group Member" else "Edit Group Member"
form [ _action "/small-group/member/save"; _method "post"; _class "pt-center-columns" ] [ form [ _action "/small-group/member/save"; _method "post"; _class "pt-center-columns"; Target.content ] [
style [ _scoped ] [ rawText "#name { width: 15rem; } #email { width: 20rem; }" ] style [ _scoped ] [ rawText "#name { width: 15rem; } #email { width: 20rem; }" ]
csrfToken ctx csrfToken ctx
input [ _type "hidden"; _name (nameof m.MemberId); _value (flatGuid m.MemberId) ] input [ _type "hidden"; _name (nameof m.MemberId); _value (flatGuid m.MemberId) ]
@ -137,15 +135,14 @@ let editMember (m : EditMember) (types : (string * LocalizedString) seq) ctx vi
let logOn (groups : SmallGroup list) grpId ctx vi = let logOn (groups : SmallGroup list) grpId ctx vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let m = { SmallGroupId = System.Guid.Empty; Password = ""; RememberMe = None } let m = { SmallGroupId = System.Guid.Empty; Password = ""; RememberMe = None }
[ form [ _action "/small-group/log-on/submit"; _method "post"; _class "pt-center-columns" ] [ [ form [ _action "/small-group/log-on/submit"; _method "post"; _class "pt-center-columns"; Target.body ] [
csrfToken ctx csrfToken ctx
div [ _class "pt-field-row" ] [ div [ _class "pt-field-row" ] [
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for (nameof m.SmallGroupId) ] [ locStr s["Group"] ] label [ _for (nameof m.SmallGroupId) ] [ locStr s["Group"] ]
seq { seq {
match groups.Length with if groups.Length = 0 then "", s["There are no classes with passwords defined"].Value
| 0 -> "", s["There are no classes with passwords defined"].Value else
| _ ->
"", selectDefault s["Select Group"].Value "", selectDefault s["Select Group"].Value
yield! yield!
groups groups
@ -158,8 +155,8 @@ let logOn (groups : SmallGroup list) grpId ctx vi =
input [ _type "password" input [ _type "password"
_name (nameof m.Password) _name (nameof m.Password)
_id "password" _id "password"
_required; _placeholder (s["Case-Sensitive"].Value.ToLower ())
_placeholder (s["Case-Sensitive"].Value.ToLower ()) ] _required ]
] ]
] ]
div [ _class "pt-checkbox-field" ] [ div [ _class "pt-checkbox-field" ] [
@ -200,12 +197,12 @@ let maintain (groups : SmallGroup list) ctx vi =
$"""{s["Small Group"].Value.ToLower ()} ({g.name})""" ].Value $"""{s["Small Group"].Value.ToLower ()} ({g.name})""" ].Value
tr [] [ tr [] [
td [] [ td [] [
a [ _href $"/small-group/{grpId}/edit"; _title s["Edit This Group"].Value ] a [ _href $"/small-group/{grpId}/edit"; _title s["Edit This Group"].Value ] [ icon "edit" ]
[ icon "edit" ]
a [ _href delAction a [ _href delAction
_title s["Delete This Group"].Value _title s["Delete This Group"].Value
_onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ] _onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ] [
[ icon "delete_forever" ] icon "delete_forever"
]
] ]
td [] [ str g.name ] td [] [ str g.name ]
td [] [ str g.church.name ] td [] [ str g.church.name ]
@ -216,9 +213,7 @@ let maintain (groups : SmallGroup list) ctx vi =
[ div [ _class "pt-center-text" ] [ [ div [ _class "pt-center-text" ] [
br [] br []
a [ _href $"/small-group/{emptyGuid}/edit"; _title s["Add a New Group"].Value ] [ a [ _href $"/small-group/{emptyGuid}/edit"; _title s["Add a New Group"].Value ] [
icon "add_circle" icon "add_circle"; rawText " &nbsp;"; locStr s["Add a New Group"]
rawText " &nbsp;"
locStr s["Add a New Group"]
] ]
br [] br []
br [] br []
@ -256,12 +251,14 @@ let members (members : Member list) (emailTyps : Map<string, LocalizedString>) c
.Value.Replace("?", $" ({mbr.memberName})?") .Value.Replace("?", $" ({mbr.memberName})?")
tr [] [ tr [] [
td [] [ td [] [
a [ _href $"/small-group/member/{mbrId}/edit"; _title s["Edit This Group Member"].Value ] a [ _href $"/small-group/member/{mbrId}/edit"; _title s["Edit This Group Member"].Value ] [
[ icon "edit" ] icon "edit"
]
a [ _href delAction a [ _href delAction
_title s["Delete This Group Member"].Value _title s["Delete This Group Member"].Value
_onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ] _onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ] [
[ icon "delete_forever" ] icon "delete_forever"
]
] ]
td [] [ str mbr.memberName ] td [] [ str mbr.memberName ]
td [] [ str mbr.email ] td [] [ str mbr.email ]
@ -271,8 +268,9 @@ let members (members : Member list) (emailTyps : Map<string, LocalizedString>) c
] ]
[ div [ _class"pt-center-text" ] [ [ div [ _class"pt-center-text" ] [
br [] br []
a [ _href $"/small-group/member/{emptyGuid}/edit"; _title s["Add a New Group Member"].Value ] a [ _href $"/small-group/member/{emptyGuid}/edit"; _title s["Add a New Group Member"].Value ] [
[ icon "add_circle"; rawText " &nbsp;"; locStr s["Add a New Group Member"] ] icon "add_circle"; rawText " &nbsp;"; locStr s["Add a New Group Member"]
]
br [] br []
br [] br []
] ]
@ -284,31 +282,26 @@ let members (members : Member list) (emailTyps : Map<string, LocalizedString>) c
|> Layout.standard vi "Maintain Group Members" |> Layout.standard vi "Maintain Group Members"
open Giraffe.ViewEngine.Accessibility
/// View for the small group overview page /// View for the small group overview page
let overview m vi = let overview m vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let linkSpacer = rawText "&nbsp; " let linkSpacer = rawText "&nbsp; "
let types = ReferenceList.requestTypeList s |> dict let types = ReferenceList.requestTypeList s |> dict
article [ _class "pt-overview" ] [ article [ _class "pt-overview" ] [
section [] [ section [ _ariaLabel "Quick actions" ] [
header [ _role "heading" ] [ header [ _roleHeading ] [ iconSized 72 "bookmark_border"; locStr s["Quick Actions"] ]
iconSized 72 "bookmark_border"
locStr s["Quick Actions"]
]
div [] [ div [] [
a [ _href "/prayer-requests/view" ] a [ _href "/prayer-requests/view" ] [ icon "list"; linkSpacer; locStr s["View Prayer Request List"] ]
[ icon "list"; linkSpacer; locStr s["View Prayer Request List"] ]
hr [] hr []
a [ _href "/small-group/announcement" ] [ icon "send"; linkSpacer; locStr s["Send Announcement"] ] a [ _href "/small-group/announcement" ] [ icon "send"; linkSpacer; locStr s["Send Announcement"] ]
hr [] hr []
a [ _href "/small-group/preferences" ] [ icon "build"; linkSpacer; locStr s["Change Preferences"] ] a [ _href "/small-group/preferences" ] [ icon "build"; linkSpacer; locStr s["Change Preferences"] ]
] ]
] ]
section [] [ section [ _ariaLabel "Prayer requests" ] [
header [ _role "heading" ] [ header [ _roleHeading ] [ iconSized 72 "question_answer"; locStr s["Prayer Requests"] ]
iconSized 72 "question_answer"
locStr s["Prayer Requests"]
]
div [] [ div [] [
p [ _class "pt-center-text" ] [ p [ _class "pt-center-text" ] [
strong [] [ str (m.TotalActiveReqs.ToString "N0"); space; locStr s["Active Requests"] ] strong [] [ str (m.TotalActiveReqs.ToString "N0"); space; locStr s["Active Requests"] ]
@ -325,17 +318,12 @@ let overview m vi =
locStr s["Total Requests"] locStr s["Total Requests"]
hr [] hr []
a [ _href "/prayer-requests/maintain" ] [ a [ _href "/prayer-requests/maintain" ] [
icon "compare_arrows" icon "compare_arrows"; linkSpacer; locStr s["Maintain Prayer Requests"]
linkSpacer
locStr s["Maintain Prayer Requests"]
] ]
] ]
] ]
section [] [ section [ _ariaLabel "Small group members" ] [
header [ _role "heading" ] [ header [ _roleHeading ] [ iconSized 72 "people_outline"; locStr s["Group Members"] ]
iconSized 72 "people_outline"
locStr s["Group Members"]
]
div [ _class "pt-center-text" ] [ div [ _class "pt-center-text" ] [
strong [] [ str (m.TotalMembers.ToString "N0"); space; locStr s["Members"] ] strong [] [ str (m.TotalMembers.ToString "N0"); space; locStr s["Members"] ]
hr [] hr []
@ -348,15 +336,19 @@ let overview m vi =
|> Layout.standard vi "Small Group Overview" |> Layout.standard vi "Small Group Overview"
open System.IO
open PrayerTracker
/// View for the small group preferences page /// View for the small group preferences page
let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi = let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let l = I18N.forView "SmallGroup/Preferences" let l = I18N.forView "SmallGroup/Preferences"
use sw = new StringWriter () use sw = new StringWriter ()
let raw = rawLocText sw let raw = rawLocText sw
[ form [ _action "/small-group/preferences/save"; _method "post"; _class "pt-center-columns" ] [ [ style [ _scoped ] [
style [ _scoped ] rawText "#expireDays, #daysToKeepNew, #longTermUpdateWeeks, #headingFontSize, #listFontSize, #pageSize { width: 3rem; } #emailFromAddress { width: 20rem; } #fonts { width: 40rem; } @media screen and (max-width: 40rem) { #fonts { width: 100%; } }"
[ rawText "#expireDays, #daysToKeepNew, #longTermUpdateWeeks, #headingFontSize, #listFontSize, #pageSize { width: 3rem; } #emailFromAddress { width: 20rem; } #fonts { width: 40rem; } @media screen and (max-width: 40rem) { #fonts { width: 100%; } }" ] ]
form [ _action "/small-group/preferences/save"; _method "post"; _class "pt-center-columns"; Target.content ] [
csrfToken ctx csrfToken ctx
fieldset [] [ fieldset [] [
legend [] [ strong [] [ icon "date_range"; rawText " &nbsp;"; locStr s["Dates"] ] ] legend [] [ strong [] [ icon "date_range"; rawText " &nbsp;"; locStr s["Dates"] ] ]
@ -367,11 +359,12 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi =
input [ _type "number" input [ _type "number"
_name (nameof m.ExpireDays) _name (nameof m.ExpireDays)
_id "expireDays" _id "expireDays"
_value (string m.ExpireDays)
_min "1"; _max "30" _min "1"; _max "30"
_required _required
_autofocus _autofocus ]
_value (string m.ExpireDays) ] space
space; str (s["Days"].Value.ToLower ()) str (s["Days"].Value.ToLower ())
] ]
] ]
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
@ -381,8 +374,8 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi =
_name (nameof m.DaysToKeepNew) _name (nameof m.DaysToKeepNew)
_id "daysToKeepNew" _id "daysToKeepNew"
_min "1"; _max "30" _min "1"; _max "30"
_required _value (string m.DaysToKeepNew)
_value (string m.DaysToKeepNew) ] _required ]
space; str (s["Days"].Value.ToLower ()) space; str (s["Days"].Value.ToLower ())
] ]
] ]
@ -393,8 +386,8 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi =
_name (nameof m.LongTermUpdateWeeks) _name (nameof m.LongTermUpdateWeeks)
_id "longTermUpdateWeeks" _id "longTermUpdateWeeks"
_min "1"; _max "30" _min "1"; _max "30"
_required _value (string m.LongTermUpdateWeeks)
_value (string m.LongTermUpdateWeeks) ] _required ]
space; str (s["Weeks"].Value.ToLower ()) space; str (s["Weeks"].Value.ToLower ())
] ]
] ]
@ -416,16 +409,16 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi =
input [ _type "text" input [ _type "text"
_name (nameof m.EmailFromName) _name (nameof m.EmailFromName)
_id "emailFromName" _id "emailFromName"
_required _value m.EmailFromName
_value m.EmailFromName ] _required ]
] ]
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "emailFromAddress" ] [ locStr s["From Address"] ] label [ _for "emailFromAddress" ] [ locStr s["From Address"] ]
input [ _type "email" input [ _type "email"
_name (nameof m.EmailFromAddress) _name (nameof m.EmailFromAddress)
_id "emailFromAddress" _id "emailFromAddress"
_required _value m.EmailFromAddress
_value m.EmailFromAddress ] _required ]
] ]
] ]
div [ _class "pt-field-row" ] [ div [ _class "pt-field-row" ] [
@ -450,8 +443,8 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi =
span [] [ span [] [
radio (nameof m.LineColorType) "lineColorType_Name" "Name" m.LineColorType radio (nameof m.LineColorType) "lineColorType_Name" "Name" m.LineColorType
label [ _for "lineColorType_Name" ] [ locStr s["Named Color"] ] label [ _for "lineColorType_Name" ] [ locStr s["Named Color"] ]
namedColorList (nameof m.LineColor) m.LineColor namedColorList (nameof m.LineColor) m.LineColor [
[ _id "lineColor_Select" _id "lineColor_Select"
if m.LineColor.StartsWith "#" then _disabled ] s if m.LineColor.StartsWith "#" then _disabled ] s
rawText "&nbsp; &nbsp; "; str (s["or"].Value.ToUpper ()) rawText "&nbsp; &nbsp; "; str (s["or"].Value.ToUpper ())
radio (nameof m.LineColorType) "lineColorType_RGB" "RGB" m.LineColorType radio (nameof m.LineColorType) "lineColorType_RGB" "RGB" m.LineColorType
@ -470,8 +463,8 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi =
span [] [ span [] [
radio (nameof m.HeadingColorType) "headingColorType_Name" "Name" m.HeadingColorType radio (nameof m.HeadingColorType) "headingColorType_Name" "Name" m.HeadingColorType
label [ _for "headingColorType_Name" ] [ locStr s["Named Color"] ] label [ _for "headingColorType_Name" ] [ locStr s["Named Color"] ]
namedColorList (nameof m.HeadingColor) m.HeadingColor namedColorList (nameof m.HeadingColor) m.HeadingColor [
[ _id "headingColor_Select" _id "headingColor_Select"
if m.HeadingColor.StartsWith "#" then _disabled ] s if m.HeadingColor.StartsWith "#" then _disabled ] s
rawText "&nbsp; &nbsp; "; str (s["or"].Value.ToUpper ()) rawText "&nbsp; &nbsp; "; str (s["or"].Value.ToUpper ())
radio (nameof m.HeadingColorType) "headingColorType_RGB" "RGB" m.HeadingColorType radio (nameof m.HeadingColorType) "headingColorType_RGB" "RGB" m.HeadingColorType
@ -498,8 +491,8 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi =
_name (nameof m.HeadingFontSize) _name (nameof m.HeadingFontSize)
_id "headingFontSize" _id "headingFontSize"
_min "8"; _max "24" _min "8"; _max "24"
_required _value (string m.HeadingFontSize)
_value (string m.HeadingFontSize) ] _required ]
] ]
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for "listFontSize" ] [ locStr s["List Text Size"] ] label [ _for "listFontSize" ] [ locStr s["List Text Size"] ]
@ -507,8 +500,8 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi =
_name (nameof m.ListFontSize) _name (nameof m.ListFontSize)
_id "listFontSize" _id "listFontSize"
_min "8"; _max "24" _min "8"; _max "24"
_required _value (string m.ListFontSize)
_value (string m.ListFontSize) ] _required ]
] ]
] ]
] ]
@ -527,7 +520,8 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi =
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [] [ locStr s["Request List Visibility"] ] label [] [ locStr s["Request List Visibility"] ]
span [] [ span [] [
radio (nameof m.Visibility) "viz_Public" (string RequestVisibility.``public``) (string m.Visibility) radio (nameof m.Visibility) "viz_Public" (string RequestVisibility.``public``)
(string m.Visibility)
label [ _for "viz_Public" ] [ locStr s["Public"] ] label [ _for "viz_Public" ] [ locStr s["Public"] ]
rawText " &nbsp;" rawText " &nbsp;"
radio (nameof m.Visibility) "viz_Private" (string RequestVisibility.``private``) radio (nameof m.Visibility) "viz_Private" (string RequestVisibility.``private``)
@ -556,8 +550,8 @@ let preferences (m : EditPreferences) (tzs : TimeZone list) ctx vi =
_name (nameof m.PageSize) _name (nameof m.PageSize)
_id "pageSize" _id "pageSize"
_min "10"; _max "255" _min "10"; _max "255"
_required _value (string m.PageSize)
_value (string m.PageSize) ] _required ]
] ]
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for (nameof m.AsOfDate) ] [ locStr s["“As of” Date Display"] ] label [ _for (nameof m.AsOfDate) ] [ locStr s["“As of” Date Display"] ]

View File

@ -1,14 +1,13 @@
module PrayerTracker.Views.User module PrayerTracker.Views.User
open Giraffe.ViewEngine open Giraffe.ViewEngine
open PrayerTracker.Entities
open PrayerTracker.ViewModels open PrayerTracker.ViewModels
/// View for the group assignment page /// View for the group assignment page
let assignGroups m groups curGroups ctx vi = let assignGroups m groups curGroups ctx vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let pageTitle = sprintf "%s %A" m.UserName s["Assign Groups"] let pageTitle = sprintf "%s %A" m.UserName s["Assign Groups"]
form [ _action "/user/small-groups/save"; _method "post"; _class "pt-center-columns" ] [ form [ _action "/user/small-groups/save"; _method "post"; _class "pt-center-columns"; Target.content ] [
csrfToken ctx csrfToken ctx
input [ _type "hidden"; _name (nameof m.UserId); _value (flatGuid m.UserId) ] input [ _type "hidden"; _name (nameof m.UserId); _value (flatGuid m.UserId) ]
input [ _type "hidden"; _name (nameof m.UserName); _value m.UserName ] input [ _type "hidden"; _name (nameof m.UserName); _value m.UserName ]
@ -48,10 +47,11 @@ let changePassword ctx vi =
[ p [ _class "pt-center-text" ] [ [ p [ _class "pt-center-text" ] [
locStr s["To change your password, enter your current password in the specified box below, then enter your new password twice."] locStr s["To change your password, enter your current password in the specified box below, then enter your new password twice."]
] ]
style [ _scoped ] [ rawText "#oldPassword, #newPassword, #newPasswordConfirm { width: 10rem; } "]
form [ _action "/user/password/change" form [ _action "/user/password/change"
_method "post" _method "post"
_onsubmit $"""return PT.compareValidation('newPassword','newPasswordConfirm','%A{s["The passwords do not match"]}')""" ] [ _onsubmit $"""return PT.compareValidation('newPassword','newPasswordConfirm','%A{s["The passwords do not match"]}')"""
style [ _scoped ] [ rawText "#oldPassword, #newPassword, #newPasswordConfirm { width: 10rem; } "] Target.content ] [
csrfToken ctx csrfToken ctx
div [ _class "pt-field-row" ] [ div [ _class "pt-field-row" ] [
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
@ -84,10 +84,13 @@ let edit (m : EditUser) ctx vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
let pageTitle = if m.IsNew then "Add a New User" else "Edit User" let pageTitle = if m.IsNew then "Add a New User" else "Edit User"
let pwPlaceholder = s[if m.IsNew then "" else "No change"].Value let pwPlaceholder = s[if m.IsNew then "" else "No change"].Value
[ form [ _action "/user/edit/save"; _method "post"; _class "pt-center-columns" [ style [ _scoped ]
_onsubmit $"""return PT.compareValidation('password','passwordConfirm','%A{s["The passwords do not match"]}')""" ] [
style [ _scoped ]
[ rawText "#firstName, #lastName, #password, #passwordConfirm { width: 10rem; } #email { width: 20rem; } " ] [ rawText "#firstName, #lastName, #password, #passwordConfirm { width: 10rem; } #email { width: 20rem; } " ]
form [ _action "/user/edit/save"
_method "post"
_class "pt-center-columns"
_onsubmit $"""return PT.compareValidation('password','passwordConfirm','%A{s["The passwords do not match"]}')"""
Target.content ] [
csrfToken ctx csrfToken ctx
input [ _type "hidden"; _name (nameof m.UserId); _value (flatGuid m.UserId) ] input [ _type "hidden"; _name (nameof m.UserId); _value (flatGuid m.UserId) ]
div [ _class "pt-field-row" ] [ div [ _class "pt-field-row" ] [
@ -141,7 +144,7 @@ let edit (m : EditUser) ctx vi =
/// View for the user log on page /// View for the user log on page
let logOn (m : UserLogOn) groups ctx vi = let logOn (m : UserLogOn) groups ctx vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
form [ _action "/user/log-on"; _method "post"; _class "pt-center-columns" ] [ form [ _action "/user/log-on"; _method "post"; _class "pt-center-columns"; Target.body ] [
style [ _scoped ] [ rawText "#email { width: 20rem; }" ] style [ _scoped ] [ rawText "#email { width: 20rem; }" ]
csrfToken ctx csrfToken ctx
input [ _type "hidden"; _name (nameof m.RedirectUrl); _value (defaultArg m.RedirectUrl "") ] input [ _type "hidden"; _name (nameof m.RedirectUrl); _value (defaultArg m.RedirectUrl "") ]
@ -155,17 +158,14 @@ let logOn (m : UserLogOn) groups ctx vi =
input [ _type "password" input [ _type "password"
_name (nameof m.Password) _name (nameof m.Password)
_id "password" _id "password"
_required; _placeholder $"""({s["Case-Sensitive"].Value.ToLower ()})"""
_placeholder (sprintf "(%s)" (s["Case-Sensitive"].Value.ToLower ())) ] _required ]
] ]
] ]
div [ _class "pt-field-row" ] [ div [ _class "pt-field-row" ] [
div [ _class "pt-field" ] [ div [ _class "pt-field" ] [
label [ _for (nameof m.SmallGroupId) ] [ locStr s["Group"] ] label [ _for (nameof m.SmallGroupId) ] [ locStr s["Group"] ]
seq { seq { "", selectDefault s["Select Group"].Value; yield! groups }
"", selectDefault s["Select Group"].Value
yield! groups
}
|> selectList (nameof m.SmallGroupId) "" [ _required ] |> selectList (nameof m.SmallGroupId) "" [ _required ]
] ]
] ]
@ -173,7 +173,7 @@ let logOn (m : UserLogOn) groups ctx vi =
input [ _type "checkbox"; _name (nameof m.RememberMe); _id "rememberMe"; _value "True" ] input [ _type "checkbox"; _name (nameof m.RememberMe); _id "rememberMe"; _value "True" ]
label [ _for "rememberMe" ] [ locStr s["Remember Me"] ] label [ _for "rememberMe" ] [ locStr s["Remember Me"] ]
br [] br []
small [] [ em [] [ rawText "("; str (s["Requires Cookies"].Value.ToLower ()); rawText ")" ] ] small [] [ em [] [ str $"""({s["Requires Cookies"].Value.ToLower ()})""" ] ]
] ]
div [ _class "pt-field-row" ] [ submit [] "account_circle" s["Log On"] ] div [ _class "pt-field-row" ] [ submit [] "account_circle" s["Log On"] ]
] ]
@ -182,6 +182,8 @@ let logOn (m : UserLogOn) groups ctx vi =
|> Layout.standard vi "User Log On" |> Layout.standard vi "User Log On"
open PrayerTracker.Entities
/// View for the user maintenance page /// View for the user maintenance page
let maintain (users : User list) ctx vi = let maintain (users : User list) ctx vi =
let s = I18N.localizer.Force () let s = I18N.localizer.Force ()
@ -206,12 +208,14 @@ let maintain (users : User list) ctx vi =
tr [] [ tr [] [
td [] [ td [] [
a [ _href $"/user/{userId}/edit"; _title s["Edit This User"].Value ] [ icon "edit" ] a [ _href $"/user/{userId}/edit"; _title s["Edit This User"].Value ] [ icon "edit" ]
a [ _href $"/user/{userId}/small-groups"; _title s["Assign Groups to This User"].Value ] a [ _href $"/user/{userId}/small-groups"; _title s["Assign Groups to This User"].Value ] [
[ icon "group" ] icon "group"
]
a [ _href delAction a [ _href delAction
_title s["Delete This User"].Value _title s["Delete This User"].Value
_onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ] _onclick $"return PT.confirmDelete('{delAction}','{delPrompt}')" ] [
[ icon "delete_forever" ] icon "delete_forever"
]
] ]
td [] [ str user.fullName ] td [] [ str user.fullName ]
td [ _class "pt-center-text" ] [ td [ _class "pt-center-text" ] [
@ -222,8 +226,9 @@ let maintain (users : User list) ctx vi =
] ]
[ div [ _class "pt-center-text" ] [ [ div [ _class "pt-center-text" ] [
br [] br []
a [ _href $"/user/{emptyGuid}/edit"; _title s["Add a New User"].Value ] a [ _href $"/user/{emptyGuid}/edit"; _title s["Add a New User"].Value ] [
[ icon "add_circle"; rawText " &nbsp;"; locStr s["Add a New User"] ] icon "add_circle"; rawText " &nbsp;"; locStr s["Add a New User"]
]
br [] br []
br [] br []
] ]

View File

@ -103,6 +103,18 @@ module UserMessage =
Description = None Description = None
} }
/// The template with which the content will be rendered
type LayoutType =
/// A full page load
| FullPage
/// A response that will provide a new body tag
| PartialPage
/// A response that will replace the page content
| ContentOnly
open System open System
@ -132,6 +144,9 @@ type AppViewInfo =
/// The currently logged on small group, if there is one /// The currently logged on small group, if there is one
Group : SmallGroup option Group : SmallGroup option
/// The layout with which the content will be rendered
Layout : LayoutType
} }
/// Support for the AppViewInfo type /// Support for the AppViewInfo type
@ -147,6 +162,7 @@ module AppViewInfo =
RequestStart = DateTime.Now.Ticks RequestStart = DateTime.Now.Ticks
User = None User = None
Group = None Group = None
Layout = FullPage
} }

View File

@ -18,7 +18,7 @@ let private findStats (db : AppDbContext) churchId = task {
/// POST /church/[church-id]/delete /// POST /church/[church-id]/delete
let delete churchId : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next ctx -> task { let delete churchId : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.db.TryChurchById churchId with match! ctx.db.TryChurchById churchId with
| Some church -> | Some church ->
let! _, stats = findStats ctx.db churchId let! _, stats = findStats ctx.db churchId
@ -66,7 +66,7 @@ let maintain : HttpHandler = requireAccess [ Admin ] >=> fun next ctx -> task {
/// POST /church/save /// POST /church/save
let save : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next ctx -> task { let save : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<EditChurch> () with match! ctx.TryBindFormAsync<EditChurch> () with
| Ok m -> | Ok m ->
let! church = let! church =

View File

@ -2,20 +2,7 @@
[<AutoOpen>] [<AutoOpen>]
module PrayerTracker.Handlers.CommonFunctions module PrayerTracker.Handlers.CommonFunctions
open System
open System.Net
open System.Reflection
open System.Threading.Tasks
open Giraffe
open Microsoft.AspNetCore.Antiforgery
open Microsoft.AspNetCore.Html
open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.Http.Extensions
open Microsoft.AspNetCore.Mvc.Rendering open Microsoft.AspNetCore.Mvc.Rendering
open Microsoft.Extensions.Localization
open PrayerTracker
open PrayerTracker.Cookies
open PrayerTracker.ViewModels
/// Create a select list from an enumeration /// Create a select list from an enumeration
let toSelectList<'T> valFunc textFunc withDefault emptyText (items : 'T seq) = let toSelectList<'T> valFunc textFunc withDefault emptyText (items : 'T seq) =
@ -38,7 +25,7 @@ let toSelectListWithDefault<'T> valFunc textFunc (items : 'T seq) =
/// The version of PrayerTracker /// The version of PrayerTracker
let appVersion = let appVersion =
let v = Assembly.GetExecutingAssembly().GetName().Version let v = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version
#if (DEBUG) #if (DEBUG)
$"v{v}" $"v{v}"
#else #else
@ -53,6 +40,10 @@ let appVersion =
|> String.concat "" |> String.concat ""
#endif #endif
open Microsoft.AspNetCore.Http
open PrayerTracker
/// The currently signed-in user (will raise if none exists) /// The currently signed-in user (will raise if none exists)
let currentUser (ctx : HttpContext) = let currentUser (ctx : HttpContext) =
match ctx.Session.user with Some u -> u | None -> nullArg "User" match ctx.Session.user with Some u -> u | None -> nullArg "User"
@ -61,6 +52,12 @@ let currentUser (ctx : HttpContext) =
let currentGroup (ctx : HttpContext) = let currentGroup (ctx : HttpContext) =
match ctx.Session.smallGroup with Some g -> g | None -> nullArg "SmallGroup" match ctx.Session.smallGroup with Some g -> g | None -> nullArg "SmallGroup"
open System
open Giraffe
open PrayerTracker.Cookies
open PrayerTracker.ViewModels
/// Create the common view information heading /// Create the common view information heading
let viewInfo (ctx : HttpContext) startTicks = let viewInfo (ctx : HttpContext) startTicks =
let msg = let msg =
@ -84,12 +81,18 @@ let viewInfo (ctx : HttpContext) startTicks =
CookieOptions (Expires = Nullable<DateTimeOffset> (DateTimeOffset (DateTime timeout.Until)), CookieOptions (Expires = Nullable<DateTimeOffset> (DateTimeOffset (DateTime timeout.Until)),
HttpOnly = true)) HttpOnly = true))
| None -> () | None -> ()
let layout =
match ctx.TryGetRequestHeader "X-Target" with
| Some hdr when hdr = "#pt-body" -> ContentOnly
| Some _ -> PartialPage
| None -> FullPage
{ AppViewInfo.fresh with { AppViewInfo.fresh with
Version = appVersion Version = appVersion
Messages = msg Messages = msg
RequestStart = startTicks RequestStart = startTicks
User = ctx.Session.user User = ctx.Session.user
Group = ctx.Session.smallGroup Group = ctx.Session.smallGroup
Layout = layout
} }
/// The view is the last parameter, so it can be composed /// The view is the last parameter, so it can be composed
@ -107,23 +110,24 @@ let fourOhFour next (ctx : HttpContext) =
ctx.SetStatusCode 404 ctx.SetStatusCode 404
text "Not Found" next ctx text "Not Found" next ctx
/// Handler to validate CSRF prevention token /// Handler to validate CSRF prevention token
let validateCSRF : HttpHandler = fun next ctx -> task { let validateCsrf : HttpHandler = fun next ctx -> task {
match! (ctx.GetService<IAntiforgery> ()).IsRequestValidAsync ctx with match! (ctx.GetService<Microsoft.AspNetCore.Antiforgery.IAntiforgery> ()).IsRequestValidAsync ctx with
| true -> return! next ctx | true -> return! next ctx
| false -> | false -> return! (clearResponse >=> setStatusCode 400 >=> text "Quit hacking...") earlyReturn ctx
return! (clearResponse >=> setStatusCode 400 >=> text "Quit hacking...") (fun _ -> Task.FromResult None) ctx
} }
/// Add a message to the session /// Add a message to the session
let addUserMessage (ctx : HttpContext) msg = let addUserMessage (ctx : HttpContext) msg =
ctx.Session.messages <- msg :: ctx.Session.messages ctx.Session.messages <- msg :: ctx.Session.messages
open Microsoft.AspNetCore.Html
open Microsoft.Extensions.Localization
/// Convert a localized string to an HTML string /// Convert a localized string to an HTML string
let htmlLocString (x : LocalizedString) = let htmlLocString (x : LocalizedString) =
(WebUtility.HtmlEncode >> HtmlString) x.Value (System.Net.WebUtility.HtmlEncode >> HtmlString) x.Value
let htmlString (x : LocalizedString) = let htmlString (x : LocalizedString) =
HtmlString x.Value HtmlString x.Value
@ -157,6 +161,8 @@ type AccessLevel =
| Public | Public
open Microsoft.AspNetCore.Http.Extensions
/// Require the given access role (also refreshes "Remember Me" user and group logons) /// Require the given access role (also refreshes "Remember Me" user and group logons)
let requireAccess level : HttpHandler = let requireAccess level : HttpHandler =

View File

@ -94,7 +94,7 @@ let email date : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task {
/// POST /prayer-request/[request-id]/delete /// POST /prayer-request/[request-id]/delete
let delete reqId : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> fun next ctx -> task { let delete reqId : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
match! findRequest ctx reqId with match! findRequest ctx reqId with
| Ok req -> | Ok req ->
let s = Views.I18N.localizer.Force () let s = Views.I18N.localizer.Force ()
@ -215,7 +215,7 @@ let restore reqId : HttpHandler = requireAccess [ User ] >=> fun next ctx -> tas
/// POST /prayer-request/save /// POST /prayer-request/save
let save : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> fun next ctx -> task { let save : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<EditRequest> () with match! ctx.TryBindFormAsync<EditRequest> () with
| Ok m -> | Ok m ->
let! req = let! req =

View File

@ -26,6 +26,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Giraffe" Version="6.0.0" /> <PackageReference Include="Giraffe" Version="6.0.0" />
<PackageReference Include="Giraffe.Htmx" Version="1.8.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.5" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.5" />
<PackageReference Update="FSharp.Core" Version="6.0.5" /> <PackageReference Update="FSharp.Core" Version="6.0.5" />
</ItemGroup> </ItemGroup>

View File

@ -27,7 +27,7 @@ let announcement : HttpHandler = requireAccess [ User ] >=> fun next ctx ->
/// POST /small-group/[group-id]/delete /// POST /small-group/[group-id]/delete
let delete groupId : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next ctx -> task { let delete groupId : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
let s = Views.I18N.localizer.Force () let s = Views.I18N.localizer.Force ()
match! ctx.db.TryGroupById groupId with match! ctx.db.TryGroupById groupId with
| Some grp -> | Some grp ->
@ -44,7 +44,7 @@ let delete groupId : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=>
/// POST /small-group/member/[member-id]/delete /// POST /small-group/member/[member-id]/delete
let deleteMember memberId : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> fun next ctx -> task { let deleteMember memberId : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
let s = Views.I18N.localizer.Force () let s = Views.I18N.localizer.Force ()
match! ctx.db.TryMemberById memberId with match! ctx.db.TryMemberById memberId with
| Some mbr when mbr.smallGroupId = (currentGroup ctx).smallGroupId -> | Some mbr when mbr.smallGroupId = (currentGroup ctx).smallGroupId ->
@ -113,7 +113,7 @@ let logOn (groupId : SmallGroupId option) : HttpHandler = requireAccess [ Access
/// POST /small-group/log-on/submit /// POST /small-group/log-on/submit
let logOnSubmit : HttpHandler = requireAccess [ AccessLevel.Public ] >=> validateCSRF >=> fun next ctx -> task { let logOnSubmit : HttpHandler = requireAccess [ AccessLevel.Public ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<GroupLogOn> () with match! ctx.TryBindFormAsync<GroupLogOn> () with
| Ok m -> | Ok m ->
let s = Views.I18N.localizer.Force () let s = Views.I18N.localizer.Force ()
@ -193,7 +193,7 @@ let preferences : HttpHandler = requireAccess [ User ] >=> fun next ctx -> task
/// POST /small-group/save /// POST /small-group/save
let save : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next ctx -> task { let save : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<EditSmallGroup> () with match! ctx.TryBindFormAsync<EditSmallGroup> () with
| Ok m -> | Ok m ->
let s = Views.I18N.localizer.Force () let s = Views.I18N.localizer.Force ()
@ -218,7 +218,7 @@ let save : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next c
/// POST /small-group/member/save /// POST /small-group/member/save
let saveMember : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> fun next ctx -> task { let saveMember : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<EditMember> () with match! ctx.TryBindFormAsync<EditMember> () with
| Ok m -> | Ok m ->
let grp = currentGroup ctx let grp = currentGroup ctx
@ -246,7 +246,7 @@ let saveMember : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> fun n
/// POST /small-group/preferences/save /// POST /small-group/preferences/save
let savePreferences : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> fun next ctx -> task { let savePreferences : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<EditPreferences> () with match! ctx.TryBindFormAsync<EditPreferences> () with
| Ok m -> | Ok m ->
// Since the class is stored in the session, we'll use an intermediate instance to persist it; once that works, // Since the class is stored in the session, we'll use an intermediate instance to persist it; once that works,
@ -268,7 +268,7 @@ let savePreferences : HttpHandler = requireAccess [ User ] >=> validateCSRF >=>
/// POST /small-group/announcement/send /// POST /small-group/announcement/send
let sendAnnouncement : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> fun next ctx -> task { let sendAnnouncement : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
let startTicks = DateTime.Now.Ticks let startTicks = DateTime.Now.Ticks
match! ctx.TryBindFormAsync<Announcement> () with match! ctx.TryBindFormAsync<Announcement> () with
| Ok m -> | Ok m ->

View File

@ -44,7 +44,7 @@ let private findUserByPassword m (db : AppDbContext) = task {
/// POST /user/password/change /// POST /user/password/change
let changePassword : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> fun next ctx -> task { let changePassword : HttpHandler = requireAccess [ User ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<ChangePassword> () with match! ctx.TryBindFormAsync<ChangePassword> () with
| Ok m -> | Ok m ->
let s = Views.I18N.localizer.Force () let s = Views.I18N.localizer.Force ()
@ -81,7 +81,7 @@ let changePassword : HttpHandler = requireAccess [ User ] >=> validateCSRF >=> f
/// POST /user/[user-id]/delete /// POST /user/[user-id]/delete
let delete userId : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next ctx -> task { let delete userId : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.db.TryUserById userId with match! ctx.db.TryUserById userId with
| Some user -> | Some user ->
ctx.db.RemoveEntry user ctx.db.RemoveEntry user
@ -94,7 +94,7 @@ let delete userId : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> f
/// POST /user/log-on /// POST /user/log-on
let doLogOn : HttpHandler = requireAccess [ AccessLevel.Public ] >=> validateCSRF >=> fun next ctx -> task { let doLogOn : HttpHandler = requireAccess [ AccessLevel.Public ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<UserLogOn> () with match! ctx.TryBindFormAsync<UserLogOn> () with
| Ok m -> | Ok m ->
let s = Views.I18N.localizer.Force () let s = Views.I18N.localizer.Force ()
@ -192,7 +192,7 @@ let password : HttpHandler = requireAccess [ User ] >=> fun next ctx ->
/// POST /user/save /// POST /user/save
let save : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next ctx -> task { let save : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<EditUser> () with match! ctx.TryBindFormAsync<EditUser> () with
| Ok m -> | Ok m ->
let! user = let! user =
@ -235,7 +235,7 @@ let save : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next c
/// POST /user/small-groups/save /// POST /user/small-groups/save
let saveGroups : HttpHandler = requireAccess [ Admin ] >=> validateCSRF >=> fun next ctx -> task { let saveGroups : HttpHandler = requireAccess [ Admin ] >=> validateCsrf >=> fun next ctx -> task {
match! ctx.TryBindFormAsync<AssignGroups> () with match! ctx.TryBindFormAsync<AssignGroups> () with
| Ok m -> | Ok m ->
let s = Views.I18N.localizer.Force () let s = Views.I18N.localizer.Force ()