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,
|
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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
|
|
|
@ -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…</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…</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>
|
||||||
|
|
|
@ -5,6 +5,5 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{% include_template "_layout" %}
|
{% include_template "_layout" %}
|
||||||
<script>Admin.dismissSuccesses()</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -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 }} « Admin « {{ web_log.name | strip_html }}</title>
|
<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"
|
<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>
|
||||||
|
|
|
@ -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>
|
<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> •
|
Go to: <a href="#users">Users</a> • <a href="#rss-settings">RSS Settings</a> •
|
||||||
<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 “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>
|
</article>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 -%}
|
||||||
|
|
|
@ -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 {
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user