Version 2.1 #41

Merged
danieljsummers merged 123 commits from version-2.1 into main 2024-03-27 00:13:28 +00:00
7 changed files with 287 additions and 441 deletions
Showing only changes of commit e9bd3b2802 - Show all commits

View File

@ -1158,16 +1158,11 @@ let manageRevisionsModelTests = testList "ManageRevisionsModel" [
{ AsOf = Noda.epoch + Duration.FromDays 20; Text = Html "<p>huh</p>" } ] { AsOf = Noda.epoch + Duration.FromDays 20; Text = Html "<p>huh</p>" } ]
let model = let model =
ManageRevisionsModel.FromPage ManageRevisionsModel.FromPage
{ WebLog.Empty with TimeZone = "Etc/GMT+1" }
{ Page.Empty with Id = PageId "revs"; Title = "A Revised Page"; Revisions = revisions } { Page.Empty with Id = PageId "revs"; Title = "A Revised Page"; Revisions = revisions }
Expect.equal model.Id "revs" "Id not filled properly" Expect.equal model.Id "revs" "Id not filled properly"
Expect.equal model.Entity "page" "Entity not filled properly" Expect.equal model.Entity "page" "Entity not filled properly"
Expect.equal model.CurrentTitle "A Revised Page" "CurrentTitle not filled properly" Expect.equal model.CurrentTitle "A Revised Page" "CurrentTitle not filled properly"
Expect.equal model.Revisions.Length 2 "There should be two revisions" Expect.equal model.Revisions.Length 2 "There should be two revisions"
Expect.equal
model.Revisions[0].AsOfLocal
((revisions[0].AsOf - Duration.FromHours 1).ToDateTimeUtc())
"AsOfLocal not filled properly"
} }
test "FromPost succeeds" { test "FromPost succeeds" {
let revisions = let revisions =
@ -1175,16 +1170,11 @@ let manageRevisionsModelTests = testList "ManageRevisionsModel" [
{ AsOf = Noda.epoch + Duration.FromDays 12; Text = Html "<p>original</p>" } ] { AsOf = Noda.epoch + Duration.FromDays 12; Text = Html "<p>original</p>" } ]
let model = let model =
ManageRevisionsModel.FromPost ManageRevisionsModel.FromPost
{ WebLog.Empty with TimeZone = "Etc/GMT-3" }
{ Post.Empty with Id = PostId "altered"; Title = "Round Two"; Revisions = revisions } { Post.Empty with Id = PostId "altered"; Title = "Round Two"; Revisions = revisions }
Expect.equal model.Id "altered" "Id not filled properly" Expect.equal model.Id "altered" "Id not filled properly"
Expect.equal model.Entity "post" "Entity not filled properly" Expect.equal model.Entity "post" "Entity not filled properly"
Expect.equal model.CurrentTitle "Round Two" "CurrentTitle not filled properly" Expect.equal model.CurrentTitle "Round Two" "CurrentTitle not filled properly"
Expect.equal model.Revisions.Length 2 "There should be two revisions" Expect.equal model.Revisions.Length 2 "There should be two revisions"
Expect.equal
model.Revisions[0].AsOfLocal
((revisions[0].AsOf + Duration.FromHours 3).ToDateTimeUtc())
"AsOfLocal not filled properly"
} }
] ]

View File

@ -483,42 +483,24 @@ module Theme =
/// ~~~ WEB LOG SETTINGS ~~~ /// ~~~ WEB LOG SETTINGS ~~~
module WebLog = module WebLog =
open System.Collections.Generic
open System.IO open System.IO
// GET /admin/settings // GET /admin/settings
let settings : HttpHandler = fun next ctx -> task { let settings : HttpHandler = fun next ctx -> task {
let data = ctx.Data let data = ctx.Data
let! allPages = data.Page.All ctx.WebLog.Id let! allPages = data.Page.All ctx.WebLog.Id
let pages =
allPages
|> List.sortBy _.Title.ToLower()
|> List.append [ { Page.Empty with Id = PageId "posts"; Title = "- First Page of Posts -" } ]
let! themes = data.Theme.All() let! themes = data.Theme.All()
let uploads = [ Database; Disk ]
let feeds = ctx.WebLog.Rss.CustomFeeds |> List.map (DisplayCustomFeed.FromFeed (CategoryCache.get ctx))
return! return!
hashForPage "Web Log Settings" Views.Admin.webLogSettings
|> withAntiCsrf ctx (SettingsModel.FromWebLog ctx.WebLog) themes pages uploads (EditRssModel.FromRssOptions ctx.WebLog.Rss)
|> addToHash ViewContext.Model (SettingsModel.FromWebLog ctx.WebLog) feeds
|> addToHash "pages" ( |> adminPage "Web Log Settings" true next ctx
seq {
KeyValuePair.Create("posts", "- First Page of Posts -")
yield! allPages
|> List.sortBy _.Title.ToLower()
|> List.map (fun p -> KeyValuePair.Create(string p.Id, p.Title))
}
|> Array.ofSeq)
|> addToHash "themes" (
themes
|> Seq.ofList
|> Seq.map (fun it ->
KeyValuePair.Create(string it.Id, $"{it.Name} (v{it.Version})"))
|> Array.ofSeq)
|> addToHash "upload_values" [|
KeyValuePair.Create(string Database, "Database")
KeyValuePair.Create(string Disk, "Disk")
|]
|> addToHash "rss_model" (EditRssModel.FromRssOptions ctx.WebLog.Rss)
|> addToHash "custom_feeds" (
ctx.WebLog.Rss.CustomFeeds
|> List.map (DisplayCustomFeed.FromFeed (CategoryCache.get ctx))
|> Array.ofList)
|> adminView "settings" next ctx
} }
// POST /admin/settings // POST /admin/settings

View File

@ -90,25 +90,17 @@ let redirectEdit (model: EditRedirectRuleModel) app = [
input [ _type "hidden"; _name "RuleId"; _value (string model.RuleId) ] input [ _type "hidden"; _name "RuleId"; _value (string model.RuleId) ]
div [ _class "row" ] [ div [ _class "row" ] [
div [ _class "col-12 col-lg-5 mb-3" ] [ div [ _class "col-12 col-lg-5 mb-3" ] [
div [ _class "form-floating" ] [ textField [ _autofocus; _required ] (nameof model.From) "From" model.From [
input [ _type "text"; _name "From"; _id "from"; _class "form-control" span [ _class "form-text" ] [ raw "From local URL/pattern" ]
_placeholder "From local URL/pattern"; _autofocus; _required; _value model.From ]
label [ _for "from" ] [ raw "From" ]
] ]
] ]
div [ _class "col-12 col-lg-5 mb-3" ] [ div [ _class "col-12 col-lg-5 mb-3" ] [
div [ _class "form-floating" ] [ textField [ _required ] (nameof model.To) "To" model.To [
input [ _type "text"; _name "To"; _id "to"; _class "form-control"; _placeholder "To URL/pattern" span [ _class "form-text" ] [ raw "To URL/pattern" ]
_required; _value model.To ]
label [ _for "to" ] [ raw "To" ]
] ]
] ]
div [ _class "col-12 col-lg-2 mb-3" ] [ div [ _class "col-12 col-lg-2 mb-3" ] [
div [ _class "form-check form-switch" ] [ checkboxSwitch [] (nameof model.IsRegex) "Use RegEx" model.IsRegex []
input [ _type "checkbox"; _name "IsRegex"; _id "isRegex"; _class "form-check-input"; _value "true"
if model.IsRegex then _checked ]
label [ _for "isRegex" ] [ raw "Use RegEx" ]
]
] ]
] ]
if model.RuleId < 0 then if model.RuleId < 0 then
@ -126,7 +118,7 @@ let redirectEdit (model: EditRedirectRuleModel) app = [
] ]
div [ _class "row mb-3" ] [ div [ _class "row mb-3" ] [
div [ _class "col text-center" ] [ div [ _class "col text-center" ] [
button [ _type "submit"; _class "btn btn-sm btn-primary" ] [ raw "Save Changes" ] saveButton; raw " &nbsp; "
a [ _href (relUrl app "admin/settings/redirect-rules"); _class "btn btn-sm btn-secondary ms-3" ] [ a [ _href (relUrl app "admin/settings/redirect-rules"); _class "btn btn-sm btn-secondary ms-3" ] [
raw "Cancel" raw "Cancel"
] ]
@ -223,23 +215,15 @@ let tagMapEdit (model: EditTagMapModel) app = [
input [ _type "hidden"; _name "Id"; _value model.Id ] input [ _type "hidden"; _name "Id"; _value model.Id ]
div [ _class "row mb-3" ] [ div [ _class "row mb-3" ] [
div [ _class "col-6 col-lg-4 offset-lg-2" ] [ div [ _class "col-6 col-lg-4 offset-lg-2" ] [
div [ _class "form-floating" ] [ textField [ _autofocus; _required ] (nameof model.Tag) "Tag" model.Tag []
input [ _type "text"; _name "Tag"; _id "tag"; _class "form-control"; _placeholder "Tag"; _autofocus
_required; _value model.Tag ]
label [ _for "tag" ] [ raw "Tag" ]
]
] ]
div [ _class "col-6 col-lg-4" ] [ div [ _class "col-6 col-lg-4" ] [
div [ _class "form-floating" ] [ textField [ _required ] (nameof model.UrlValue) "URL Value" model.UrlValue []
input [ _type "text"; _name "UrlValue"; _id "urlValue"; _class "form-control"
_placeholder "URL Value"; _required; _value model.UrlValue ]
label [ _for "urlValue" ] [ raw "URL Value" ]
]
] ]
] ]
div [ _class "row mb-3" ] [ div [ _class "row mb-3" ] [
div [ _class "col text-center" ] [ div [ _class "col text-center" ] [
button [ _type "submit"; _class "btn btn-sm btn-primary" ] [ raw "Save Changes" ]; raw " &nbsp; " saveButton; raw " &nbsp; "
a [ _href (relUrl app "admin/settings/tag-mappings"); _class "btn btn-sm btn-secondary ms-3" ] [ a [ _href (relUrl app "admin/settings/tag-mappings"); _class "btn btn-sm btn-secondary ms-3" ] [
raw "Cancel" raw "Cancel"
] ]
@ -380,4 +364,185 @@ let themeUpload app =
] ]
] ]
|> List.singleton |> List.singleton
/// Web log settings page
let webLogSettings
(model: SettingsModel) (themes: Theme list) (pages: Page list) (uploads: UploadDestination list)
(rss: EditRssModel) (feeds: DisplayCustomFeed list) app = [
h2 [ _class "my-3" ] [ txt app.WebLog.Name; raw " Settings" ]
article [] [
p [ _class "text-muted" ] [
raw "Go to: "; a [ _href "#users" ] [ raw "Users" ]; raw " &bull; "
a [ _href "#rss-settings" ] [ raw "RSS Settings" ]; raw " &bull; "
a [ _href "#tag-mappings" ] [ raw "Tag Mappings" ]; raw " &bull; "
a [ _href (relUrl app "admin/settings/redirect-rules") ] [ raw "Redirect Rules" ]
]
fieldset [ _class "container mb-3" ] [
legend [] [ raw "Web Log Settings" ]
form [ _action (relUrl app "admin/settings"); _method "post" ] [
antiCsrf app
div [ _class "container g-0" ] [
div [ _class "row" ] [
div [ _class "col-12 col-md-6 col-xl-4 pb-3" ] [
textField [ _required; _autofocus ] (nameof model.Name) "Name" model.Name []
]
div [ _class "col-12 col-md-6 col-xl-4 pb-3" ] [
textField [ _required ] (nameof model.Slug) "Slug" model.Slug [
span [ _class "form-text" ] [
span [ _class "badge rounded-pill bg-warning text-dark" ] [ raw "WARNING" ]
raw " changing this value may break links ("
a [ _href "https://bitbadger.solutions/open-source/myweblog/configuring.html#blog-settings"
_target "_blank" ] [
raw "more"
]; raw ")"
]
]
]
div [ _class "col-12 col-md-6 col-xl-4 pb-3" ] [
textField [] (nameof model.Subtitle) "Subtitle" model.Subtitle []
]
div [ _class "col-12 col-md-6 col-xl-4 offset-xl-1 pb-3" ] [
selectField [ _required ] (nameof model.ThemeId) "Theme" model.ThemeId themes
(fun t -> string t.Id) (fun t -> $"{t.Name} (v{t.Version})") []
]
div [ _class "col-12 col-md-6 offset-md-1 col-xl-4 offset-xl-0 pb-3" ] [
selectField [ _required ] (nameof model.DefaultPage) "Default Page" model.DefaultPage pages
(fun p -> string p.Id) (_.Title) []
]
div [ _class "col-12 col-md-4 col-xl-2 pb-3" ] [
numberField [ _required; _min "0"; _max "50" ] (nameof model.PostsPerPage) "Posts per Page"
model.PostsPerPage []
]
]
div [ _class "row" ] [
div [ _class "col-12 col-md-4 col-xl-3 offset-xl-2 pb-3" ] [
textField [ _required ] (nameof model.TimeZone) "Time Zone" model.TimeZone []
]
div [ _class "col-12 col-md-4 col-xl-2" ] [
checkboxSwitch [] (nameof model.AutoHtmx) "Auto-Load htmx" model.AutoHtmx []
span [ _class "form-text fst-italic" ] [
a [ _href "https://htmx.org"; _target "_blank"; _rel "noopener" ] [
raw "What is this?"
]
]
]
div [ _class "col-12 col-md-4 col-xl-3 pb-3" ] [
selectField [] (nameof model.Uploads) "Default Upload Destination" model.Uploads uploads
string string []
]
]
div [ _class "row pb-3" ] [
div [ _class "col text-center" ] [
button [ _type "submit"; _class "btn btn-primary" ] [ raw "Save Changes" ]
]
]
]
]
]
fieldset [ _id "users"; _class "container mb-3 pb-0" ] [
legend [] [ raw "Users" ]
span [ _hxGet (relUrl app "admin/settings/users"); _hxTrigger HxTrigger.Load; _hxSwap HxSwap.OuterHtml ] []
]
fieldset [ _id "rss-settings"; _class "container mb-3 pb-0" ] [
legend [] [ raw "RSS Settings" ]
form [ _action (relUrl app "admin/settings/rss"); _method "post"; _class "container g-0" ] [
antiCsrf app
div [ _class "row pb-3" ] [
div [ _class "col col-xl-8 offset-xl-2" ] [
fieldset [ _class "d-flex justify-content-evenly flex-row" ] [
legend [] [ raw "Feeds Enabled" ]
checkboxSwitch [] (nameof rss.IsFeedEnabled) "All Posts" rss.IsFeedEnabled []
checkboxSwitch [] (nameof rss.IsCategoryEnabled) "Posts by Category" rss.IsCategoryEnabled
[]
checkboxSwitch [] (nameof rss.IsTagEnabled) "Posts by Tag" rss.IsTagEnabled []
]
]
]
div [ _class "row" ] [
div [ _class "col-12 col-sm-6 col-md-3 col-xl-2 offset-xl-2 pb-3" ] [
textField [] (nameof rss.FeedName) "Feed File Name" rss.FeedName [
span [ _class "form-text" ] [ raw "Default is "; code [] [ raw "feed.xml" ] ]
]
]
div [ _class "col-12 col-sm-6 col-md-4 col-xl-2 pb-3" ] [
numberField [ _required; _min "0" ] (nameof rss.ItemsInFeed) "Items in Feed" rss.ItemsInFeed [
span [ _class "form-text" ] [
raw "Set to &ldquo;0&rdquo; to use &ldquo;Posts per Page&rdquo; setting ("
raw (string app.WebLog.PostsPerPage); raw ")"
]
]
]
div [ _class "col-12 col-md-5 col-xl-4 pb-3" ] [
textField [] (nameof rss.Copyright) "Copyright String" rss.Copyright [
span [ _class "form-text" ] [
raw "Can be a "
a [ _href "https://creativecommons.org/share-your-work/"; _target "_blank"
_rel "noopener" ] [
raw "Creative Commons license string"
]
]
]
]
]
div [ _class "row pb-3" ] [
div [ _class "col text-center" ] [
button [ _type "submit"; _class "btn btn-primary" ] [ raw "Save Changes" ]
]
]
]
fieldset [ _class "container mb-3 pb-0" ] [
legend [] [ raw "Custom Feeds" ]
a [ _class "btn btn-sm btn-secondary"; _href (relUrl app "admin/settings/rss/new/edit") ] [
raw "Add a New Custom Feed"
]
if feeds.Length = 0 then
p [ _class "text-muted fst-italic text-center" ] [ raw "No custom feeds defined" ]
else
form [ _method "post"; _class "container g-0"; _hxTarget "body" ] [
antiCsrf app
div [ _class "row mwl-table-heading" ] [
div [ _class "col-12 col-md-6" ] [
span [ _class "d-md-none" ] [ raw "Feed" ]
span [ _class "d-none d-md-inline" ] [ raw "Source" ]
]
div [ _class $"col-12 col-md-6 d-none d-md-inline-block" ] [ raw "Relative Path" ]
]
for feed in feeds do
div [ _class "row mwl-table-detail" ] [
div [ _class "col-12 col-md-6" ] [
txt feed.Source
if feed.IsPodcast then
raw " &nbsp; "; span [ _class "badge bg-primary" ] [ raw "PODCAST" ]
br []
small [] [
let feedUrl = relUrl app $"admin/settings/rss/{feed.Id}"
a [ _href (relUrl app feed.Path); _target "_blank" ] [ raw "View Feed" ]
span [ _class "text-muted" ] [ raw " &bull; " ]
a [ _href $"{feedUrl}/edit" ] [ raw "Edit" ]
span [ _class "text-muted" ] [ raw " &bull; " ]
a [ _href feedUrl; _hxDelete feedUrl; _class "text-danger"
_hxConfirm $"Are you sure you want to delete the custom RSS feed based on {feed.Source}? This action cannot be undone." ] [
raw "Delete"
]
]
]
div [ _class "col-12 col-md-6" ] [
small [ _class "d-md-none" ] [ raw "Served at "; txt feed.Path ]
span [ _class "d-none d-md-inline" ] [ txt feed.Path ]
]
]
]
]
]
fieldset [ _id "tag-mappings"; _class "container mb-3 pb-0" ] [
legend [] [ raw "Tag Mappings" ]
a [ _href (relUrl app "admin/settings/tag-mapping/new/edit"); _class "btn btn-primary btn-sm mb-3"
_hxTarget "#tag_new" ] [
raw "Add a New Tag Mapping"
]
span [ _hxGet (relUrl app "admin/settings/tag-mappings"); _hxTrigger HxTrigger.Load
_hxSwap HxSwap.OuterHtml ] []
]
]
]

View File

@ -102,6 +102,59 @@ let shortTime app (instant: Instant) =
let yesOrNo value = let yesOrNo value =
raw (if value then "Yes" else "No") raw (if value then "Yes" else "No")
/// Create a text input field
let inputField fieldType attrs name labelText value extra =
div [ _class "form-floating" ] [
[ _type fieldType; _name name; _id name; _class "form-control"; _placeholder labelText; _value value ]
|> List.append attrs
|> input
label [ _for name ] [ raw labelText ]
yield! extra
]
/// Create a text input field
let textField attrs name labelText value extra =
inputField "text" attrs name labelText value extra
/// Create a number input field
let numberField attrs name labelText (value: int) extra =
inputField "number" attrs name labelText (string value) extra
/// Create an e-mail input field
let emailField attrs name labelText value extra =
inputField "email" attrs name labelText value extra
/// Create a password input field
let passwordField attrs name labelText value extra =
inputField "password" attrs name labelText value extra
/// Create a select (dropdown) field
let selectField<'T, 'a>
attrs name labelText value (values: 'T list) (idFunc: 'T -> 'a) (displayFunc: 'T -> string) extra =
div [ _class "form-floating" ] [
select ([ _name name; _id name; _class "form-control" ] |> List.append attrs) [
for item in values do
let itemId = string (idFunc item)
option [ _value itemId; if value = itemId then _selected ] [ txt (displayFunc item) ]
]
label [ _for name ] [ raw labelText ]
yield! extra
]
/// Create a checkbox input styled as a switch
let checkboxSwitch attrs name labelText (value: bool) extra =
div [ _class "form-check form-switch" ] [
[ _type "checkbox"; _name name; _id name; _class "form-check-input"; _value "true"; if value then _checked ]
|> List.append attrs
|> input
label [ _for name; _class "form-check-label" ] [ raw labelText ]
yield! extra
]
/// A standard save button
let saveButton =
button [ _type "submit"; _class "btn btn-sm btn-primary" ] [ raw "Save Changes" ]
/// Functions for generating content in varying layouts /// Functions for generating content in varying layouts
module Layout = module Layout =

View File

@ -24,83 +24,47 @@ let chapterEdit (model: EditChapterModel) app = [
input [ _type "hidden"; _name "Index"; _value (string model.Index) ] input [ _type "hidden"; _name "Index"; _value (string model.Index) ]
div [ _class "row" ] [ div [ _class "row" ] [
div [ _class "col-6 col-lg-3 mb-3" ] [ div [ _class "col-6 col-lg-3 mb-3" ] [
div [ _class "form-floating" ] [ textField [ _required; _autofocus ] (nameof model.StartTime) "Start Time"
input [ _type "text"; _id "start_time"; _name "StartTime"; _class "form-control"; _required (if model.Index < 0 then "" else model.StartTime) []
_autofocus; _placeholder "Start Time"
if model.Index >= 0 then _value model.StartTime ]
label [ _for "start_time" ] [ raw "Start Time" ]
]
] ]
div [ _class "col-6 col-lg-3 mb-3" ] [ div [ _class "col-6 col-lg-3 mb-3" ] [
div [ _class "form-floating" ] [ textField [] (nameof model.EndTime) "End Time" model.EndTime [
input [ _type "text"; _id "end_time"; _name "EndTime"; _class "form-control"; _value model.EndTime
_placeholder "End Time" ]
label [ _for "end_time" ] [ raw "End Time" ]
span [ _class "form-text" ] [ raw "Optional; ends when next starts" ] span [ _class "form-text" ] [ raw "Optional; ends when next starts" ]
] ]
] ]
div [ _class "col-12 col-lg-6 mb-3" ] [ div [ _class "col-12 col-lg-6 mb-3" ] [
div [ _class "form-floating" ] [ textField [] (nameof model.Title) "Chapter Title" model.Title [
input [ _type "text"; _id "title"; _name "Title"; _class "form-control"; _value model.Title
_placeholder "Title" ]
label [ _for "title" ] [ raw "Chapter Title" ]
span [ _class "form-text" ] [ raw "Optional" ] span [ _class "form-text" ] [ raw "Optional" ]
] ]
] ]
div [ _class "col-12 col-lg-6 col-xl-5 mb-3" ] [ div [ _class "col-12 col-lg-6 col-xl-5 mb-3" ] [
div [ _class "form-floating" ] [ textField [] (nameof model.ImageUrl) "Image URL" model.ImageUrl [
input [ _type "text"; _id "image_url"; _name "ImageUrl"; _class "form-control"
_value model.ImageUrl; _placeholder "Image URL" ]
label [ _for "image_url" ] [ raw "Image URL" ]
span [ _class "form-text" ] [ span [ _class "form-text" ] [
raw "Optional; a separate image to display while this chapter is playing" raw "Optional; a separate image to display while this chapter is playing"
] ]
] ]
] ]
div [ _class "col-12 col-lg-6 col-xl-5 mb-3" ] [ div [ _class "col-12 col-lg-6 col-xl-5 mb-3" ] [
div [ _class "form-floating" ] [ textField [] (nameof model.Url) "URL" model.Url [
input [ _type "text"; _id "url"; _name "Url"; _class "form-control"; _value model.Url span [ _class "form-text" ] [ raw "Optional; informational link for this chapter" ]
_placeholder "URL" ]
label [ _for "url" ] [ raw "URL" ]
span [ _class "form-text" ] [
raw "Optional; informational link for this chapter"
]
] ]
] ]
div [ _class "col-12 col-lg-6 offset-lg-3 col-xl-2 offset-xl-0 mb-3 align-self-end d-flex flex-column" ] [ div [ _class "col-12 col-lg-6 offset-lg-3 col-xl-2 offset-xl-0 mb-3 align-self-end d-flex flex-column" ] [
div [ _class "form-check form-switch mb-3" ] [ checkboxSwitch [] (nameof model.IsHidden) "Hidden Chapter" model.IsHidden []
input [ _type "checkbox"; _id "is_hidden"; _name "IsHidden"; _class "form-check-input" span [ _class "mt-2 form-text" ] [ raw "Not displayed, but may update image and location" ]
_value "true"
if model.IsHidden then _checked ]
label [ _for "is_hidden" ] [ raw "Hidden Chapter" ]
]
span [ _class "form-text" ] [ raw "Not displayed, but may update image and location" ]
] ]
] ]
div [ _class "row" ] [ div [ _class "row" ] [
let hasLoc = model.LocationName <> "" let hasLoc = model.LocationName <> ""
let attrs = if hasLoc then [] else [ _disabled ]
div [ _class "col-12 col-md-4 col-lg-3 offset-lg-1 mb-3 align-self-end" ] [ div [ _class "col-12 col-md-4 col-lg-3 offset-lg-1 mb-3 align-self-end" ] [
div [ _class "form-check form-switch mb-3" ] [ checkboxSwitch [ _onclick "Admin.checkChapterLocation()" ] "has_location" "Associate Location" hasLoc []
input [ _type "checkbox"; _id "has_location"; _class "form-check-input"; _value "true"
if hasLoc then _checked
_onclick "Admin.checkChapterLocation()" ]
label [ _for "has_location" ] [ raw "Associate Location" ]
]
] ]
div [ _class "col-12 col-md-8 col-lg-6 offset-lg-1 mb-3" ] [ div [ _class "col-12 col-md-8 col-lg-6 offset-lg-1 mb-3" ] [
div [ _class "form-floating" ] [ textField (_required :: attrs) (nameof model.LocationName) "Name" model.LocationName []
input [ _type "text"; _id "location_name"; _name "LocationName"; _class "form-control"
_value model.LocationName; _placeholder "Location Name"; _required
if not hasLoc then _disabled ]
label [ _for "location_name" ] [ raw "Name" ]
]
] ]
div [ _class "col-6 col-lg-4 offset-lg-2 mb-3" ] [ div [ _class "col-6 col-lg-4 offset-lg-2 mb-3" ] [
div [ _class "form-floating" ] [ textField (_required :: attrs) (nameof model.LocationGeo) "Geo URL" model.LocationGeo [
input [ _type "text"; _id "location_geo"; _name "LocationGeo"; _class "form-control"
_value model.LocationGeo; _placeholder "Location Geo URL"; _required
if not hasLoc then _disabled ]
label [ _for "location_geo" ] [ raw "Geo URL" ]
em [ _class "form-text" ] [ em [ _class "form-text" ] [
a [ _href "https://github.com/Podcastindex-org/podcast-namespace/blob/main/location/location.md#geo-recommended" a [ _href "https://github.com/Podcastindex-org/podcast-namespace/blob/main/location/location.md#geo-recommended"
_target "_blank"; _rel "noopener" ] [ _target "_blank"; _rel "noopener" ] [
@ -110,11 +74,7 @@ let chapterEdit (model: EditChapterModel) app = [
] ]
] ]
div [ _class "col-6 col-lg-4 mb-3" ] [ div [ _class "col-6 col-lg-4 mb-3" ] [
div [ _class "form-floating" ] [ textField attrs (nameof model.LocationOsm) "OpenStreetMap ID" model.LocationOsm [
input [ _type "text"; _id "location_osm"; _name "LocationOsm"; _class "form-control"
_value model.LocationOsm; _placeholder "Location OSM Query"
if not hasLoc then _disabled ]
label [ _for "location_osm" ] [ raw "OpenStreetMap ID" ]
em [ _class "form-text" ] [ em [ _class "form-text" ] [
raw "Optional; " raw "Optional; "
a [ _href "https://www.openstreetmap.org/"; _target "_blank"; _rel "noopener" ] [ raw "get ID" ] a [ _href "https://www.openstreetmap.org/"; _target "_blank"; _rel "noopener" ] [ raw "get ID" ]
@ -138,8 +98,7 @@ let chapterEdit (model: EditChapterModel) app = [
] ]
else else
input [ _type "hidden"; _name "AddAnother"; _value "false" ] input [ _type "hidden"; _name "AddAnother"; _value "false" ]
button [ _type "submit"; _class "btn btn-primary" ] [ raw "Save" ] saveButton; raw " &nbsp; "
raw " &nbsp; "
a [ _href cancelLink; _hxGet cancelLink; _class "btn btn-secondary"; _hxTarget "body" ] [ raw "Cancel" ] a [ _href cancelLink; _hxGet cancelLink; _class "btn btn-secondary"; _hxTarget "body" ] [ raw "Cancel" ]
] ]
] ]

View File

@ -30,41 +30,21 @@ let edit (model: EditUserModel) app =
] ]
] ]
div [ _class "col-12 col-md-7 col-lg-4 col-xxl-3 mb-3" ] [ div [ _class "col-12 col-md-7 col-lg-4 col-xxl-3 mb-3" ] [
div [ _class "form-floating" ] [ emailField [ _required ] (nameof model.Email) "E-mail Address" model.Email []
input [ _type "email"; _name "Email"; _id "email"; _class "form-control"; _placeholder "E-mail"
_required; _value model.Email ]
label [ _for "email" ] [ raw "E-mail Address" ]
]
] ]
div [ _class "col-12 col-lg-5 mb-3" ] [ div [ _class "col-12 col-lg-5 mb-3" ] [
div [ _class "form-floating" ] [ textField [] (nameof model.Url) "User&rsquo;s Personal URL" model.Url []
input [ _type "text"; _name "Url"; _id "url"; _class "form-control"; _placeholder "URL"
_value model.Url ]
label [ _for "url" ] [ raw "User&rsquo;s Personal URL" ]
]
] ]
] ]
div [ _class "row mb-3" ] [ div [ _class "row mb-3" ] [
div [ _class "col-12 col-md-6 col-lg-4 col-xl-3 offset-xl-1 pb-3" ] [ div [ _class "col-12 col-md-6 col-lg-4 col-xl-3 offset-xl-1 pb-3" ] [
div [ _class "form-floating" ] [ textField [ _required ] (nameof model.FirstName) "First Name" model.FirstName []
input [ _type "text"; _name "FirstName"; _id "firstName"; _class "form-control"
_placeholder "First"; _required; _value model.FirstName ]
label [ _for "firstName" ] [ raw "First Name" ]
]
] ]
div [ _class "col-12 col-md-6 col-lg-4 col-xl-3 pb-3" ] [ div [ _class "col-12 col-md-6 col-lg-4 col-xl-3 pb-3" ] [
div [ _class "form-floating" ] [ textField [ _required ] (nameof model.LastName) "Last Name" model.LastName []
input [ _type "text"; _name "LastName"; _id "lastName"; _class "form-control"
_placeholder "Last"; _required; _value model.LastName ]
label [ _for "lastName" ] [ raw "Last Name" ]
]
] ]
div [ _class "col-12 col-md-6 offset-md-3 col-lg-4 offset-lg-0 col-xl-3 offset-xl-1 pb-3" ] [ div [ _class "col-12 col-md-6 offset-md-3 col-lg-4 offset-lg-0 col-xl-3 offset-xl-1 pb-3" ] [
div [ _class "form-floating " ] [ textField [ _required ] (nameof model.PreferredName) "Preferred Name" model.PreferredName []
input [ _type "text"; _name "PreferredName"; _id "preferredName"; _class "form-control"
_placeholder "Preferred"; _required; _value model.PreferredName ]
label [ _for "preferredName" ] [ raw "Preferred Name" ]
]
] ]
] ]
div [ _class "row mb-3" ] [ div [ _class "row mb-3" ] [
@ -83,28 +63,12 @@ let edit (model: EditUserModel) app =
] ]
] ]
div [ _class "row" ] [ div [ _class "row" ] [
let attrs, newLbl = if model.IsNew then [ _required ], "" else [], "New "
div [ _class "col-12 col-md-6 pb-3" ] [ div [ _class "col-12 col-md-6 pb-3" ] [
div [ _class "form-floating" ] [ passwordField attrs (nameof model.Password) $"{newLbl}Password" "" []
input [ _type "password"; _name "Password"; _id "password"; _class "form-control"
_placeholder "Password"
if model.IsNew then _required ]
label [ _for "password" ] [
if not model.IsNew then raw "New "
raw "Password"
]
]
] ]
div [ _class "col-12 col-md-6 pb-3" ] [ div [ _class "col-12 col-md-6 pb-3" ] [
div [ _class "form-floating" ] [ passwordField attrs (nameof model.PasswordConfirm) $"Confirm {newLbl}Password" "" []
input [ _type "password"; _name "PasswordConfirm"; _id "passwordConfirm"
_class "form-control"; _placeholder "Confirm"
if model.IsNew then _required ]
label [ _for "passwordConfirm" ] [
raw "Confirm"
if not model.IsNew then raw " New"
raw " Password"
]
]
] ]
] ]
] ]
@ -112,7 +76,7 @@ let edit (model: EditUserModel) app =
] ]
div [ _class "row mb-3" ] [ div [ _class "row mb-3" ] [
div [ _class "col text-center" ] [ div [ _class "col text-center" ] [
button [ _type "submit"; _class "btn btn-sm btn-primary" ] [ raw "Save Changes" ]; raw " &nbsp; " saveButton; raw " &nbsp; "
if model.IsNew then if model.IsNew then
button [ _type "button"; _class "btn btn-sm btn-secondary ms-3" button [ _type "button"; _class "btn btn-sm btn-secondary ms-3"
_onclick "document.getElementById('user_new').innerHTML = ''" ] [ _onclick "document.getElementById('user_new').innerHTML = ''" ] [
@ -138,17 +102,10 @@ let logOn (model: LogOnModel) (app: AppViewContext) = [
if Option.isSome model.ReturnTo then input [ _type "hidden"; _name "ReturnTo"; _value model.ReturnTo.Value ] if Option.isSome model.ReturnTo then input [ _type "hidden"; _name "ReturnTo"; _value model.ReturnTo.Value ]
div [ _class "row" ] [ div [ _class "row" ] [
div [ _class "col-12 col-md-6 col-lg-4 offset-lg-2 pb-3" ] [ div [ _class "col-12 col-md-6 col-lg-4 offset-lg-2 pb-3" ] [
div [ _class "form-floating" ] [ emailField [ _required; _autofocus ] (nameof model.EmailAddress) "E-mail Address" "" []
input [ _type "email"; _id "email"; _name "EmailAddress"; _class "form-control"; _autofocus
_required ]
label [ _for "email" ] [ rawText "E-mail Address" ]
]
] ]
div [ _class "col-12 col-md-6 col-lg-4 pb-3" ] [ div [ _class "col-12 col-md-6 col-lg-4 pb-3" ] [
div [ _class "form-floating" ] [ passwordField [ _required ] (nameof model.Password) "Password" "" []
input [ _type "password"; _id "password"; _name "Password"; _class "form-control"; _required ]
label [ _for "password" ] [ rawText "Password" ]
]
] ]
] ]
div [ _class "row pb-3" ] [ div [ _class "row pb-3" ] [
@ -263,25 +220,13 @@ let myInfo (model: EditMyInfoModel) (user: WebLogUser) app = [
div [ _class "row" ] [ div [ _class "col" ] [ hr [ _class "mt-0" ] ] ] div [ _class "row" ] [ div [ _class "col" ] [ hr [ _class "mt-0" ] ] ]
div [ _class "row mb-3" ] [ div [ _class "row mb-3" ] [
div [ _class "col-12 col-md-6 col-lg-4 pb-3" ] [ div [ _class "col-12 col-md-6 col-lg-4 pb-3" ] [
div [ _class "form-floating" ] [ textField [ _required; _autofocus ] (nameof model.FirstName) "First Name" model.FirstName []
input [ _type "text"; _name "FirstName"; _id "firstName"; _class "form-control"; _autofocus
_required; _placeholder "First"; _value model.FirstName ]
label [ _for "firstName" ] [ raw "First Name" ]
]
] ]
div [ _class "col-12 col-md-6 col-lg-4 pb-3" ] [ div [ _class "col-12 col-md-6 col-lg-4 pb-3" ] [
div [ _class "form-floating" ] [ textField [ _required ] (nameof model.LastName) "Last Name" model.LastName []
input [ _type "text"; _name "LastName"; _id "lastName"; _class "form-control"; _required
_placeholder "Last"; _value model.LastName ]
label [ _for "lastName" ] [ raw "Last Name" ]
]
] ]
div [ _class "col-12 col-md-6 col-lg-4 pb-3" ] [ div [ _class "col-12 col-md-6 col-lg-4 pb-3" ] [
div [ _class "form-floating" ] [ textField [ _required ] (nameof model.PreferredName) "Preferred Name" model.PreferredName []
input [ _type "text"; _name "PreferredName"; _id "preferredName"; _class "form-control"
_required; _placeholder "Preferred"; _value model.PreferredName ]
label [ _for "preferredName" ] [ raw "Preferred Name" ]
]
] ]
] ]
div [ _class "row mb-3" ] [ div [ _class "row mb-3" ] [
@ -297,28 +242,16 @@ let myInfo (model: EditMyInfoModel) (user: WebLogUser) app = [
] ]
div [ _class "row" ] [ div [ _class "row" ] [
div [ _class "col-12 col-md-6 pb-3" ] [ div [ _class "col-12 col-md-6 pb-3" ] [
div [ _class "form-floating" ] [ passwordField [] (nameof model.NewPassword) "New Password" "" []
input [ _type "password"; _name "NewPassword"; _id "newPassword"
_class "form-control"; _placeholder "Password" ]
label [ _for "newPassword" ] [ raw "New Password" ]
]
] ]
div [ _class "col-12 col-md-6 pb-3" ] [ div [ _class "col-12 col-md-6 pb-3" ] [
div [ _class "form-floating" ] [ passwordField [] (nameof model.NewPasswordConfirm) "Confirm New Password" "" []
input [ _type "password"; _name "NewPasswordConfirm"; _id "newPasswordConfirm"
_class "form-control"; _placeholder "Confirm" ]
label [ _for "newPasswordConfirm" ] [ raw "Confirm New Password" ]
]
] ]
] ]
] ]
] ]
] ]
div [ _class "row" ] [ div [ _class "row" ] [ div [ _class "col text-center mb-3" ] [ saveButton ] ]
div [ _class "col text-center mb-3" ] [
button [ _type "submit"; _class "btn btn-primary" ] [ raw "Save Changes" ]
]
]
] ]
] ]
] ]

View File

@ -1,236 +0,0 @@
<h2 class=my-3>{{ web_log.name }} Settings</h2>
<article>
<p class=text-muted>
Go to: <a href=#users>Users</a> &bull; <a href=#rss-settings>RSS Settings</a> &bull;
<a href=#tag-mappings>Tag Mappings</a> &bull;
<a href="{{ "admin/settings/redirect-rules" | relative_link }}">Redirect Rules</a>
</p>
<fieldset class="container mb-3">
<legend>Web Log Settings</legend>
<form action="{{ "admin/settings" | relative_link }}" method=post>
<input type=hidden name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
<div class=container>
<div class=row>
<div class="col-12 col-md-6 col-xl-4 pb-3">
<div class=form-floating>
<input type=text name=Name id=name class=form-control placeholder=Name required autofocus
value="{{ model.name }}">
<label for=name>Name</label>
</div>
</div>
<div class="col-12 col-md-6 col-xl-4 pb-3">
<div class=form-floating>
<input type=text name=Slug id=slug class=form-control placeholder="Slug" required
value="{{ model.slug }}">
<label for=slug>Slug</label>
<span class=form-text>
<span class="badge rounded-pill bg-warning text-dark">WARNING</span> changing this value may break
links
(<a href=https://bitbadger.solutions/open-source/myweblog/configuring.html#blog-settings
target=_blank>more</a>)
</span>
</div>
</div>
<div class="col-12 col-md-6 col-xl-4 pb-3">
<div class=form-floating>
<input type=text name=Subtitle id=subtitle class=form-control placeholder=Subtitle
value="{{ model.subtitle }}">
<label for=subtitle>Subtitle</label>
</div>
</div>
<div class="col-12 col-md-6 col-xl-4 offset-xl-1 pb-3">
<div class=form-floating>
<select name=ThemeId id=themeId class=form-control required>
{% for theme in themes -%}
<option value="{{ theme[0] }}"{% if model.theme_id == theme[0] %} selected{% endif %}>
{{ theme[1] }}
</option>
{%- endfor %}
</select>
<label for=themeId>Theme</label>
</div>
</div>
<div class="col-12 col-md-6 offset-md-1 col-xl-4 offset-xl-0 pb-3">
<div class=form-floating>
<select name=DefaultPage id=defaultPage class=form-control required>
{%- for pg in pages %}
<option value="{{ pg[0] }}"{% if pg[0] == model.default_page %} selected{% endif %}>
{{ pg[1] }}
</option>
{%- endfor %}
</select>
<label for=defaultPage>Default Page</label>
</div>
</div>
<div class="col-12 col-md-4 col-xl-2 pb-3">
<div class=form-floating>
<input type=number name=PostsPerPage id=postsPerPage class=form-control min=0 max=50 required
value="{{ model.posts_per_page }}">
<label for=postsPerPage>Posts per Page</label>
</div>
</div>
</div>
<div class=row>
<div class="col-12 col-md-4 col-xl-3 offset-xl-2 pb-3">
<div class=form-floating>
<input type=text name=TimeZone id=timeZone class=form-control placeholder="Time Zone" required
value="{{ model.time_zone }}">
<label for=timeZone>Time Zone</label>
</div>
</div>
<div class="col-12 col-md-4 col-xl-2">
<div class="form-check form-switch">
<input type=checkbox name=AutoHtmx id=autoHtmx class=form-check-input value=true
{%- if model.auto_htmx %} checked{% endif %}>
<label for=autoHtmx class=form-check-label>Auto-Load htmx</label>
</div>
<span class="form-text fst-italic">
<a href=https://htmx.org target=_blank rel=noopener>What is this?</a>
</span>
</div>
<div class="col-12 col-md-4 col-xl-3 pb-3">
<div class=form-floating>
<select name=Uploads id=uploads class=form-control>
{%- for it in upload_values %}
<option value="{{ it[0] }}"{% if model.uploads == it[0] %} selected{% endif %}>{{ it[1] }}</option>
{%- endfor %}
</select>
<label for=uploads>Default Upload Destination</label>
</div>
</div>
</div>
<div class="row pb-3">
<div class="col text-center">
<button type=submit class="btn btn-primary">Save Changes</button>
</div>
</div>
</div>
</form>
</fieldset>
<fieldset id=users class="container mb-3 pb-0">
<legend>Users</legend>
<span hx-get="{{ "admin/settings/users" | relative_link }}" hx-trigger="load" hx-swap="outerHTML"></span>
</fieldset>
<fieldset id=rss-settings class="container mb-3 pb-0">
<legend>RSS Settings</legend>
<form action="{{ "admin/settings/rss" | relative_link }}" method=post>
<input type=hidden name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
<div class=container>
<div class="row pb-3">
<div class="col col-xl-8 offset-xl-2">
<fieldset class="d-flex justify-content-evenly flex-row">
<legend>Feeds Enabled</legend>
<div class="form-check form-switch pb-2">
<input type=checkbox name=IsFeedEnabled id=feedEnabled class=form-check-input value=true
{%- if rss_model.is_feed_enabled %} checked{% endif %}>
<label for=feedEnabled class=form-check-label>All Posts</label>
</div>
<div class="form-check form-switch pb-2">
<input type=checkbox name=IsCategoryEnabled id=categoryEnabled class=form-check-input value=true
{%- if rss_model.is_category_enabled %} checked{% endif %}>
<label for=categoryEnabled class=form-check-label>Posts by Category</label>
</div>
<div class="form-check form-switch pb-2">
<input type=checkbox name=IsTagEnabled id=tagEnabled class=form-check-input value=true
{%- if rss_model.is_tag_enabled %} checked{% endif %}>
<label for=tagEnabled class=form-check-label>Posts by Tag</label>
</div>
</fieldset>
</div>
</div>
<div class=row>
<div class="col-12 col-sm-6 col-md-3 col-xl-2 offset-xl-2 pb-3">
<div class=form-floating>
<input type=text name=FeedName id=feedName class=form-control placeholder="Feed File Name"
value="{{ rss_model.feed_name }}">
<label for=feedName>Feed File Name</label>
<span class=form-text>Default is <code>feed.xml</code></span>
</div>
</div>
<div class="col-12 col-sm-6 col-md-4 col-xl-2 pb-3">
<div class=form-floating>
<input type=number name=ItemsInFeed id=itemsInFeed class=form-control min=0 placeholder="Items in Feed"
required value="{{ rss_model.items_in_feed }}">
<label for=itemsInFeed>Items in Feed</label>
<span class=form-text>
Set to &ldquo;0&rdquo; to use &ldquo;Posts per Page&rdquo; setting ({{ web_log.posts_per_page }})
</span>
</div>
</div>
<div class="col-12 col-md-5 col-xl-4 pb-3">
<div class=form-floating>
<input type=text name=Copyright id=copyright class=form-control placeholder="Copyright String"
value="{{ rss_model.copyright }}">
<label for=copyright>Copyright String</label>
<span class=form-text>
Can be a
<a href="https://creativecommons.org/share-your-work/" target=_blank rel=noopener>
Creative Commons license string
</a>
</span>
</div>
</div>
</div>
<div class="row pb-3">
<div class="col text-center">
<button type=submit class="btn btn-primary">Save Changes</button>
</div>
</div>
</div>
</form>
<fieldset class="container mb-3 pb-0">
<legend>Custom Feeds</legend>
<a class="btn btn-sm btn-secondary" href="{{ 'admin/settings/rss/new/edit' | relative_link }}">
Add a New Custom Feed
</a>
{%- assign feed_count = custom_feeds | size -%}
{%- if feed_count > 0 %}
<form method=post class="container g-0" hx-target=body>
{%- assign source_col = "col-12 col-md-6" -%}
{%- assign path_col = "col-12 col-md-6" -%}
<input type=hidden name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
<div class="row mwl-table-heading">
<div class="{{ source_col }}">
<span class=d-md-none>Feed</span><span class="d-none d-md-inline">Source</span>
</div>
<div class="{{ path_col }} d-none d-md-inline-block">Relative Path</div>
</div>
{% for feed in custom_feeds %}
<div class="row mwl-table-detail">
<div class="{{ source_col }}">
{{ feed.source }}
{%- if feed.is_podcast %} &nbsp; <span class="badge bg-primary">PODCAST</span>{% endif %}<br>
<small>
{%- assign feed_url = "admin/settings/rss/" | append: feed.id -%}
<a href="{{ feed.path | relative_link }}" target=_blank>View Feed</a>
<span class=text-muted> &bull; </span>
<a href="{{ feed_url | append: "/edit" | relative_link }}">Edit</a>
<span class=text-muted> &bull; </span>
{%- assign feed_del_link = feed_url | append: "/delete" | relative_link -%}
<a href="{{ feed_del_link }}" hx-post="{{ feed_del_link }}" class=text-danger
hx-confirm="Are you sure you want to delete the custom RSS feed based on {{ feed.source | strip_html | escape }}? This action cannot be undone.">
Delete
</a>
</small>
</div>
<div class="{{ path_col }}">
<small class=d-md-none>Served at {{ feed.path }}</small>
<span class="d-none d-md-inline">{{ feed.path }}</span>
</div>
</div>
{%- endfor %}
</form>
{%- else %}
<p class="text-muted fst-italic text-center">No custom feeds defined
{%- endif %}
</fieldset>
</fieldset>
<fieldset id="tag-mappings" class="container mb-3 pb-0">
<legend>Tag Mappings</legend>
<a href="{{ "admin/settings/tag-mapping/new/edit" | relative_link }}" class="btn btn-primary btn-sm mb-3"
hx-target=#tag_new>
Add a New Tag Mapping
</a>
<span hx-get="{{ "admin/settings/tag-mappings" | relative_link }}" hx-trigger="load" hx-swap="outerHTML"></span>
</fieldset>
</article>