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,
template = @template,
page_text = @text
WHERE id = @pageId
WHERE id = @id
AND web_log_id = @webLogId"""
addPageParameters cmd page
do! write cmd

View File

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

View File

@ -198,31 +198,20 @@ open Microsoft.AspNetCore.Http
// ~~ TAG MAPPINGS ~~
/// Get the hash necessary to render the tag mapping list
let private tagMappingHash (ctx : HttpContext) = task {
/// Add tag mappings to the given hash
let private withTagMappings (ctx : HttpContext) hash = task {
let! mappings = ctx.Data.TagMap.FindByWebLog ctx.WebLog.Id
return!
hashForPage "Tag Mappings"
|> withAntiCsrf ctx
|> addToHash "mappings" mappings
return
addToHash "mappings" mappings hash
|> addToHash "mapping_ids" (mappings |> List.map (fun it -> { Name = it.Tag; Value = TagMapId.toString it.Id }))
|> addViewContext ctx
}
// GET /admin/settings/tag-mappings
let tagMappings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
match! TemplateCache.get adminTheme "tag-mapping-list-body" ctx.Data with
| Ok listTemplate ->
let! hash = tagMappingHash ctx
return!
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
let! hash =
hashForPage ""
|> withAntiCsrf ctx
|> withTagMappings ctx
return! adminBareView "tag-mapping-list-body" next ctx hash
}
@ -253,7 +242,7 @@ let saveMapping : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> ta
| Some tm ->
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" }
return! tagMappingsBare next ctx
return! tagMappings 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
| 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" }
return! tagMappingsBare next ctx
return! tagMappings next ctx
}
// ~~ THEMES ~~
@ -433,35 +422,45 @@ let settings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task
let data = ctx.Data
match! TemplateCache.get adminTheme "user-list-body" data with
| Ok userTemplate ->
let! allPages = data.Page.All ctx.WebLog.Id
let! themes = data.Theme.All ()
let! users = data.WebLogUser.FindByWebLog ctx.WebLog.Id
let! hash =
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
|> List.sortBy (fun p -> p.Title.ToLower ())
|> List.map (fun p -> KeyValuePair.Create (PageId.toString p.Id, p.Title))
}
|> Array.ofSeq)
|> addToHash "themes" (
themes
|> Seq.ofList
|> Seq.map (fun it -> KeyValuePair.Create (ThemeId.toString it.Id, $"{it.Name} (v{it.Version})"))
|> Array.ofSeq)
|> addToHash "upload_values" [|
KeyValuePair.Create (UploadDestination.toString Database, "Database")
KeyValuePair.Create (UploadDestination.toString Disk, "Disk")
|]
|> addToHash "users" (users |> List.map (DisplayUser.fromUser ctx.WebLog) |> Array.ofList)
|> addViewContext ctx
return!
addToHash "user_list" (userTemplate.Render hash) hash
|> adminView "settings" next ctx
match! TemplateCache.get adminTheme "tag-mapping-list-body" ctx.Data with
| Ok tagMapTemplate ->
let! allPages = data.Page.All ctx.WebLog.Id
let! themes = data.Theme.All ()
let! users = data.WebLogUser.FindByWebLog ctx.WebLog.Id
let! hash =
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
|> List.sortBy (fun p -> p.Title.ToLower ())
|> List.map (fun p -> KeyValuePair.Create (PageId.toString p.Id, p.Title))
}
|> Array.ofSeq)
|> addToHash "themes" (
themes
|> Seq.ofList
|> Seq.map (fun it -> KeyValuePair.Create (ThemeId.toString it.Id, $"{it.Name} (v{it.Version})"))
|> Array.ofSeq)
|> addToHash "upload_values" [|
KeyValuePair.Create (UploadDestination.toString Database, "Database")
KeyValuePair.Create (UploadDestination.toString Disk, "Disk")
|]
|> addToHash "users" (users |> List.map (DisplayUser.fromUser ctx.WebLog) |> Array.ofList)
|> addToHash "rss_model" (EditRssModel.fromRssOptions ctx.WebLog.Rss)
|> 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
}

View File

@ -414,17 +414,6 @@ let generate (feedType : FeedType) postCount : HttpHandler = fun next ctx -> bac
// ~~ 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
let saveSettings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
let data = ctx.Data
@ -435,7 +424,7 @@ let saveSettings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> t
do! data.WebLog.UpdateRssOptions webLog
WebLogCache.set webLog
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
}
@ -507,6 +496,6 @@ let deleteCustomFeed feedId : HttpHandler = requireAccess WebLogAdmin >=> fun ne
do! addMessage ctx { UserMessage.success with Message = "Custom feed deleted successfully" }
else
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
}

View File

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

View File

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

View File

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

View File

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

View File

@ -4,14 +4,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="generator" content="{{ generator }}">
<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"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<link rel="stylesheet" href="{{ "themes/admin/admin.css" | relative_link }}">
</head>
<body hx-boost="true" hx-indicator="#loadOverlay">
{% include_template "_layout" %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
crossorigin="anonymous"></script>
{{ htmx_script }}
<script>
@ -27,6 +27,5 @@
}, 2000)
</script>
<script src="{{ "themes/admin/admin.js" | relative_link }}"></script>
<script>Admin.dismissSuccesses()</script>
</body>
</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>
<article>
<p class="text-muted">
Other Settings: <a href="{{ "admin/settings/tag-mappings" | relative_link }}">Tag Mappings</a> &bull;
<a href="{{ "admin/settings/rss" | relative_link }}">RSS Settings</a>
Go to: <a href="#users">Users</a> &bull; <a href="#rss-settings">RSS Settings</a> &bull;
<a href="#tag-mappings">Tag Mappings</a>
</p>
<fieldset class="container mb-3">
<legend>Web Log Settings</legend>
@ -112,7 +112,7 @@
</div>
</div>
</fieldset>
<fieldset class="container mb-3 pb-0">
<fieldset id="users" class="container mb-3 pb-0">
<legend>Users</legend>
<div class="row">
<div class="col">
@ -133,4 +133,136 @@
</div>
</div>
</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>

View File

@ -22,7 +22,7 @@
<div class="row mb-3">
<div class="col text-center">
<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
</a>
</div>

View File

@ -9,7 +9,7 @@
<div class="col">URL Value</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 }}">
<div class="row mwl-table-detail" id="tag_new"></div>
{% 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 {
background-color: #808080;
border-top: solid 1px black;
color: white;
}
.messages {
max-width: 60rem;
@ -93,24 +92,26 @@ a.text-danger:link:hover, a.text-danger:visited:hover {
padding: .5rem;
}
.load-overlay {
position: fixed;
background-color: rgba(0, 0, 0, .25);
color: white;
z-index: 100;
display: none;
position: fixed;
top: 55px;
left: 0;
z-index: 100;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, .1);
transition: ease-in-out .5s;
}
.load-overlay h1 {
background-color: rgba(0, 0, 0, .75);
background-color: rgba(204, 204, 0, .95);
height: fit-content;
border: solid 6px white;
border-radius: .5rem;
border: solid 6px darkgreen;
border-radius: 2rem;
}
.load-overlay.htmx-request {
display: flex;
flex-flow: row;
}
#toastHost {
z-index: 5;
}

View File

@ -293,33 +293,46 @@ this.Admin = {
const parts = msg.split("|||")
if (parts.length < 2) return
const msgDiv = document.createElement("div")
msgDiv.className = `alert alert-${parts[0]} alert-dismissible fade show`
msgDiv.setAttribute("role", "alert")
msgDiv.innerHTML = parts[1]
// Create the toast header
const toastType = document.createElement("strong")
toastType.className = "me-auto text-uppercase"
toastType.innerText = parts[0] === "danger" ? "error" : parts[0]
const closeBtn = document.createElement("button")
closeBtn.type = "button"
closeBtn.className = "btn-close"
closeBtn.setAttribute("data-bs-dismiss", "alert")
closeBtn.setAttribute("data-bs-dismiss", "toast")
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) {
msgDiv.innerHTML += `<hr>${parts[2]}`
toastBody.innerHTML += `<hr>${parts[2]}`
}
document.getElementById("msgContainer").appendChild(msgDiv)
})
},
/**
* Set all "success" alerts to close after 4 seconds
*/
dismissSuccesses() {
[...document.querySelectorAll(".alert-success")].forEach(alert => {
setTimeout(() => {
(bootstrap.Alert.getInstance(alert) ?? new bootstrap.Alert(alert)).close()
}, 4000)
// Assemble the toast
const toast = document.createElement("div")
toast.className = "toast"
toast.setAttribute("role", "alert")
toast.setAttribute("aria-live", "assertive")
toast.setAttribute("aria-atomic", "true")
toast.appendChild(toastHead)
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
if (hdrs.indexOf("x-message") >= 0) {
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) {