WIP on fixi implementation

This commit is contained in:
Daniel J. Summers 2025-01-30 15:51:52 -05:00
parent 7fb1eca2a3
commit 5240b78487
13 changed files with 238 additions and 134 deletions

View File

@ -5,7 +5,6 @@ open System.Threading.Tasks
open Microsoft.Extensions.Caching.Distributed open Microsoft.Extensions.Caching.Distributed
open NodaTime open NodaTime
open Npgsql open Npgsql
open Npgsql.FSharp
/// Helper types and functions for the cache /// Helper types and functions for the cache
[<AutoOpen>] [<AutoOpen>]

View File

@ -3,15 +3,13 @@ module PrayerTracker.Views.CommonFunctions
open System.IO open System.IO
open System.Text.Encodings.Web open System.Text.Encodings.Web
open Giraffe
open Giraffe.ViewEngine open Giraffe.ViewEngine
open Microsoft.AspNetCore.Antiforgery
open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.Mvc.Localization open Microsoft.AspNetCore.Mvc.Localization
open Microsoft.Extensions.Localization open Microsoft.Extensions.Localization
/// Encoded text for a localized string /// Encoded text for a localized string
let locStr (text : LocalizedString) = str text.Value let locStr (text: LocalizedString) =
str text.Value
/// Raw text for a localized HTML string /// Raw text for a localized HTML string
let rawLocText (writer: StringWriter) (text: LocalizedHtmlString) = let rawLocText (writer: StringWriter) (text: LocalizedHtmlString) =
@ -24,10 +22,17 @@ let rawLocText (writer : StringWriter) (text : LocalizedHtmlString) =
let space = rawText " " let space = rawText " "
/// Generate a Material Design icon /// Generate a Material Design icon
let icon name = i [ _class "material-icons" ] [ rawText name ] let icon name =
i [ _class "material-icons" ] [ rawText name ]
/// Generate a Material Design icon, specifying the point size (must be defined in CSS) /// Generate a Material Design icon, specifying the point size (must be defined in CSS)
let iconSized size name = i [ _class $"material-icons md-%i{size}" ] [ rawText name ] let iconSized size name =
i [ _class $"material-icons md-%i{size}" ] [ rawText name ]
open Giraffe
open Microsoft.AspNetCore.Antiforgery
open Microsoft.AspNetCore.Http
/// Generate a CSRF prevention token /// Generate a CSRF prevention token
let csrfToken (ctx: HttpContext) = let csrfToken (ctx: HttpContext) =
@ -100,7 +105,7 @@ let colorToHex (color : string) =
| "yellow" -> "#ffff00" | "yellow" -> "#ffff00"
| it -> it | it -> it
/// Generate an input[type=radio] that is selected if its value is the current value /// <summary>Generate an <c>input type=radio</c> that is selected if its value is the current value</summary>
let radio name domId value current = let radio name domId value current =
input [ _type "radio" input [ _type "radio"
_name name _name name
@ -108,7 +113,7 @@ let radio name domId value current =
_value value _value value
if value = current then _checked ] if value = current then _checked ]
/// Generate a select list with the current value selected /// <summary>Generate a <c>select</c> list with the current value selected</summary>
let selectList name selected attrs items = let selectList name selected attrs items =
items items
|> Seq.map (fun (value, text) -> |> Seq.map (fun (value, text) ->
@ -119,16 +124,18 @@ let selectList name selected attrs items =
|> List.ofSeq |> List.ofSeq
|> select (List.concat [ [ _name name; _id name ]; attrs ]) |> select (List.concat [ [ _name name; _id name ]; attrs ])
/// Generate the text for a default entry at the top of a select list /// <summary>Generate the text for a default entry at the top of a <c>select</c> list</summary>
let selectDefault text = $"— %s{text} " let selectDefault text =
$"— %s{text} "
/// Generate a standard submit button with icon and text /// <summary>Generate a standard <c>button type=submit</c> with icon and text</summary>
let submit attrs ico text = button (_type "submit" :: attrs) [ icon ico; rawText " &nbsp;"; locStr text ] let submit attrs ico text =
button (_type "submit" :: attrs) [ icon ico; rawText " &nbsp;"; locStr text ]
/// Create an HTML onsubmit event handler /// Create an HTML onsubmit event handler
let _onsubmit = attr "onsubmit" let _onsubmit = attr "onsubmit"
/// A "rel='noopener'" attribute /// <summary>A <c>rel="noopener"</c> attribute</summary>
let _relNoOpener = _rel "noopener" let _relNoOpener = _rel "noopener"
/// A class attribute that designates a row of fields, with the additional classes passed /// A class attribute that designates a row of fields, with the additional classes passed
@ -153,7 +160,9 @@ let _checkboxField = _class "pt-checkbox-field"
/// A group of related fields, inputs, links, etc., displayed in a row /// A group of related fields, inputs, links, etc., displayed in a row
let _group = _class "pt-group" let _group = _class "pt-group"
/// Create an input field of the given type, with matching name and ID and the given value /// <summary>
/// Create an <c>input</c> field of the given <c>type</c>, with matching name and ID and the given value
/// </summary>
let inputField typ name value attrs = let inputField typ name value attrs =
List.concat [ [ _type typ; _name name; _id name; if value <> "" then _value value ]; attrs ] |> input List.concat [ [ _type typ; _name name; _id name; if value <> "" then _value value ]; attrs ] |> input
@ -166,12 +175,20 @@ let tableHeadings (s : IStringLocalizer) (headings : string list) =
|> thead [] |> thead []
/// For a list of strings, prepend a pound sign and string them together with commas (CSS selector by ID) /// For a list of strings, prepend a pound sign and string them together with commas (CSS selector by ID)
let toHtmlIds it = it |> List.map (fun x -> $"#%s{x}") |> String.concat ", " let toHtmlIds it =
it |> List.map (fun x -> $"#%s{x}") |> String.concat ", "
/// 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
open Giraffe.Fixi
/// Create a page link that will make the request with fixi
let pageLink href attrs content =
a (List.append [ _href href; _fxGet; _fxAction href; _fxTarget "#pt-body" ] attrs) content
open Microsoft.AspNetCore.Html open Microsoft.AspNetCore.Html
/// Render an HTML node, then return the value as an HTML string /// Render an HTML node, then return the value as an HTML string

View File

@ -15,7 +15,7 @@ let setUpFactories fac =
htmlLocFactory <- HtmlLocalizerFactory stringLocFactory htmlLocFactory <- HtmlLocalizerFactory stringLocFactory
/// An instance of the common string localizer /// An instance of the common string localizer
let localizer = lazy (stringLocFactory.Create ("Common", resAsmName)) let localizer = lazy stringLocFactory.Create("Common", resAsmName)
/// Get a view localizer /// Get a view localizer
let forView (view: string) = let forView (view: string) =

View File

@ -25,20 +25,25 @@ module Navigation =
a [ _dropdown; _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 ] [
a [ _href "/prayer-requests"; _roleMenuItem ] [ pageLink "/prayer-requests"
icon "compare_arrows"; menuSpacer; locStr s["Maintain"] ] [ _roleMenuItem ]
a [ _href "/prayer-requests/view"; _roleMenuItem ] [ [ icon "compare_arrows"; menuSpacer; locStr s["Maintain"] ]
icon "list"; menuSpacer; locStr s["View List"] ] ] ] pageLink "/prayer-requests/view"
[ _roleMenuItem ]
[ icon "list"; menuSpacer; locStr s["View List"] ] ] ]
li [ _class "dropdown" ] [ li [ _class "dropdown" ] [
a [ _dropdown; _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 ] [
a [ _href "/small-group/members"; _roleMenuItem ] [ pageLink "/small-group/members"
icon "email"; menuSpacer; locStr s["Maintain Group Members"] ] [ _roleMenuItem ]
a [ _href "/small-group/announcement"; _roleMenuItem ] [ [ icon "email"; menuSpacer; locStr s["Maintain Group Members"] ]
icon "send"; menuSpacer; locStr s["Send Announcement"] ] pageLink "/small-group/announcement"
a [ _href "/small-group/preferences"; _roleMenuItem ] [ [ _roleMenuItem ]
icon "build"; menuSpacer; locStr s["Change Preferences"] ] ] ] [ icon "send"; menuSpacer; locStr s["Send Announcement"] ]
pageLink "/small-group/preferences"
[ _roleMenuItem ]
[ icon "build"; menuSpacer; locStr s["Change Preferences"] ] ] ]
if u.IsAdmin then if u.IsAdmin then
li [ _class "dropdown" ] [ li [ _class "dropdown" ] [
a [ _dropdown a [ _dropdown
@ -47,31 +52,31 @@ module Navigation =
_roleButton ] [ _roleButton ] [
icon "settings"; space; locStr s["Administration"]; space; icon "keyboard_arrow_down" ] icon "settings"; space; locStr s["Administration"]; space; icon "keyboard_arrow_down" ]
div [ _class "dropdown-content"; _roleMenuBar ] [ div [ _class "dropdown-content"; _roleMenuBar ] [
a [ _href "/churches"; _roleMenuItem ] [ icon "home"; menuSpacer; locStr s["Churches"] ] pageLink "/churches" [ _roleMenuItem ] [ icon "home"; menuSpacer; locStr s["Churches"] ]
a [ _href "/small-groups"; _roleMenuItem ] [ pageLink "/small-groups"
icon "send"; menuSpacer; locStr s["Groups"] ] [ _roleMenuItem ]
a [ _href "/users"; _roleMenuItem ] [ icon "build"; menuSpacer; locStr s["Users"] ] ] ] [ icon "send"; menuSpacer; locStr s["Groups"] ]
pageLink "/users" [ _roleMenuItem ] [ icon "build"; menuSpacer; locStr s["Users"] ] ] ]
| None -> | None ->
match m.Group with match m.Group with
| Some _ -> | Some _ ->
li [] [ li [] [
a [ _href "/prayer-requests/view" pageLink "/prayer-requests/view"
_ariaLabel s["View Request List"].Value [ _ariaLabel s["View Request List"].Value; _title s["View Request List"].Value ]
_title s["View Request List"].Value ] [ [ icon "list"; space; locStr s["View Request List"] ] ]
icon "list"; space; locStr s["View Request List"] ] ]
| None -> | None ->
li [ _class "dropdown" ] [ li [ _class "dropdown" ] [
a [ _dropdown; _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 ] [
a [ _href "/user/log-on"; _roleMenuItem ] [ icon "person"; menuSpacer; locStr s["User"] ] pageLink "/user/log-on" [ _roleMenuItem ] [ icon "person"; menuSpacer; locStr s["User"] ]
a [ _href "/small-group/log-on"; _roleMenuItem ] [ pageLink "/small-group/log-on"
icon "group"; menuSpacer; locStr s["Group"] ] ] ] [ _roleMenuItem ]
[ icon "group"; menuSpacer; locStr s["Group"] ] ] ]
li [] [ li [] [
a [ _href "/prayer-requests/lists" pageLink "/prayer-requests/lists"
_ariaLabel s["View Request List"].Value [ _ariaLabel s["View Request List"].Value; _title s["View Request List"].Value ]
_title s["View Request List"].Value ] [ [ icon "list"; space; locStr s["View Request List"] ] ]
icon "list"; space; locStr s["View Request List"] ] ]
li [] [ li [] [
a [ _href "/help"; _ariaLabel s["Help"].Value; _title s["View Help"].Value; _target "_blank" ] [ a [ _href "/help"; _ariaLabel s["Help"].Value; _title s["View Help"].Value; _target "_blank" ] [
icon "help"; space; locStr s["Help"] ] ] ] icon "help"; space; locStr s["Help"] ] ] ]
@ -81,19 +86,19 @@ module Navigation =
match m.User with match m.User with
| Some _ -> | Some _ ->
li [] [ li [] [
a [ _href "/user/password" pageLink "/user/password"
_ariaLabel s["Change Your Password"].Value [ _ariaLabel s["Change Your Password"].Value; _title s["Change Your Password"].Value ]
_title s["Change Your Password"].Value ] [ [ icon "lock"; space; locStr s["Change Your Password"] ] ]
icon "lock"; space; locStr s["Change Your Password"] ] ]
| None -> () | None -> ()
li [] [ li [] [
a [ _href "/log-off"; _ariaLabel s["Log Off"].Value; _title s["Log Off"].Value; Target.body ] [ pageLink "/log-off"
icon "power_settings_new"; space; locStr s["Log Off"] ] ] ] [ _ariaLabel s["Log Off"].Value; _title s["Log Off"].Value; Target.body ]
[ icon "power_settings_new"; space; locStr s["Log Off"] ] ] ]
| None -> [] | None -> []
header [ _class "pt-title-bar"; Target.content ] [ header [ _class "pt-title-bar"; Target.content ] [
section [ _class "pt-title-bar-left"; _ariaLabel "Left side of top menu" ] [ section [ _class "pt-title-bar-left"; _ariaLabel "Left side of top menu" ] [
span [ _class "pt-title-bar-home" ] [ span [ _class "pt-title-bar-home" ] [
a [ _href "/"; _title s["Home"].Value ] [ locStr s["PrayerTracker"] ] ] pageLink "/" [ _title s["Home"].Value ] [ locStr s["PrayerTracker"] ] ]
ul [] leftLinks ] ul [] leftLinks ]
section [ _class "pt-title-bar-center"; _ariaLabel "Empty center space in top menu" ] [] section [ _class "pt-title-bar-center"; _ariaLabel "Empty center space in top menu" ] []
section [ _class "pt-title-bar-right"; _roleToolBar; _ariaLabel "Right side of top menu" ] [ section [ _class "pt-title-bar-right"; _roleToolBar; _ariaLabel "Right side of top menu" ] [
@ -109,11 +114,11 @@ module Navigation =
| "es" -> | "es" ->
strong [] [ locStr s["Spanish"] ] strong [] [ locStr s["Spanish"] ]
rawText " &nbsp; &nbsp; " rawText " &nbsp; &nbsp; "
a [ _href "/language/en" ] [ locStr s["Change to English"] ] pageLink "/language/en" [] [ locStr s["Change to English"] ]
| _ -> | _ ->
strong [] [ locStr s["English"] ] strong [] [ locStr s["English"] ]
rawText " &nbsp; &nbsp; " rawText " &nbsp; &nbsp; "
a [ _href "/language/es" ] [ locStr s["Cambie a Español"] ] ] pageLink "/language/es" [] [ locStr s["Cambie a Español"] ] ]
match m.Group with match m.Group with
| Some g -> | Some g ->
[ match m.User with [ match m.User with
@ -129,7 +134,7 @@ module Navigation =
icon "group" icon "group"
space space
match m.User with match m.User with
| Some _ -> a [ _href "/small-group"; Target.content ] [ strong [] [ str g.Name ] ] | Some _ -> pageLink "/small-group" [] [ strong [] [ str g.Name ] ]
| None -> strong [] [ str g.Name ] ] | None -> strong [] [ str g.Name ] ]
| None -> [] | None -> []
|> div [] ] |> div [] ]
@ -153,7 +158,7 @@ let private commonHead = [
meta [ _name "viewport"; _content "width=device-width, initial-scale=1" ] meta [ _name "viewport"; _content "width=device-width, initial-scale=1" ]
meta [ _name "generator"; _content "Giraffe" ] meta [ _name "generator"; _content "Giraffe" ]
link [ _rel "stylesheet"; _href "https://fonts.googleapis.com/icon?family=Material+Icons" ] link [ _rel "stylesheet"; _href "https://fonts.googleapis.com/icon?family=Material+Icons" ]
link [ _rel "stylesheet"; _href "/css/app.css" ] ] link [ _rel "stylesheet"; _href "/_/app.css" ] ]
/// Render the <head> portion of the page /// Render the <head> portion of the page
let private htmlHead viewInfo pgTitle = let private htmlHead viewInfo pgTitle =
@ -163,19 +168,16 @@ let private htmlHead viewInfo pgTitle =
title [] [ locStr pgTitle; titleSep; locStr s["PrayerTracker"] ] title [] [ locStr pgTitle; titleSep; locStr s["PrayerTracker"] ]
yield! commonHead yield! commonHead
for cssFile in viewInfo.Style do for cssFile in viewInfo.Style do
link [ _rel "stylesheet"; _href $"/css/{cssFile}.css"; _type "text/css" ] ] link [ _rel "stylesheet"; _href $"/_/{cssFile}.css"; _type "text/css" ] ]
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()
sup [ _class "pt-help-link" ] [ sup [ _class "pt-help-link" ] [
a [ _href link a [ _href link
_title s["Click for Help on This Page"].Value _title s["Click for Help on This Page"].Value
_onclick $"return PT.showHelp('{link}')" _onclick $"return PT.showHelp('{link}')" ] [ iconSized 18 "help_outline" ] ]
_hxNoBoost ] [ iconSized 18 "help_outline" ] ]
/// Render the page title, and optionally a help link /// Render the page title, and optionally a help link
let private renderPageTitle viewInfo pgTitle = let private renderPageTitle viewInfo pgTitle =
@ -217,9 +219,9 @@ let private htmlFooter viewInfo =
let resultTime = (SystemClock.Instance.GetCurrentInstant() - viewInfo.RequestStart).TotalSeconds let resultTime = (SystemClock.Instance.GetCurrentInstant() - viewInfo.RequestStart).TotalSeconds
footer [ _class "pt-footer" ] [ footer [ _class "pt-footer" ] [
div [ _id "pt-legal" ] [ div [ _id "pt-legal" ] [
a [ _href "/legal/privacy-policy" ] [ locStr s["Privacy Policy"] ] pageLink "/legal/privacy-policy" [] [ locStr s["Privacy Policy"] ]
rawText " &nbsp; " rawText " &nbsp; "
a [ _href "/legal/terms-of-service" ] [ locStr s["Terms of Service"] ] pageLink "/legal/terms-of-service" [] [ locStr s["Terms of Service"] ]
rawText " &nbsp; " rawText " &nbsp; "
a [ _href "https://git.bitbadger.solutions/bit-badger/PrayerTracker" a [ _href "https://git.bitbadger.solutions/bit-badger/PrayerTracker"
_title s["View source code and get technical support"].Value _title s["View source code and get technical support"].Value
@ -227,7 +229,7 @@ let private htmlFooter viewInfo =
_relNoOpener ] [ _relNoOpener ] [
locStr s["Source & Support"] ] ] locStr s["Source & Support"] ] ]
div [ _id "pt-footer" ] [ div [ _id "pt-footer" ] [
a [ _href "/"; _style "line-height:28px;" ] [ pageLink "/" [ _style "line-height:28px;" ] [
img [ _src $"""/img/%O{s["footer_en"]}.png""" img [ _src $"""/img/%O{s["footer_en"]}.png"""
_alt imgText _alt imgText
_title imgText _title imgText
@ -268,19 +270,16 @@ let private partialHead pgTitle =
meta [ _charset "UTF-8" ] meta [ _charset "UTF-8" ]
title [] [ locStr pgTitle; titleSep; locStr s["PrayerTracker"] ] ] title [] [ locStr pgTitle; titleSep; locStr s["PrayerTracker"] ] ]
open Giraffe.Htmx.Common
/// The body of the PrayerTracker layout /// The body of the PrayerTracker layout
let private pageLayout viewInfo pgTitle content = let private pageLayout viewInfo pgTitle content =
body [ _hxBoost ] [ body [] [
Navigation.top viewInfo Navigation.top viewInfo
div [ _id "pt-body"; Target.content; _hxSwap $"{HxSwap.InnerHtml} show:window:top" ] div [ _id "pt-body" ] (contentSection viewInfo pgTitle content)
(contentSection viewInfo pgTitle content)
match viewInfo.Layout with match viewInfo.Layout with
| FullPage -> | FullPage ->
Script.minified
script [ _src "/js/ckeditor/ckeditor.js" ] [] script [ _src "/js/ckeditor/ckeditor.js" ] []
script [ _src "/js/app.js" ] [] script [ _src "/_/fixi-0.5.7.js" ] []
script [ _src "/_/app.js" ] []
| _ -> () ] | _ -> () ]
/// The standard layout(s) for PrayerTracker /// The standard layout(s) for PrayerTracker
@ -316,8 +315,8 @@ let help pageTitle isHome content =
meta [ _name "viewport"; _content "width=device-width, initial-scale=1" ] meta [ _name "viewport"; _content "width=device-width, initial-scale=1" ]
title [] [ locStr pgTitle; titleSep; locStr s["PrayerTracker Help"] ] title [] [ locStr pgTitle; titleSep; locStr s["PrayerTracker Help"] ]
link [ _href "https://fonts.googleapis.com/icon?family=Material+Icons"; _rel "stylesheet" ] link [ _href "https://fonts.googleapis.com/icon?family=Material+Icons"; _rel "stylesheet" ]
link [ _href "/css/app.css"; _rel "stylesheet" ] link [ _href "/_/app.css"; _rel "stylesheet" ]
link [ _href "/css/help.css"; _rel "stylesheet" ] ] link [ _href "/_/help.css"; _rel "stylesheet" ] ]
body [] [ body [] [
header [ _class "pt-title-bar" ] [ header [ _class "pt-title-bar" ] [
section [ _class "pt-title-bar-left" ] [ section [ _class "pt-title-bar-left" ] [

View File

@ -15,6 +15,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Giraffe.Fixi" Version="0.5.7" />
<PackageReference Include="Giraffe.ViewEngine" Version="1.4.0" /> <PackageReference Include="Giraffe.ViewEngine" Version="1.4.0" />
<PackageReference Include="Giraffe.ViewEngine.Htmx" Version="2.0.4" /> <PackageReference Include="Giraffe.ViewEngine.Htmx" Version="2.0.4" />
<PackageReference Include="MailKit" Version="4.9.0" /> <PackageReference Include="MailKit" Version="4.9.0" />

View File

@ -19,10 +19,12 @@ let emptyGuid = shortGuid Guid.Empty
module String = module String =
/// string.Trim() /// string.Trim()
let trim (str: string) = str.Trim() let trim (str: string) =
str.Trim()
/// string.Replace() /// string.Replace()
let replace (find: string) repl (str: string) = str.Replace(find, repl) let replace (find: string) repl (str: string) =
str.Replace(find, repl)
/// Replace the first occurrence of a string with a second string within a given string /// Replace the first occurrence of a string with a second string within a given string
let replaceFirst (needle: string) replacement (haystack: string) = let replaceFirst (needle: string) replacement (haystack: string) =
@ -51,7 +53,7 @@ let stripTags allowedTags input =
allowedTags allowedTags
|> List.fold (fun acc t -> |> List.fold (fun acc t ->
acc acc
|| htmlTag.IndexOf $"<{t}>" = 0 || htmlTag.IndexOf $"<%s{t}>" = 0
|| htmlTag.IndexOf $"<{t} " = 0 || htmlTag.IndexOf $"<{t} " = 0
|| htmlTag.IndexOf $"</{t}" = 0) false || htmlTag.IndexOf $"</{t}" = 0) false
|> not |> not
@ -119,8 +121,8 @@ let htmlToPlainText html =
|> String.replace "\u00a0" " " |> String.replace "\u00a0" " "
/// Get the second portion of a tuple as a string /// Get the second portion of a tuple as a string
let sndAsString x = (snd >> string) x let sndAsString x =
(snd >> string) x
/// Make a URL with query string parameters /// Make a URL with query string parameters
let makeUrl url qs = let makeUrl url qs =

View File

@ -761,8 +761,8 @@ with
|> List.map (fun (typ, name) -> |> List.map (fun (typ, name) ->
let sort = let sort =
match this.SmallGroup.Preferences.RequestSort with match this.SmallGroup.Preferences.RequestSort with
| SortByDate -> Seq.sortByDescending (fun req -> req.UpdatedDate) | SortByDate -> Seq.sortByDescending _.UpdatedDate
| SortByRequestor -> Seq.sortBy (fun req -> req.Requestor) | SortByRequestor -> Seq.sortBy _.Requestor
let reqs = let reqs =
this.Requests this.Requests
|> Seq.ofList |> Seq.ofList

View File

@ -50,8 +50,8 @@ module Configure =
let _ = svc.AddLocalization(fun options -> options.ResourcesPath <- "Resources") let _ = svc.AddLocalization(fun options -> options.ResourcesPath <- "Resources")
let _ = let _ =
svc.Configure<RequestLocalizationOptions>(fun (opts: RequestLocalizationOptions) -> svc.Configure<RequestLocalizationOptions>(fun (opts: RequestLocalizationOptions) ->
let supportedCultures = [| let supportedCultures =
CultureInfo "en-US"; CultureInfo "en-GB"; CultureInfo "en-AU"; CultureInfo "en" [| CultureInfo "en-US"; CultureInfo "en-GB"; CultureInfo "en-AU"; CultureInfo "en"
CultureInfo "es-MX"; CultureInfo "es-ES"; CultureInfo "es" |] CultureInfo "es-MX"; CultureInfo "es-ES"; CultureInfo "es" |]
opts.DefaultRequestCulture <- RequestCulture("en-US", "en-US") opts.DefaultRequestCulture <- RequestCulture("en-US", "en-US")
opts.SupportedCultures <- supportedCultures opts.SupportedCultures <- supportedCultures

View File

@ -56,19 +56,18 @@ type ClaimsPrincipal with
/// The ID of the currently logged on small group /// The ID of the currently logged on small group
member this.SmallGroupId = member this.SmallGroupId =
if this.HasClaim (fun c -> c.Type = ClaimTypes.GroupSid) then this.FindFirstValue ClaimTypes.GroupSid
Some (idFromShort SmallGroupId (this.FindFirst(fun c -> c.Type = ClaimTypes.GroupSid).Value)) |> Option.ofObj
else None |> Option.map (idFromShort SmallGroupId)
/// The ID of the currently signed in user /// The ID of the currently signed-in user
member this.UserId = member this.UserId =
if this.HasClaim (fun c -> c.Type = ClaimTypes.NameIdentifier) then this.FindFirstValue ClaimTypes.NameIdentifier
Some (idFromShort UserId (this.FindFirst(fun c -> c.Type = ClaimTypes.NameIdentifier).Value)) |> Option.ofObj
else None |> Option.map (idFromShort UserId)
open Giraffe open Giraffe
open Npgsql
/// Extensions on the ASP.NET Core HTTP context /// Extensions on the ASP.NET Core HTTP context
type HttpContext with type HttpContext with

View File

@ -0,0 +1,87 @@
(()=>{
let send = (elt, type, detail, bub)=>elt.dispatchEvent(new CustomEvent("fx:" + type, {detail, cancelable:true, bubbles:bub !== false, composed:true}))
let attr = (elt, name, defaultVal)=>elt.getAttribute(name) || defaultVal
let ignore = (elt)=>elt.matches("[fx-ignore]") || elt.closest("[fx-ignore]") != null
let init = (elt)=>{
let options = {}
if (elt.__fixi || ignore(elt) || !send(elt, "init", {options})) return
elt.__fixi = async(evt)=>{
let reqs = elt.__fixi.requests ||= new Set()
let form = elt.form || elt.closest("form")
let body = new FormData(form ?? undefined, evt.submitter)
if (!form && elt.name) body.append(elt.name, elt.value)
let ac = new AbortController()
let cfg = {
trigger:evt,
action:attr(elt, "fx-action"),
method:attr(elt, "fx-method", "GET").toUpperCase(),
target: document.querySelector(attr(elt, "fx-target")) ?? elt,
swap:attr(elt, "fx-swap", "outerHTML"),
body,
drop:reqs.size,
headers:{"FX-Request":"true"},
abort:ac.abort.bind(ac),
signal:ac.signal,
preventTrigger:true,
transition:document.startViewTransition?.bind(document),
fetch:fetch.bind(window)
}
let go = send(elt, "config", {cfg, requests:reqs})
if (cfg.preventTrigger) evt.preventDefault()
if (!go || cfg.drop) return
if (/GET|DELETE/.test(cfg.method)){
let params = new URLSearchParams(cfg.body)
if (params.size)
cfg.action += (/\?/.test(cfg.action) ? "&" : "?") + params
cfg.body = null
}
reqs.add(cfg)
try {
if (cfg.confirm){
let result = await cfg.confirm()
if (!result) return
}
if (!send(elt, "before", {cfg, requests:reqs})) return
cfg.response = await cfg.fetch(cfg.action, cfg)
cfg.text = await cfg.response.text()
if (!send(elt, "after", {cfg})) return
} catch(error) {
send(elt, "error", {cfg, error})
return
} finally {
reqs.delete(cfg)
send(elt, "finally", {cfg})
}
let doSwap = ()=>{
if (cfg.swap instanceof Function)
return cfg.swap(cfg)
else if (/(before|after)(start|end)/.test(cfg.swap))
cfg.target.insertAdjacentHTML(cfg.swap, cfg.text)
else if(cfg.swap in cfg.target)
cfg.target[cfg.swap] = cfg.text
else throw cfg.swap
}
if (cfg.transition)
await cfg.transition(doSwap).finished
else
await doSwap()
send(elt, "swapped", {cfg})
}
elt.__fixi.evt = attr(elt, "fx-trigger", elt.matches("form") ? "submit" : elt.matches("input:not([type=button]),select,textarea") ? "change" : "click")
elt.addEventListener(elt.__fixi.evt, elt.__fixi, options)
send(elt, "inited", {}, false)
}
let process = (elt)=>{
if (elt instanceof Element){
if (ignore(elt)) return
if (elt.matches("[fx-action]")) init(elt)
elt.querySelectorAll("[fx-action]").forEach(init)
}
}
document.addEventListener("fx:process", (evt)=>process(evt.target))
document.addEventListener("DOMContentLoaded", ()=>{
document.__fixi_mo = new MutationObserver((recs)=>recs.forEach((r)=>r.type === "childList" && r.addedNodes.forEach((n)=>process(n))))
document.__fixi_mo.observe(document.body, {childList:true, subtree:true})
process(document.body)
})
})()