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>" } ]
let model =
ManageRevisionsModel.FromPage
{ WebLog.Empty with TimeZone = "Etc/GMT+1" }
{ Page.Empty with Id = PageId "revs"; Title = "A Revised Page"; Revisions = revisions }
Expect.equal model.Id "revs" "Id 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.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" {
let revisions =
@ -1175,16 +1170,11 @@ let manageRevisionsModelTests = testList "ManageRevisionsModel" [
{ AsOf = Noda.epoch + Duration.FromDays 12; Text = Html "<p>original</p>" } ]
let model =
ManageRevisionsModel.FromPost
{ WebLog.Empty with TimeZone = "Etc/GMT-3" }
{ Post.Empty with Id = PostId "altered"; Title = "Round Two"; Revisions = revisions }
Expect.equal model.Id "altered" "Id 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.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 ~~~
module WebLog =
open System.Collections.Generic
open System.IO
// GET /admin/settings
let settings : HttpHandler = fun next ctx -> task {
let data = ctx.Data
let! allPages = data.Page.All ctx.WebLog.Id
let! themes = data.Theme.All()
return!
hashForPage "Web Log Settings"
|> withAntiCsrf ctx
|> addToHash ViewContext.Model (SettingsModel.FromWebLog ctx.WebLog)
|> addToHash "pages" (
seq {
KeyValuePair.Create("posts", "- First Page of Posts -")
yield! allPages
let pages =
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
|> List.append [ { Page.Empty with Id = PageId "posts"; Title = "- First Page of Posts -" } ]
let! themes = data.Theme.All()
let uploads = [ Database; Disk ]
let feeds = ctx.WebLog.Rss.CustomFeeds |> List.map (DisplayCustomFeed.FromFeed (CategoryCache.get ctx))
return!
Views.Admin.webLogSettings
(SettingsModel.FromWebLog ctx.WebLog) themes pages uploads (EditRssModel.FromRssOptions ctx.WebLog.Rss)
feeds
|> adminPage "Web Log Settings" true next ctx
}
// POST /admin/settings

View File

@ -90,25 +90,17 @@ let redirectEdit (model: EditRedirectRuleModel) app = [
input [ _type "hidden"; _name "RuleId"; _value (string model.RuleId) ]
div [ _class "row" ] [
div [ _class "col-12 col-lg-5 mb-3" ] [
div [ _class "form-floating" ] [
input [ _type "text"; _name "From"; _id "from"; _class "form-control"
_placeholder "From local URL/pattern"; _autofocus; _required; _value model.From ]
label [ _for "from" ] [ raw "From" ]
textField [ _autofocus; _required ] (nameof model.From) "From" model.From [
span [ _class "form-text" ] [ raw "From local URL/pattern" ]
]
]
div [ _class "col-12 col-lg-5 mb-3" ] [
div [ _class "form-floating" ] [
input [ _type "text"; _name "To"; _id "to"; _class "form-control"; _placeholder "To URL/pattern"
_required; _value model.To ]
label [ _for "to" ] [ raw "To" ]
textField [ _required ] (nameof model.To) "To" model.To [
span [ _class "form-text" ] [ raw "To URL/pattern" ]
]
]
div [ _class "col-12 col-lg-2 mb-3" ] [
div [ _class "form-check form-switch" ] [
input [ _type "checkbox"; _name "IsRegex"; _id "isRegex"; _class "form-check-input"; _value "true"
if model.IsRegex then _checked ]
label [ _for "isRegex" ] [ raw "Use RegEx" ]
]
checkboxSwitch [] (nameof model.IsRegex) "Use RegEx" model.IsRegex []
]
]
if model.RuleId < 0 then
@ -126,7 +118,7 @@ let redirectEdit (model: EditRedirectRuleModel) app = [
]
div [ _class "row mb-3" ] [
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" ] [
raw "Cancel"
]
@ -223,23 +215,15 @@ let tagMapEdit (model: EditTagMapModel) app = [
input [ _type "hidden"; _name "Id"; _value model.Id ]
div [ _class "row mb-3" ] [
div [ _class "col-6 col-lg-4 offset-lg-2" ] [
div [ _class "form-floating" ] [
input [ _type "text"; _name "Tag"; _id "tag"; _class "form-control"; _placeholder "Tag"; _autofocus
_required; _value model.Tag ]
label [ _for "tag" ] [ raw "Tag" ]
]
textField [ _autofocus; _required ] (nameof model.Tag) "Tag" model.Tag []
]
div [ _class "col-6 col-lg-4" ] [
div [ _class "form-floating" ] [
input [ _type "text"; _name "UrlValue"; _id "urlValue"; _class "form-control"
_placeholder "URL Value"; _required; _value model.UrlValue ]
label [ _for "urlValue" ] [ raw "URL Value" ]
]
textField [ _required ] (nameof model.UrlValue) "URL Value" model.UrlValue []
]
]
div [ _class "row mb-3" ] [
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" ] [
raw "Cancel"
]
@ -381,3 +365,184 @@ let themeUpload app =
]
|> 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 =
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
module Layout =

View File

@ -24,83 +24,47 @@ let chapterEdit (model: EditChapterModel) app = [
input [ _type "hidden"; _name "Index"; _value (string model.Index) ]
div [ _class "row" ] [
div [ _class "col-6 col-lg-3 mb-3" ] [
div [ _class "form-floating" ] [
input [ _type "text"; _id "start_time"; _name "StartTime"; _class "form-control"; _required
_autofocus; _placeholder "Start Time"
if model.Index >= 0 then _value model.StartTime ]
label [ _for "start_time" ] [ raw "Start Time" ]
]
textField [ _required; _autofocus ] (nameof model.StartTime) "Start Time"
(if model.Index < 0 then "" else model.StartTime) []
]
div [ _class "col-6 col-lg-3 mb-3" ] [
div [ _class "form-floating" ] [
input [ _type "text"; _id "end_time"; _name "EndTime"; _class "form-control"; _value model.EndTime
_placeholder "End Time" ]
label [ _for "end_time" ] [ raw "End Time" ]
textField [] (nameof model.EndTime) "End Time" model.EndTime [
span [ _class "form-text" ] [ raw "Optional; ends when next starts" ]
]
]
div [ _class "col-12 col-lg-6 mb-3" ] [
div [ _class "form-floating" ] [
input [ _type "text"; _id "title"; _name "Title"; _class "form-control"; _value model.Title
_placeholder "Title" ]
label [ _for "title" ] [ raw "Chapter Title" ]
textField [] (nameof model.Title) "Chapter Title" model.Title [
span [ _class "form-text" ] [ raw "Optional" ]
]
]
div [ _class "col-12 col-lg-6 col-xl-5 mb-3" ] [
div [ _class "form-floating" ] [
input [ _type "text"; _id "image_url"; _name "ImageUrl"; _class "form-control"
_value model.ImageUrl; _placeholder "Image URL" ]
label [ _for "image_url" ] [ raw "Image URL" ]
textField [] (nameof model.ImageUrl) "Image URL" model.ImageUrl [
span [ _class "form-text" ] [
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 "form-floating" ] [
input [ _type "text"; _id "url"; _name "Url"; _class "form-control"; _value model.Url
_placeholder "URL" ]
label [ _for "url" ] [ raw "URL" ]
span [ _class "form-text" ] [
raw "Optional; informational link for this chapter"
]
textField [] (nameof model.Url) "URL" model.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 "form-check form-switch mb-3" ] [
input [ _type "checkbox"; _id "is_hidden"; _name "IsHidden"; _class "form-check-input"
_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" ]
checkboxSwitch [] (nameof model.IsHidden) "Hidden Chapter" model.IsHidden []
span [ _class "mt-2 form-text" ] [ raw "Not displayed, but may update image and location" ]
]
]
div [ _class "row" ] [
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 "form-check form-switch mb-3" ] [
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" ]
]
checkboxSwitch [ _onclick "Admin.checkChapterLocation()" ] "has_location" "Associate Location" hasLoc []
]
div [ _class "col-12 col-md-8 col-lg-6 offset-lg-1 mb-3" ] [
div [ _class "form-floating" ] [
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" ]
]
textField (_required :: attrs) (nameof model.LocationName) "Name" model.LocationName []
]
div [ _class "col-6 col-lg-4 offset-lg-2 mb-3" ] [
div [ _class "form-floating" ] [
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" ]
textField (_required :: attrs) (nameof model.LocationGeo) "Geo URL" model.LocationGeo [
em [ _class "form-text" ] [
a [ _href "https://github.com/Podcastindex-org/podcast-namespace/blob/main/location/location.md#geo-recommended"
_target "_blank"; _rel "noopener" ] [
@ -110,11 +74,7 @@ let chapterEdit (model: EditChapterModel) app = [
]
]
div [ _class "col-6 col-lg-4 mb-3" ] [
div [ _class "form-floating" ] [
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" ]
textField attrs (nameof model.LocationOsm) "OpenStreetMap ID" model.LocationOsm [
em [ _class "form-text" ] [
raw "Optional; "
a [ _href "https://www.openstreetmap.org/"; _target "_blank"; _rel "noopener" ] [ raw "get ID" ]
@ -138,8 +98,7 @@ let chapterEdit (model: EditChapterModel) app = [
]
else
input [ _type "hidden"; _name "AddAnother"; _value "false" ]
button [ _type "submit"; _class "btn btn-primary" ] [ raw "Save" ]
raw " &nbsp; "
saveButton; raw " &nbsp; "
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 "form-floating" ] [
input [ _type "email"; _name "Email"; _id "email"; _class "form-control"; _placeholder "E-mail"
_required; _value model.Email ]
label [ _for "email" ] [ raw "E-mail Address" ]
]
emailField [ _required ] (nameof model.Email) "E-mail Address" model.Email []
]
div [ _class "col-12 col-lg-5 mb-3" ] [
div [ _class "form-floating" ] [
input [ _type "text"; _name "Url"; _id "url"; _class "form-control"; _placeholder "URL"
_value model.Url ]
label [ _for "url" ] [ raw "User&rsquo;s Personal URL" ]
]
textField [] (nameof model.Url) "User&rsquo;s Personal URL" model.Url []
]
]
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 "form-floating" ] [
input [ _type "text"; _name "FirstName"; _id "firstName"; _class "form-control"
_placeholder "First"; _required; _value model.FirstName ]
label [ _for "firstName" ] [ raw "First Name" ]
]
textField [ _required ] (nameof model.FirstName) "First Name" model.FirstName []
]
div [ _class "col-12 col-md-6 col-lg-4 col-xl-3 pb-3" ] [
div [ _class "form-floating" ] [
input [ _type "text"; _name "LastName"; _id "lastName"; _class "form-control"
_placeholder "Last"; _required; _value model.LastName ]
label [ _for "lastName" ] [ raw "Last Name" ]
]
textField [ _required ] (nameof model.LastName) "Last Name" model.LastName []
]
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 " ] [
input [ _type "text"; _name "PreferredName"; _id "preferredName"; _class "form-control"
_placeholder "Preferred"; _required; _value model.PreferredName ]
label [ _for "preferredName" ] [ raw "Preferred Name" ]
]
textField [ _required ] (nameof model.PreferredName) "Preferred Name" model.PreferredName []
]
]
div [ _class "row mb-3" ] [
@ -83,28 +63,12 @@ let edit (model: EditUserModel) app =
]
]
div [ _class "row" ] [
let attrs, newLbl = if model.IsNew then [ _required ], "" else [], "New "
div [ _class "col-12 col-md-6 pb-3" ] [
div [ _class "form-floating" ] [
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"
]
]
passwordField attrs (nameof model.Password) $"{newLbl}Password" "" []
]
div [ _class "col-12 col-md-6 pb-3" ] [
div [ _class "form-floating" ] [
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"
]
]
passwordField attrs (nameof model.PasswordConfirm) $"Confirm {newLbl}Password" "" []
]
]
]
@ -112,7 +76,7 @@ let edit (model: EditUserModel) app =
]
div [ _class "row mb-3" ] [
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
button [ _type "button"; _class "btn btn-sm btn-secondary ms-3"
_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 ]
div [ _class "row" ] [
div [ _class "col-12 col-md-6 col-lg-4 offset-lg-2 pb-3" ] [
div [ _class "form-floating" ] [
input [ _type "email"; _id "email"; _name "EmailAddress"; _class "form-control"; _autofocus
_required ]
label [ _for "email" ] [ rawText "E-mail Address" ]
]
emailField [ _required; _autofocus ] (nameof model.EmailAddress) "E-mail Address" "" []
]
div [ _class "col-12 col-md-6 col-lg-4 pb-3" ] [
div [ _class "form-floating" ] [
input [ _type "password"; _id "password"; _name "Password"; _class "form-control"; _required ]
label [ _for "password" ] [ rawText "Password" ]
]
passwordField [ _required ] (nameof model.Password) "Password" "" []
]
]
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 mb-3" ] [
div [ _class "col-12 col-md-6 col-lg-4 pb-3" ] [
div [ _class "form-floating" ] [
input [ _type "text"; _name "FirstName"; _id "firstName"; _class "form-control"; _autofocus
_required; _placeholder "First"; _value model.FirstName ]
label [ _for "firstName" ] [ raw "First Name" ]
]
textField [ _required; _autofocus ] (nameof model.FirstName) "First Name" model.FirstName []
]
div [ _class "col-12 col-md-6 col-lg-4 pb-3" ] [
div [ _class "form-floating" ] [
input [ _type "text"; _name "LastName"; _id "lastName"; _class "form-control"; _required
_placeholder "Last"; _value model.LastName ]
label [ _for "lastName" ] [ raw "Last Name" ]
]
textField [ _required ] (nameof model.LastName) "Last Name" model.LastName []
]
div [ _class "col-12 col-md-6 col-lg-4 pb-3" ] [
div [ _class "form-floating" ] [
input [ _type "text"; _name "PreferredName"; _id "preferredName"; _class "form-control"
_required; _placeholder "Preferred"; _value model.PreferredName ]
label [ _for "preferredName" ] [ raw "Preferred Name" ]
]
textField [ _required ] (nameof model.PreferredName) "Preferred Name" model.PreferredName []
]
]
div [ _class "row mb-3" ] [
@ -297,28 +242,16 @@ let myInfo (model: EditMyInfoModel) (user: WebLogUser) app = [
]
div [ _class "row" ] [
div [ _class "col-12 col-md-6 pb-3" ] [
div [ _class "form-floating" ] [
input [ _type "password"; _name "NewPassword"; _id "newPassword"
_class "form-control"; _placeholder "Password" ]
label [ _for "newPassword" ] [ raw "New Password" ]
]
passwordField [] (nameof model.NewPassword) "New Password" "" []
]
div [ _class "col-12 col-md-6 pb-3" ] [
div [ _class "form-floating" ] [
input [ _type "password"; _name "NewPasswordConfirm"; _id "newPasswordConfirm"
_class "form-control"; _placeholder "Confirm" ]
label [ _for "newPasswordConfirm" ] [ raw "Confirm New Password" ]
passwordField [] (nameof model.NewPasswordConfirm) "Confirm New Password" "" []
]
]
]
]
]
]
div [ _class "row" ] [
div [ _class "col text-center mb-3" ] [
button [ _type "submit"; _class "btn btn-primary" ] [ raw "Save Changes" ]
]
]
div [ _class "row" ] [ div [ _class "col text-center mb-3" ] [ saveButton ] ]
]
]
]

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>