Change alerts to toasts (#25)

- Upgrade to Bootstrap 5.1.3
- Move RSS settings and tag mappings to web log settings (#25)
- Fix parameters in 2 SQLite queries
This commit is contained in:
Daniel J. Summers 2022-07-27 21:38:46 -04:00
parent a8386d6c97
commit 6b49793fbb
16 changed files with 278 additions and 258 deletions

View File

@ -328,7 +328,7 @@ type SQLitePageData (conn : SqliteConnection) =
is_in_page_list = @isInPageList, is_in_page_list = @isInPageList,
template = @template, template = @template,
page_text = @text page_text = @text
WHERE id = @pageId WHERE id = @id
AND web_log_id = @webLogId""" AND web_log_id = @webLogId"""
addPageParameters cmd page addPageParameters cmd page
do! write cmd do! write cmd

View File

@ -320,6 +320,7 @@ type SQLiteWebLogData (conn : SqliteConnection) =
copyright = @copyright copyright = @copyright
WHERE id = @id""" WHERE id = @id"""
addWebLogRssParameters cmd webLog addWebLogRssParameters cmd webLog
cmd.Parameters.AddWithValue ("@id", WebLogId.toString webLog.Id) |> ignore
do! write cmd do! write cmd
do! updateCustomFeeds webLog do! updateCustomFeeds webLog
} }

View File

@ -198,31 +198,20 @@ open Microsoft.AspNetCore.Http
// ~~ TAG MAPPINGS ~~ // ~~ TAG MAPPINGS ~~
/// Get the hash necessary to render the tag mapping list /// Add tag mappings to the given hash
let private tagMappingHash (ctx : HttpContext) = task { let private withTagMappings (ctx : HttpContext) hash = task {
let! mappings = ctx.Data.TagMap.FindByWebLog ctx.WebLog.Id let! mappings = ctx.Data.TagMap.FindByWebLog ctx.WebLog.Id
return! return
hashForPage "Tag Mappings" addToHash "mappings" mappings hash
|> withAntiCsrf ctx
|> addToHash "mappings" mappings
|> addToHash "mapping_ids" (mappings |> List.map (fun it -> { Name = it.Tag; Value = TagMapId.toString it.Id })) |> addToHash "mapping_ids" (mappings |> List.map (fun it -> { Name = it.Tag; Value = TagMapId.toString it.Id }))
|> addViewContext ctx
} }
// GET /admin/settings/tag-mappings // GET /admin/settings/tag-mappings
let tagMappings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task { let tagMappings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
match! TemplateCache.get adminTheme "tag-mapping-list-body" ctx.Data with let! hash =
| Ok listTemplate -> hashForPage ""
let! hash = tagMappingHash ctx |> withAntiCsrf ctx
return! |> withTagMappings ctx
addToHash "tag_mapping_list" (listTemplate.Render hash) hash
|> adminView "tag-mapping-list" next ctx
| Error message -> return! Error.server message next ctx
}
// GET /admin/settings/tag-mappings/bare
let tagMappingsBare : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
let! hash = tagMappingHash ctx
return! adminBareView "tag-mapping-list-body" next ctx hash return! adminBareView "tag-mapping-list-body" next ctx hash
} }
@ -253,7 +242,7 @@ let saveMapping : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> ta
| Some tm -> | Some tm ->
do! data.TagMap.Save { tm with Tag = model.Tag.ToLower (); UrlValue = model.UrlValue.ToLower () } do! data.TagMap.Save { tm with Tag = model.Tag.ToLower (); UrlValue = model.UrlValue.ToLower () }
do! addMessage ctx { UserMessage.success with Message = "Tag mapping saved successfully" } do! addMessage ctx { UserMessage.success with Message = "Tag mapping saved successfully" }
return! tagMappingsBare next ctx return! tagMappings next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
} }
@ -262,7 +251,7 @@ let deleteMapping tagMapId : HttpHandler = requireAccess WebLogAdmin >=> fun nex
match! ctx.Data.TagMap.Delete (TagMapId tagMapId) ctx.WebLog.Id with match! ctx.Data.TagMap.Delete (TagMapId tagMapId) ctx.WebLog.Id with
| true -> do! addMessage ctx { UserMessage.success with Message = "Tag mapping deleted successfully" } | true -> do! addMessage ctx { UserMessage.success with Message = "Tag mapping deleted successfully" }
| false -> do! addMessage ctx { UserMessage.error with Message = "Tag mapping not found; nothing deleted" } | false -> do! addMessage ctx { UserMessage.error with Message = "Tag mapping not found; nothing deleted" }
return! tagMappingsBare next ctx return! tagMappings next ctx
} }
// ~~ THEMES ~~ // ~~ THEMES ~~
@ -433,35 +422,45 @@ let settings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task
let data = ctx.Data let data = ctx.Data
match! TemplateCache.get adminTheme "user-list-body" data with match! TemplateCache.get adminTheme "user-list-body" data with
| Ok userTemplate -> | Ok userTemplate ->
let! allPages = data.Page.All ctx.WebLog.Id match! TemplateCache.get adminTheme "tag-mapping-list-body" ctx.Data with
let! themes = data.Theme.All () | Ok tagMapTemplate ->
let! users = data.WebLogUser.FindByWebLog ctx.WebLog.Id let! allPages = data.Page.All ctx.WebLog.Id
let! hash = let! themes = data.Theme.All ()
hashForPage "Web Log Settings" let! users = data.WebLogUser.FindByWebLog ctx.WebLog.Id
|> withAntiCsrf ctx let! hash =
|> addToHash ViewContext.Model (SettingsModel.fromWebLog ctx.WebLog) hashForPage "Web Log Settings"
|> addToHash "pages" ( |> withAntiCsrf ctx
seq { |> addToHash ViewContext.Model (SettingsModel.fromWebLog ctx.WebLog)
KeyValuePair.Create ("posts", "- First Page of Posts -") |> addToHash "pages" (
yield! allPages seq {
|> List.sortBy (fun p -> p.Title.ToLower ()) KeyValuePair.Create ("posts", "- First Page of Posts -")
|> List.map (fun p -> KeyValuePair.Create (PageId.toString p.Id, p.Title)) yield! allPages
} |> List.sortBy (fun p -> p.Title.ToLower ())
|> Array.ofSeq) |> List.map (fun p -> KeyValuePair.Create (PageId.toString p.Id, p.Title))
|> addToHash "themes" ( }
themes |> Array.ofSeq)
|> Seq.ofList |> addToHash "themes" (
|> Seq.map (fun it -> KeyValuePair.Create (ThemeId.toString it.Id, $"{it.Name} (v{it.Version})")) themes
|> Array.ofSeq) |> Seq.ofList
|> addToHash "upload_values" [| |> Seq.map (fun it -> KeyValuePair.Create (ThemeId.toString it.Id, $"{it.Name} (v{it.Version})"))
KeyValuePair.Create (UploadDestination.toString Database, "Database") |> Array.ofSeq)
KeyValuePair.Create (UploadDestination.toString Disk, "Disk") |> addToHash "upload_values" [|
|] KeyValuePair.Create (UploadDestination.toString Database, "Database")
|> addToHash "users" (users |> List.map (DisplayUser.fromUser ctx.WebLog) |> Array.ofList) KeyValuePair.Create (UploadDestination.toString Disk, "Disk")
|> addViewContext ctx |]
return! |> addToHash "users" (users |> List.map (DisplayUser.fromUser ctx.WebLog) |> Array.ofList)
addToHash "user_list" (userTemplate.Render hash) hash |> addToHash "rss_model" (EditRssModel.fromRssOptions ctx.WebLog.Rss)
|> adminView "settings" next ctx |> addToHash "custom_feeds" (
ctx.WebLog.Rss.CustomFeeds
|> List.map (DisplayCustomFeed.fromFeed (CategoryCache.get ctx))
|> Array.ofList)
|> addViewContext ctx
let! hash' = withTagMappings ctx hash
return!
addToHash "user_list" (userTemplate.Render hash') hash'
|> addToHash "tag_mapping_list" (tagMapTemplate.Render hash')
|> adminView "settings" next ctx
| Error message -> return! Error.server message next ctx
| Error message -> return! Error.server message next ctx | Error message -> return! Error.server message next ctx
} }

View File

@ -414,17 +414,6 @@ let generate (feedType : FeedType) postCount : HttpHandler = fun next ctx -> bac
// ~~ FEED ADMINISTRATION ~~ // ~~ FEED ADMINISTRATION ~~
// GET /admin/settings/rss
let editSettings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx ->
hashForPage "RSS Settings"
|> withAntiCsrf ctx
|> addToHash ViewContext.Model (EditRssModel.fromRssOptions ctx.WebLog.Rss)
|> addToHash "custom_feeds" (
ctx.WebLog.Rss.CustomFeeds
|> List.map (DisplayCustomFeed.fromFeed (CategoryCache.get ctx))
|> Array.ofList)
|> adminView "rss-settings" next ctx
// POST /admin/settings/rss // POST /admin/settings/rss
let saveSettings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task { let saveSettings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
let data = ctx.Data let data = ctx.Data
@ -435,7 +424,7 @@ let saveSettings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> t
do! data.WebLog.UpdateRssOptions webLog do! data.WebLog.UpdateRssOptions webLog
WebLogCache.set webLog WebLogCache.set webLog
do! addMessage ctx { UserMessage.success with Message = "RSS settings updated successfully" } do! addMessage ctx { UserMessage.success with Message = "RSS settings updated successfully" }
return! redirectToGet "admin/settings/rss" next ctx return! redirectToGet "admin/settings#rss-settings" next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
} }
@ -507,6 +496,6 @@ let deleteCustomFeed feedId : HttpHandler = requireAccess WebLogAdmin >=> fun ne
do! addMessage ctx { UserMessage.success with Message = "Custom feed deleted successfully" } do! addMessage ctx { UserMessage.success with Message = "Custom feed deleted successfully" }
else else
do! addMessage ctx { UserMessage.warning with Message = "Custom feed not found; no action taken" } do! addMessage ctx { UserMessage.warning with Message = "Custom feed not found; no action taken" }
return! redirectToGet "admin/settings/rss" next ctx return! redirectToGet "admin/settings#rss-settings" next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
} }

View File

@ -343,7 +343,6 @@ let validateCsrf : HttpHandler = fun next ctx -> task {
| false -> return! RequestErrors.BAD_REQUEST "CSRF token invalid" earlyReturn ctx | false -> return! RequestErrors.BAD_REQUEST "CSRF token invalid" earlyReturn ctx
} }
/// Require a user to be logged on /// Require a user to be logged on
let requireUser : HttpHandler = requiresAuthentication Error.notAuthorized let requireUser : HttpHandler = requiresAuthentication Error.notAuthorized

View File

@ -131,19 +131,14 @@ let router : HttpHandler = choose [
routef "/%s/revisions" Post.editRevisions routef "/%s/revisions" Post.editRevisions
]) ])
subRoute "/settings" (choose [ subRoute "/settings" (choose [
route "" >=> Admin.settings route "" >=> Admin.settings
subRoute "/rss" (choose [ routef "/rss/%s/edit" Feed.editCustomFeed
route "" >=> Feed.editSettings
routef "/%s/edit" Feed.editCustomFeed
])
subRoute "/user" (choose [ subRoute "/user" (choose [
route "s" >=> User.all route "s" >=> User.all
routef "/%s/edit" User.edit routef "/%s/edit" User.edit
]) ])
subRoute "/tag-mapping" (choose [ subRoute "/tag-mapping" (choose [
route "s" >=> Admin.tagMappings route "s" >=> Admin.tagMappings
route "s/bare" >=> Admin.tagMappingsBare
routef "/%s/edit" Admin.editMapping routef "/%s/edit" Admin.editMapping
]) ])
]) ])

View File

@ -26,7 +26,7 @@
{%- endif %} {%- endif %}
<ul class="navbar-nav flex-grow-1 justify-content-end"> <ul class="navbar-nav flex-grow-1 justify-content-end">
{%- if is_logged_on %} {%- if is_logged_on %}
{{ "admin/user/my-info" | nav_link: "My Info" }} {{ "admin/my-info" | nav_link: "My Info" }}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="https://bitbadger.solutions/open-source/myweblog/#how-to-use-myweblog" <a class="nav-link" href="https://bitbadger.solutions/open-source/myweblog/#how-to-use-myweblog"
target="_blank"> target="_blank">
@ -50,30 +50,36 @@
</div> </div>
</nav> </nav>
</header> </header>
<main class="mx-3 mt-3"> <div id="toastHost" class="position-fixed top-0 w-100" aria-live="polite" aria-atomic="true">
<div class="load-overlay p-5" id="loadOverlay"><h1 class="p-3">Loading&hellip;</h1></div> <div id="toasts" class="toast-container position-absolute p-3 mt-5 top-0 end-0">
<div class="messages mt-2" id="msgContainer">
{% for msg in messages %} {% for msg in messages %}
<div role="alert" class="alert alert-{{ msg.level }} alert-dismissible fade show"> <div class="toast" role="alert" aria-live="assertive" aria-atomic="true"
{{ msg.message }} {%- unless msg.level == "success" %} data-bs-autohide="false"{% endunless %}>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <div class="toast-header bg-{{ msg.level }}{% unless msg.level == "warning" %} text-white{% endunless %}">
{% if msg.detail %} <strong class="me-auto text-uppercase">
<hr> {% if msg.level == "danger" %}error{% else %}{{ msg.level}}{% endif %}
{{ msg.detail.value }} </strong>
{% endif %} <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body bg-{{ msg.level }} bg-opacity-25">
{{ msg.message }}
{%- if msg.detail %}
<hr>
{{ msg.detail.value }}
{%- endif %}
</div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
</div>
<main class="mx-3 mt-3">
<div class="load-overlay p-5" id="loadOverlay"><h1 class="p-3">Loading&hellip;</h1></div>
{{ content }} {{ content }}
</main> </main>
<footer class="position-fixed bottom-0 w-100"> <footer class="position-fixed bottom-0 w-100">
<div class="container-fluid"> <div class="text-end text-white me-2">
<div class="row"> {%- assign version = generator | split: " " -%}
<div class="col-xs-12 text-end"> <small class="me-1 align-baseline">v{{ version[1] }}</small>
{%- assign version = generator | split: " " -%} <img src="{{ "themes/admin/logo-light.png" | relative_link }}" alt="myWebLog" width="120" height="34">
<small class="me-1 align-baseline">v{{ version[1] }}</small>
<img src="{{ "themes/admin/logo-light.png" | relative_link }}" alt="myWebLog" width="120" height="34">
</div>
</div>
</div> </div>
</footer> </footer>

View File

@ -5,6 +5,5 @@
</head> </head>
<body> <body>
{% include_template "_layout" %} {% include_template "_layout" %}
<script>Admin.dismissSuccesses()</script>
</body> </body>
</html> </html>

View File

@ -4,14 +4,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="generator" content="{{ generator }}"> <meta name="generator" content="{{ generator }}">
<title>{{ page_title | strip_html }} &laquo; Admin &laquo; {{ web_log.name | strip_html }}</title> <title>{{ page_title | strip_html }} &laquo; Admin &laquo; {{ web_log.name | strip_html }}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous"> integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<link rel="stylesheet" href="{{ "themes/admin/admin.css" | relative_link }}"> <link rel="stylesheet" href="{{ "themes/admin/admin.css" | relative_link }}">
</head> </head>
<body hx-boost="true" hx-indicator="#loadOverlay"> <body hx-boost="true" hx-indicator="#loadOverlay">
{% include_template "_layout" %} {% include_template "_layout" %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
{{ htmx_script }} {{ htmx_script }}
<script> <script>
@ -27,6 +27,5 @@
}, 2000) }, 2000)
</script> </script>
<script src="{{ "themes/admin/admin.js" | relative_link }}"></script> <script src="{{ "themes/admin/admin.js" | relative_link }}"></script>
<script>Admin.dismissSuccesses()</script>
</body> </body>
</html> </html>

View File

@ -1,116 +0,0 @@
<h2 class="my-3">{{ page_title }}</h2>
<article>
<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 model.is_feed_enabled %} checked="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 model.is_category_enabled %} checked="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 model.tag_enabled %} checked="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="{{ 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="{{ 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="{{ 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>
<div class="row">
<div class="col">
<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</p>
{% endif %}
</div>
</div>
</fieldset>
</article>

View File

@ -1,8 +1,8 @@
<h2 class="my-3">{{ web_log.name }} Settings</h2> <h2 class="my-3">{{ web_log.name }} Settings</h2>
<article> <article>
<p class="text-muted"> <p class="text-muted">
Other Settings: <a href="{{ "admin/settings/tag-mappings" | relative_link }}">Tag Mappings</a> &bull; Go to: <a href="#users">Users</a> &bull; <a href="#rss-settings">RSS Settings</a> &bull;
<a href="{{ "admin/settings/rss" | relative_link }}">RSS Settings</a> <a href="#tag-mappings">Tag Mappings</a>
</p> </p>
<fieldset class="container mb-3"> <fieldset class="container mb-3">
<legend>Web Log Settings</legend> <legend>Web Log Settings</legend>
@ -112,7 +112,7 @@
</div> </div>
</div> </div>
</fieldset> </fieldset>
<fieldset class="container mb-3 pb-0"> <fieldset id="users" class="container mb-3 pb-0">
<legend>Users</legend> <legend>Users</legend>
<div class="row"> <div class="row">
<div class="col"> <div class="col">
@ -133,4 +133,136 @@
</div> </div>
</div> </div>
</fieldset> </fieldset>
<fieldset id="rss-settings" class="container mb-3 pb-0">
<legend>RSS Settings</legend>
<div class="row">
<div class="col">
<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="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="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.tag_enabled %} checked="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>
<div class="row">
<div class="col">
<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</p>
{% endif %}
</div>
</div>
</fieldset>
</div>
</div>
</fieldset>
<fieldset id="tag-mappings" class="container mb-3 pb-0">
<legend>Tag Mappings</legend>
<div class="row">
<div class="col">
<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>
{{ tag_mapping_list }}
</div>
</div>
</fieldset>
</article> </article>

View File

@ -22,7 +22,7 @@
<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">Save Changes</button> <button type="submit" class="btn btn-sm btn-primary">Save Changes</button>
<a href="{{ "admin/settings/tag-mappings/bare" | relative_link }}" class="btn btn-sm btn-secondary ms-3"> <a href="{{ "admin/settings/tag-mappings" | relative_link }}" class="btn btn-sm btn-secondary ms-3">
Cancel Cancel
</a> </a>
</div> </div>

View File

@ -9,7 +9,7 @@
<div class="col">URL Value</div> <div class="col">URL Value</div>
</div> </div>
</div> </div>
<form method="post" class="container" hx-target="#tagList" hx-swap="outerHTML show:window:top"> <form method="post" class="container" hx-target="#tagList" hx-swap="outerHTML">
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}"> <input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
<div class="row mwl-table-detail" id="tag_new"></div> <div class="row mwl-table-detail" id="tag_new"></div>
{% for map in mappings -%} {% for map in mappings -%}

View File

@ -1,8 +0,0 @@
<h2 class="my-3">{{ page_title }}</h2>
<article>
<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>
{{ tag_mapping_list }}
</article>

View File

@ -29,7 +29,6 @@ header nav {
footer { footer {
background-color: #808080; background-color: #808080;
border-top: solid 1px black; border-top: solid 1px black;
color: white;
} }
.messages { .messages {
max-width: 60rem; max-width: 60rem;
@ -93,24 +92,26 @@ a.text-danger:link:hover, a.text-danger:visited:hover {
padding: .5rem; padding: .5rem;
} }
.load-overlay { .load-overlay {
position: fixed;
background-color: rgba(0, 0, 0, .25);
color: white;
z-index: 100;
display: none; display: none;
position: fixed;
top: 55px; top: 55px;
left: 0; left: 0;
z-index: 100;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: rgba(0, 0, 0, .1);
transition: ease-in-out .5s; transition: ease-in-out .5s;
} }
.load-overlay h1 { .load-overlay h1 {
background-color: rgba(0, 0, 0, .75); background-color: rgba(204, 204, 0, .95);
height: fit-content; height: fit-content;
border: solid 6px white; border: solid 6px darkgreen;
border-radius: .5rem; border-radius: 2rem;
} }
.load-overlay.htmx-request { .load-overlay.htmx-request {
display: flex; display: flex;
flex-flow: row; flex-flow: row;
} }
#toastHost {
z-index: 5;
}

View File

@ -293,33 +293,46 @@ this.Admin = {
const parts = msg.split("|||") const parts = msg.split("|||")
if (parts.length < 2) return if (parts.length < 2) return
const msgDiv = document.createElement("div") // Create the toast header
msgDiv.className = `alert alert-${parts[0]} alert-dismissible fade show` const toastType = document.createElement("strong")
msgDiv.setAttribute("role", "alert") toastType.className = "me-auto text-uppercase"
msgDiv.innerHTML = parts[1] toastType.innerText = parts[0] === "danger" ? "error" : parts[0]
const closeBtn = document.createElement("button") const closeBtn = document.createElement("button")
closeBtn.type = "button" closeBtn.type = "button"
closeBtn.className = "btn-close" closeBtn.className = "btn-close"
closeBtn.setAttribute("data-bs-dismiss", "alert") closeBtn.setAttribute("data-bs-dismiss", "toast")
closeBtn.setAttribute("aria-label", "Close") closeBtn.setAttribute("aria-label", "Close")
msgDiv.appendChild(closeBtn)
const toastHead = document.createElement("div")
toastHead.className = `toast-header bg-${parts[0]}${parts[0] === "warning" ? "" : " text-white"}`
toastHead.appendChild(toastType)
toastHead.appendChild(closeBtn)
// Create the toast body
const toastBody = document.createElement("div")
toastBody.className = `toast-body bg-${parts[0]} bg-opacity-25`
toastBody.innerHTML = parts[1]
if (parts.length === 3) { if (parts.length === 3) {
msgDiv.innerHTML += `<hr>${parts[2]}` toastBody.innerHTML += `<hr>${parts[2]}`
} }
document.getElementById("msgContainer").appendChild(msgDiv)
})
},
/** // Assemble the toast
* Set all "success" alerts to close after 4 seconds const toast = document.createElement("div")
*/ toast.className = "toast"
dismissSuccesses() { toast.setAttribute("role", "alert")
[...document.querySelectorAll(".alert-success")].forEach(alert => { toast.setAttribute("aria-live", "assertive")
setTimeout(() => { toast.setAttribute("aria-atomic", "true")
(bootstrap.Alert.getInstance(alert) ?? new bootstrap.Alert(alert)).close() toast.appendChild(toastHead)
}, 4000) toast.appendChild(toastBody)
document.getElementById("toasts").appendChild(toast)
let options = { delay: 4000 }
if (parts[0] !== "success") options.autohide = false
const theToast = new bootstrap.Toast(toast, options)
theToast.show()
}) })
} }
} }
@ -329,8 +342,19 @@ htmx.on("htmx:afterOnLoad", function (evt) {
// Show messages if there were any in the response // Show messages if there were any in the response
if (hdrs.indexOf("x-message") >= 0) { if (hdrs.indexOf("x-message") >= 0) {
Admin.showMessage(evt.detail.xhr.getResponseHeader("x-message")) Admin.showMessage(evt.detail.xhr.getResponseHeader("x-message"))
Admin.dismissSuccesses()
} }
// Initialize any toasts that were pre-rendered from the server
[...document.querySelectorAll(".toast")].forEach(el => {
if (el.getAttribute("data-mwl-shown") === "true" && el.className.indexOf("hide") >= 0) {
document.removeChild(el)
} else {
const toast = new bootstrap.Toast(el,
el.getAttribute("data-bs-autohide") === "false"
? { autohide: false } : { delay: 6000, autohide: true })
toast.show()
el.setAttribute("data-mwl-shown", "true")
}
})
}) })
htmx.on("htmx:responseError", function (evt) { htmx.on("htmx:responseError", function (evt) {