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:
parent
a8386d6c97
commit
6b49793fbb
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
])
|
||||
])
|
||||
|
@ -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…</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…</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>
|
||||
|
@ -5,6 +5,5 @@
|
||||
</head>
|
||||
<body>
|
||||
{% include_template "_layout" %}
|
||||
<script>Admin.dismissSuccesses()</script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -4,14 +4,14 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="generator" content="{{ generator }}">
|
||||
<title>{{ page_title | strip_html }} « Admin « {{ 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>
|
||||
|
@ -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 “0” to use “Posts per Page” 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 %} <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"> • </span>
|
||||
<a href="{{ feed_url | append: "/edit" | relative_link }}">Edit</a>
|
||||
<span class="text-muted"> • </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>
|
@ -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> •
|
||||
<a href="{{ "admin/settings/rss" | relative_link }}">RSS Settings</a>
|
||||
Go to: <a href="#users">Users</a> • <a href="#rss-settings">RSS Settings</a> •
|
||||
<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 “0” to use “Posts per Page” 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 %} <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"> • </span>
|
||||
<a href="{{ feed_url | append: "/edit" | relative_link }}">Edit</a>
|
||||
<span class="text-muted"> • </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>
|
||||
|
@ -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>
|
||||
|
@ -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 -%}
|
||||
|
@ -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>
|
@ -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;
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
if (parts.length === 3) {
|
||||
msgDiv.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)
|
||||
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) {
|
||||
toastBody.innerHTML += `<hr>${parts[2]}`
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
Loading…
Reference in New Issue
Block a user