Revise admin area with htmx
- Replace tables with grids (much better on mobile) - Category / tag mapping now edit inline - Messages shown in all request forms - Success messages auto-hide at 4 seconds
This commit is contained in:
parent
2a796042ac
commit
c2b6d7c82c
|
@ -264,7 +264,7 @@ type WebLog =
|
||||||
/// The number of posts to display on pages of posts
|
/// The number of posts to display on pages of posts
|
||||||
postsPerPage : int
|
postsPerPage : int
|
||||||
|
|
||||||
/// The path of the theme (within /views/themes)
|
/// The path of the theme (within /themes)
|
||||||
themePath : string
|
themePath : string
|
||||||
|
|
||||||
/// The URL base
|
/// The URL base
|
||||||
|
|
|
@ -49,15 +49,28 @@ let dashboard : HttpHandler = fun next ctx -> task {
|
||||||
|
|
||||||
// GET /admin/categories
|
// GET /admin/categories
|
||||||
let listCategories : HttpHandler = fun next ctx -> task {
|
let listCategories : HttpHandler = fun next ctx -> task {
|
||||||
return!
|
let! catListTemplate = TemplateCache.get "admin" "category-list-body"
|
||||||
Hash.FromAnonymousObject {|
|
let hash = Hash.FromAnonymousObject {|
|
||||||
|
web_log = ctx.WebLog
|
||||||
categories = CategoryCache.get ctx
|
categories = CategoryCache.get ctx
|
||||||
page_title = "Categories"
|
page_title = "Categories"
|
||||||
csrf = csrfToken ctx
|
csrf = csrfToken ctx
|
||||||
|}
|
|}
|
||||||
|> viewForTheme "admin" "category-list" next ctx
|
hash.Add ("category_list", catListTemplate.Render hash)
|
||||||
|
return! viewForTheme "admin" "category-list" next ctx hash
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET /admin/categories/bare
|
||||||
|
let listCategoriesBare : HttpHandler = fun next ctx -> task {
|
||||||
|
return!
|
||||||
|
Hash.FromAnonymousObject {|
|
||||||
|
categories = CategoryCache.get ctx
|
||||||
|
csrf = csrfToken ctx
|
||||||
|
|}
|
||||||
|
|> bareForTheme "admin" "category-list-body" next ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// GET /admin/category/{id}/edit
|
// GET /admin/category/{id}/edit
|
||||||
let editCategory catId : HttpHandler = fun next ctx -> task {
|
let editCategory catId : HttpHandler = fun next ctx -> task {
|
||||||
let! result = task {
|
let! result = task {
|
||||||
|
@ -77,7 +90,7 @@ let editCategory catId : HttpHandler = fun next ctx -> task {
|
||||||
page_title = title
|
page_title = title
|
||||||
categories = CategoryCache.get ctx
|
categories = CategoryCache.get ctx
|
||||||
|}
|
|}
|
||||||
|> viewForTheme "admin" "category-edit" next ctx
|
|> bareForTheme "admin" "category-edit" next ctx
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,9 +116,7 @@ let saveCategory : HttpHandler = fun next ctx -> task {
|
||||||
do! (match model.categoryId with "new" -> Data.Category.add | _ -> Data.Category.update) cat conn
|
do! (match model.categoryId with "new" -> Data.Category.add | _ -> Data.Category.update) cat conn
|
||||||
do! CategoryCache.update ctx
|
do! CategoryCache.update ctx
|
||||||
do! addMessage ctx { UserMessage.success with message = "Category saved successfully" }
|
do! addMessage ctx { UserMessage.success with message = "Category saved successfully" }
|
||||||
return!
|
return! listCategoriesBare next ctx
|
||||||
redirectToGet (WebLog.relativeUrl webLog (Permalink $"admin/category/{CategoryId.toString cat.id}/edit"))
|
|
||||||
next ctx
|
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,7 +128,7 @@ let deleteCategory catId : HttpHandler = fun next ctx -> task {
|
||||||
do! CategoryCache.update ctx
|
do! CategoryCache.update ctx
|
||||||
do! addMessage ctx { UserMessage.success with message = "Category deleted successfully" }
|
do! addMessage ctx { UserMessage.success with message = "Category deleted successfully" }
|
||||||
| false -> do! addMessage ctx { UserMessage.error with message = "Category not found; cannot delete" }
|
| false -> do! addMessage ctx { UserMessage.error with message = "Category not found; cannot delete" }
|
||||||
return! redirectToGet (WebLog.relativeUrl webLog (Permalink "admin/categories")) next ctx
|
return! listCategoriesBare next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- PAGES --
|
// -- PAGES --
|
||||||
|
@ -304,20 +315,37 @@ let saveSettings : HttpHandler = fun next ctx -> task {
|
||||||
|
|
||||||
// -- TAG MAPPINGS --
|
// -- TAG MAPPINGS --
|
||||||
|
|
||||||
// GET /admin/tag-mappings
|
open Microsoft.AspNetCore.Http
|
||||||
let tagMappings : HttpHandler = fun next ctx -> task {
|
|
||||||
|
/// Get the hash necessary to render the tag mapping list
|
||||||
|
let private tagMappingHash (ctx : HttpContext) = task {
|
||||||
let! mappings = Data.TagMap.findByWebLogId ctx.WebLog.id ctx.Conn
|
let! mappings = Data.TagMap.findByWebLogId ctx.WebLog.id ctx.Conn
|
||||||
return!
|
return Hash.FromAnonymousObject {|
|
||||||
Hash.FromAnonymousObject
|
web_log = ctx.WebLog
|
||||||
{| csrf = csrfToken ctx
|
csrf = csrfToken ctx
|
||||||
mappings = mappings
|
mappings = mappings
|
||||||
mapping_ids = mappings |> List.map (fun it -> { name = it.tag; value = TagMapId.toString it.id })
|
mapping_ids = mappings |> List.map (fun it -> { name = it.tag; value = TagMapId.toString it.id })
|
||||||
page_title = "Tag Mappings"
|
|
||||||
|}
|
|}
|
||||||
|> viewForTheme "admin" "tag-mapping-list" next ctx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /admin/tag-mapping/{id}/edit
|
// GET /admin/settings/tag-mappings
|
||||||
|
let tagMappings : HttpHandler = fun next ctx -> task {
|
||||||
|
let! hash = tagMappingHash ctx
|
||||||
|
let! listTemplate = TemplateCache.get "admin" "tag-mapping-list-body"
|
||||||
|
|
||||||
|
hash.Add ("tag_mapping_list", listTemplate.Render hash)
|
||||||
|
hash.Add ("page_title", "Tag Mappings")
|
||||||
|
|
||||||
|
return! viewForTheme "admin" "tag-mapping-list" next ctx hash
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /admin/settings/tag-mappings/bare
|
||||||
|
let tagMappingsBare : HttpHandler = fun next ctx -> task {
|
||||||
|
let! hash = tagMappingHash ctx
|
||||||
|
return! bareForTheme "admin" "tag-mapping-list-body" next ctx hash
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /admin/settings/tag-mapping/{id}/edit
|
||||||
let editMapping tagMapId : HttpHandler = fun next ctx -> task {
|
let editMapping tagMapId : HttpHandler = fun next ctx -> task {
|
||||||
let isNew = tagMapId = "new"
|
let isNew = tagMapId = "new"
|
||||||
let tagMap =
|
let tagMap =
|
||||||
|
@ -333,11 +361,11 @@ let editMapping tagMapId : HttpHandler = fun next ctx -> task {
|
||||||
model = EditTagMapModel.fromMapping tm
|
model = EditTagMapModel.fromMapping tm
|
||||||
page_title = if isNew then "Add Tag Mapping" else $"Mapping for {tm.tag} Tag"
|
page_title = if isNew then "Add Tag Mapping" else $"Mapping for {tm.tag} Tag"
|
||||||
|}
|
|}
|
||||||
|> viewForTheme "admin" "tag-mapping-edit" next ctx
|
|> bareForTheme "admin" "tag-mapping-edit" next ctx
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /admin/tag-mapping/save
|
// POST /admin/settings/tag-mapping/save
|
||||||
let saveMapping : HttpHandler = fun next ctx -> task {
|
let saveMapping : HttpHandler = fun next ctx -> task {
|
||||||
let webLog = ctx.WebLog
|
let webLog = ctx.WebLog
|
||||||
let conn = ctx.Conn
|
let conn = ctx.Conn
|
||||||
|
@ -351,17 +379,15 @@ let saveMapping : HttpHandler = fun next ctx -> task {
|
||||||
| Some tm ->
|
| Some tm ->
|
||||||
do! Data.TagMap.save { tm with tag = model.tag.ToLower (); urlValue = model.urlValue.ToLower () } conn
|
do! Data.TagMap.save { tm with tag = model.tag.ToLower (); urlValue = model.urlValue.ToLower () } conn
|
||||||
do! addMessage ctx { UserMessage.success with message = "Tag mapping saved successfully" }
|
do! addMessage ctx { UserMessage.success with message = "Tag mapping saved successfully" }
|
||||||
return!
|
return! tagMappingsBare next ctx
|
||||||
redirectToGet (WebLog.relativeUrl webLog (Permalink $"admin/tag-mapping/{TagMapId.toString tm.id}/edit"))
|
|
||||||
next ctx
|
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /admin/tag-mapping/{id}/delete
|
// POST /admin/settings/tag-mapping/{id}/delete
|
||||||
let deleteMapping tagMapId : HttpHandler = fun next ctx -> task {
|
let deleteMapping tagMapId : HttpHandler = fun next ctx -> task {
|
||||||
let webLog = ctx.WebLog
|
let webLog = ctx.WebLog
|
||||||
match! Data.TagMap.delete (TagMapId tagMapId) webLog.id ctx.Conn with
|
match! Data.TagMap.delete (TagMapId tagMapId) webLog.id ctx.Conn 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! redirectToGet (WebLog.relativeUrl webLog (Permalink "admin/tag-mappings")) next ctx
|
return! tagMappingsBare next ctx
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,8 +85,8 @@ open Giraffe.ViewEngine
|
||||||
/// htmx script tag
|
/// htmx script tag
|
||||||
let private htmxScript = RenderView.AsString.htmlNode Htmx.Script.minified
|
let private htmxScript = RenderView.AsString.htmlNode Htmx.Script.minified
|
||||||
|
|
||||||
/// Render a view for the specified theme, using the specified template, layout, and hash
|
/// Populate the DotLiquid hash with standard information
|
||||||
let viewForTheme theme template next ctx = fun (hash : Hash) -> task {
|
let private populateHash hash ctx = task {
|
||||||
// Don't need the web log, but this adds it to the hash if the function is called directly
|
// Don't need the web log, but this adds it to the hash if the function is called directly
|
||||||
let _ = deriveWebLogFromHash hash ctx
|
let _ = deriveWebLogFromHash hash ctx
|
||||||
let! messages = messages ctx
|
let! messages = messages ctx
|
||||||
|
@ -98,6 +98,11 @@ let viewForTheme theme template next ctx = fun (hash : Hash) -> task {
|
||||||
hash.Add ("htmx_script", htmxScript)
|
hash.Add ("htmx_script", htmxScript)
|
||||||
|
|
||||||
do! commitSession ctx
|
do! commitSession ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render a view for the specified theme, using the specified template, layout, and hash
|
||||||
|
let viewForTheme theme template next ctx = fun (hash : Hash) -> task {
|
||||||
|
do! populateHash hash ctx
|
||||||
|
|
||||||
// NOTE: DotLiquid does not support {% render %} or {% include %} in its templates, so we will do a 2-pass render;
|
// NOTE: DotLiquid does not support {% render %} or {% include %} in its templates, so we will do a 2-pass render;
|
||||||
// the net effect is a "layout" capability similar to Razor or Pug
|
// the net effect is a "layout" capability similar to Razor or Pug
|
||||||
|
@ -108,12 +113,38 @@ let viewForTheme theme template next ctx = fun (hash : Hash) -> task {
|
||||||
|
|
||||||
// ...then render that content with its layout
|
// ...then render that content with its layout
|
||||||
let isHtmx = ctx.Request.IsHtmx && not ctx.Request.IsHtmxRefresh
|
let isHtmx = ctx.Request.IsHtmx && not ctx.Request.IsHtmxRefresh
|
||||||
let layout = if isHtmx then "layout-partial" else "layout"
|
let! layoutTemplate = TemplateCache.get theme (if isHtmx then "layout-partial" else "layout")
|
||||||
let! layoutTemplate = TemplateCache.get theme layout
|
|
||||||
|
|
||||||
return! htmlString (layoutTemplate.Render hash) next ctx
|
return! htmlString (layoutTemplate.Render hash) next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Render a bare view for the specified theme, using the specified template and hash
|
||||||
|
let bareForTheme theme template next ctx = fun (hash : Hash) -> task {
|
||||||
|
do! populateHash hash ctx
|
||||||
|
|
||||||
|
// Bare templates are rendered with layout-bare
|
||||||
|
let! contentTemplate = TemplateCache.get theme template
|
||||||
|
hash.Add ("content", contentTemplate.Render hash)
|
||||||
|
|
||||||
|
let! layoutTemplate = TemplateCache.get theme "layout-bare"
|
||||||
|
|
||||||
|
// add messages as HTTP headers
|
||||||
|
let messages = hash["messages"] :?> UserMessage[]
|
||||||
|
let actions = seq {
|
||||||
|
yield!
|
||||||
|
messages
|
||||||
|
|> Array.map (fun m ->
|
||||||
|
match m.detail with
|
||||||
|
| Some detail -> $"{m.level}|||{m.message}|||{detail}"
|
||||||
|
| None -> $"{m.level}|||{m.message}"
|
||||||
|
|> setHttpHeader "X-Message")
|
||||||
|
withHxNoPush
|
||||||
|
htmlString (layoutTemplate.Render hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
return! (actions |> Seq.reduce (>=>)) next ctx
|
||||||
|
}
|
||||||
|
|
||||||
/// Return a view for the web log's default theme
|
/// Return a view for the web log's default theme
|
||||||
let themedView template next ctx = fun (hash : Hash) -> task {
|
let themedView template next ctx = fun (hash : Hash) -> task {
|
||||||
return! viewForTheme (deriveWebLogFromHash hash ctx).themePath template next ctx hash
|
return! viewForTheme (deriveWebLogFromHash hash ctx).themePath template next ctx hash
|
||||||
|
|
|
@ -98,6 +98,7 @@ let router : HttpHandler = choose [
|
||||||
GET >=> choose [
|
GET >=> choose [
|
||||||
subRoute "/categor" (choose [
|
subRoute "/categor" (choose [
|
||||||
route "ies" >=> Admin.listCategories
|
route "ies" >=> Admin.listCategories
|
||||||
|
route "ies/bare" >=> Admin.listCategoriesBare
|
||||||
routef "y/%s/edit" Admin.editCategory
|
routef "y/%s/edit" Admin.editCategory
|
||||||
])
|
])
|
||||||
route "/dashboard" >=> Admin.dashboard
|
route "/dashboard" >=> Admin.dashboard
|
||||||
|
@ -121,6 +122,7 @@ let router : HttpHandler = choose [
|
||||||
])
|
])
|
||||||
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
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"hostname": "data02.bitbadger.solutions",
|
"hostname": "data02.bitbadger.solutions",
|
||||||
"database": "myWebLog_dev"
|
"database": "myWebLog_dev"
|
||||||
},
|
},
|
||||||
"Generator": "myWebLog 2.0-alpha25",
|
"Generator": "myWebLog 2.0-alpha26",
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"MyWebLog.Handlers": "Debug"
|
"MyWebLog.Handlers": "Debug"
|
||||||
|
|
|
@ -1,27 +1,27 @@
|
||||||
<h2 class="my-3">{{ page_title }}</h2>
|
<div class="col-12">
|
||||||
<article>
|
<h5 class="my-3">{{ page_title }}</h5>
|
||||||
<form action="{{ "admin/category/save" | relative_link }}" method="post">
|
<form hx-post="{{ "admin/category/save" | relative_link }}" method="post" class="container"
|
||||||
|
hx-target="#catList" hx-swap="outerHTML show:window:top">
|
||||||
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
||||||
<input type="hidden" name="categoryId" value="{{ model.category_id }}">
|
<input type="hidden" name="categoryId" value="{{ model.category_id }}">
|
||||||
<div class="container">
|
<div class="row">
|
||||||
<div class="row mb-3">
|
<div class="col-12 col-sm-6 col-lg-4 col-xxl-3 offset-xxl-1 mb-3">
|
||||||
<div class="col-6 col-lg-4 pb-3">
|
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input type="text" name="name" id="name" class="form-control" placeholder="Name" autofocus required
|
<input type="text" name="name" id="name" class="form-control form-control-sm" placeholder="Name" autofocus
|
||||||
value="{{ model.name | escape }}">
|
required value="{{ model.name | escape }}">
|
||||||
<label for="name">Name</label>
|
<label for="name">Name</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 col-lg-4 pb-3">
|
<div class="col-12 col-sm-6 col-lg-4 col-xxl-3 mb-3">
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input type="text" name="slug" id="slug" class="form-control" placeholder="Slug" required
|
<input type="text" name="slug" id="slug" class="form-control form-control-sm" placeholder="Slug" required
|
||||||
value="{{ model.slug | escape }}">
|
value="{{ model.slug | escape }}">
|
||||||
<label for="slug">Slug</label>
|
<label for="slug">Slug</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-lg-4 pb-3">
|
<div class="col-12 col-lg-4 col-xxl-3 offset-xxl-1 mb-3">
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<select name="parentId" id="parentId" class="form-control">
|
<select name="parentId" id="parentId" class="form-control form-control-sm">
|
||||||
<option value=""{% if model.parent_id == "" %} selected="selected"{% endif %}>
|
<option value=""{% if model.parent_id == "" %} selected="selected"{% endif %}>
|
||||||
– None –
|
– None –
|
||||||
</option>
|
</option>
|
||||||
|
@ -36,21 +36,19 @@
|
||||||
<label for="parentId">Parent Category</label>
|
<label for="parentId">Parent Category</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col-12 col-xl-10 offset-xl-1 mb-3">
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col">
|
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input name="description" id="description" class="form-control"
|
<input name="description" id="description" class="form-control form-control-sm"
|
||||||
placeholder="A short description of this category" value="{{ model.description | escape }}">
|
placeholder="A short description of this category" value="{{ model.description | escape }}">
|
||||||
<label for="description">Description</label>
|
<label for="description">Description</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col">
|
<div class="col text-center">
|
||||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
<button type="submit" class="btn btn-sm btn-primary">Save Changes</button>
|
||||||
</div>
|
<a href="{{ "admin/categories/bare" | relative_link }}" class="btn btn-sm btn-secondary ms-3">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</article>
|
</div>
|
||||||
|
|
46
src/MyWebLog/themes/admin/category-list-body.liquid
Normal file
46
src/MyWebLog/themes/admin/category-list-body.liquid
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
<form method="post" id="catList" class="container" hx-target="this" hx-swap="outerHTML show:window:top">
|
||||||
|
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
||||||
|
<div class="row mwl-table-detail" id="cat_new"></div>
|
||||||
|
{%- assign cat_count = categories | size -%}
|
||||||
|
{% if cat_count > 0 %}
|
||||||
|
{%- assign cat_col = "col-12 col-md-6 col-xl-5 col-xxl-4" -%}
|
||||||
|
{%- assign desc_col = "col-12 col-md-6 col-xl-7 col-xxl-8" -%}
|
||||||
|
{% for cat in categories -%}
|
||||||
|
<div class="row mwl-table-detail" id="cat_{{ cat.id }}">
|
||||||
|
<div class="{{ cat_col }} no-wrap">
|
||||||
|
{%- if cat.parent_names %}
|
||||||
|
<small class="text-muted">{% for name in cat.parent_names %}{{ name }} ⟩ {% endfor %}</small>
|
||||||
|
{%- endif %}
|
||||||
|
{{ cat.name }}<br>
|
||||||
|
<small>
|
||||||
|
{%- if cat.post_count > 0 %}
|
||||||
|
<a href="{{ cat | category_link }}" target="_blank">
|
||||||
|
View {{ cat.post_count }} Post{% unless cat.post_count == 1 %}s{% endunless -%}
|
||||||
|
</a>
|
||||||
|
<span class="text-muted"> • </span>
|
||||||
|
{%- endif %}
|
||||||
|
{%- capture cat_edit %}admin/category/{{ cat.id }}/edit{% endcapture -%}
|
||||||
|
<a href="{{ cat_edit | relative_link }}" hx-target="#cat_{{ cat.id }}"
|
||||||
|
hx-swap="innerHTML show:#cat_{{ cat.id }}:top">
|
||||||
|
Edit
|
||||||
|
</a>
|
||||||
|
<span class="text-muted"> • </span>
|
||||||
|
{%- capture cat_del %}admin/category/{{ cat.id }}/delete{% endcapture -%}
|
||||||
|
{%- capture cat_del_link %}{{ cat_del | relative_link }}{% endcapture -%}
|
||||||
|
<a href="{{ cat_del_link }}" hx-post="{{ cat_del_link }}" class="text-danger"
|
||||||
|
hx-confirm="Are you sure you want to delete the category “{{ cat.name }}”? This action cannot be undone.">
|
||||||
|
Delete
|
||||||
|
</a>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="{{ desc_col }}">
|
||||||
|
{%- if cat.description %}{{ cat.description.value }}{% else %}<em class="text-muted">none</em>{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{%- endfor %}
|
||||||
|
{%- else -%}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 text-muted fst-italic text-center">This web log has no categores defined</div>
|
||||||
|
</div>
|
||||||
|
{%- endif %}
|
||||||
|
</form>
|
|
@ -1,54 +1,16 @@
|
||||||
<h2 class="my-3">{{ page_title }}</h2>
|
<h2 class="my-3">{{ page_title }}</h2>
|
||||||
<article>
|
<article>
|
||||||
<a href="{{ "admin/category/new/edit" | relative_link }}" class="btn btn-primary btn-sm mb-3">Add a New Category</a>
|
<a href="{{ "admin/category/new/edit" | relative_link }}" class="btn btn-primary btn-sm mb-3"
|
||||||
<table class="table table-sm table-hover">
|
hx-target="#cat_new">
|
||||||
<thead>
|
Add a New Category
|
||||||
<tr>
|
|
||||||
<th scope="col">Category</th>
|
|
||||||
<th scope="col">Description</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{%- assign cat_count = categories | size -%}
|
|
||||||
{% if cat_count > 0 %}
|
|
||||||
{% for cat in categories -%}
|
|
||||||
<tr>
|
|
||||||
<td class="no-wrap">
|
|
||||||
{%- if cat.parent_names %}
|
|
||||||
<small class="text-muted">{% for name in cat.parent_names %}{{ name }} ⟩ {% endfor %}</small>
|
|
||||||
{%- endif %}
|
|
||||||
{{ cat.name }}<br>
|
|
||||||
<small>
|
|
||||||
{%- if cat.post_count > 0 %}
|
|
||||||
<a href="{{ cat | category_link }}" target="_blank">
|
|
||||||
View {{ cat.post_count }} Post{% unless cat.post_count == 1 %}s{% endunless -%}
|
|
||||||
</a>
|
</a>
|
||||||
<span class="text-muted"> • </span>
|
<div class="container">
|
||||||
{%- endif %}
|
{%- assign cat_col = "col-12 col-md-6 col-xl-5 col-xxl-4" -%}
|
||||||
{%- capture cat_edit %}admin/category/{{ cat.id }}/edit{% endcapture -%}
|
{%- assign desc_col = "col-12 col-md-6 col-xl-7 col-xxl-8" -%}
|
||||||
<a href="{{ cat_edit | relative_link }}">Edit</a>
|
<div class="row mwl-table-heading">
|
||||||
<span class="text-muted"> • </span>
|
<div class="{{ cat_col }}">Category<span class="d-md-none">; Description</span></div>
|
||||||
{%- capture cat_del %}admin/category/{{ cat.id }}/delete{% endcapture -%}
|
<div class="{{ desc_col }} d-none d-md-inline-block">Description</div>
|
||||||
{%- capture cat_del_link %}{{ cat_del | relative_link }}{% endcapture -%}
|
</div>
|
||||||
<a href="{{ cat_del_link }}" class="text-danger"
|
</div>
|
||||||
onclick="return Admin.deleteCategory('{{ cat.name }}', '{{ cat_del_link }}')">
|
{{ category_list }}
|
||||||
Delete
|
|
||||||
</a>
|
|
||||||
</small>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{%- if cat.description %}{{ cat.description.value }}{% else %}<em class="text-muted">none</em>{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{%- endfor %}
|
|
||||||
{%- else -%}
|
|
||||||
<tr>
|
|
||||||
<td colspan="2" class="text-muted fst-italic text-center">This web log has no categores defined</td>
|
|
||||||
</tr>
|
|
||||||
{%- endif %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<form method="post" id="deleteForm">
|
|
||||||
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
|
||||||
</form>
|
|
||||||
</article>
|
</article>
|
||||||
|
|
5
src/MyWebLog/themes/admin/layout-bare.liquid
Normal file
5
src/MyWebLog/themes/admin/layout-bare.liquid
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head><title></title></head>
|
||||||
|
<body>{{ content }}</body>
|
||||||
|
</html>
|
|
@ -34,9 +34,8 @@
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<main class="mx-3">
|
<main class="mx-3 mt-3">
|
||||||
{% if messages %}
|
<div class="messages mt-2" id="msgContainer">
|
||||||
<div class="messages mt-2">
|
|
||||||
{% for msg in messages %}
|
{% for msg in messages %}
|
||||||
<div role="alert" class="alert alert-{{ msg.level }} alert-dismissible fade show">
|
<div role="alert" class="alert alert-{{ msg.level }} alert-dismissible fade show">
|
||||||
{{ msg.message }}
|
{{ msg.message }}
|
||||||
|
@ -48,7 +47,6 @@
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
{{ content }}
|
{{ content }}
|
||||||
</main>
|
</main>
|
||||||
<footer class="position-fixed bottom-0 w-100">
|
<footer class="position-fixed bottom-0 w-100">
|
||||||
|
@ -58,5 +56,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
<script>Admin.dismissSuccesses()</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<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 | escape }} « Admin « {{ web_log.name | escape }}</title>
|
<title>{{ page_title | escape }} « Admin « {{ web_log.name | escape }}</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.0.2/dist/css/bootstrap.min.css"
|
||||||
|
@ -39,9 +39,8 @@
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<main class="mx-3">
|
<main class="mx-3 mt-3">
|
||||||
{% if messages %}
|
<div class="messages mt-2" id="msgContainer">
|
||||||
<div class="messages mt-2">
|
|
||||||
{% for msg in messages %}
|
{% for msg in messages %}
|
||||||
<div role="alert" class="alert alert-{{ msg.level }} alert-dismissible fade show">
|
<div role="alert" class="alert alert-{{ msg.level }} alert-dismissible fade show">
|
||||||
{{ msg.message }}
|
{{ msg.message }}
|
||||||
|
@ -53,7 +52,6 @@
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
{{ content }}
|
{{ content }}
|
||||||
</main>
|
</main>
|
||||||
<footer class="position-fixed bottom-0 w-100">
|
<footer class="position-fixed bottom-0 w-100">
|
||||||
|
@ -80,5 +78,6 @@
|
||||||
}, 2000)
|
}, 2000)
|
||||||
</script>
|
</script>
|
||||||
<script src="/themes/admin/admin.js"></script>
|
<script src="/themes/admin/admin.js"></script>
|
||||||
|
<script>Admin.dismissSuccesses()</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -2,19 +2,22 @@
|
||||||
<article>
|
<article>
|
||||||
<a href="{{ "admin/page/new/edit" | relative_link }}" class="btn btn-primary btn-sm mb-3">Create a New Page</a>
|
<a href="{{ "admin/page/new/edit" | relative_link }}" class="btn btn-primary btn-sm mb-3">Create a New Page</a>
|
||||||
{%- assign page_count = pages | size -%}
|
{%- assign page_count = pages | size -%}
|
||||||
<table class="table table-sm table-hover">
|
{%- assign title_col = "col-12 col-md-5" -%}
|
||||||
<thead>
|
{%- assign link_col = "col-12 col-md-5" -%}
|
||||||
<tr>
|
{%- assign upd8_col = "col-12 col-md-2" -%}
|
||||||
<th scope="col">Title</th>
|
<form method="post" class="container" hx-target="body">
|
||||||
<th scope="col">Permalink</th>
|
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
||||||
<th scope="col">Last Updated</th>
|
<div class="row mwl-table-heading">
|
||||||
</tr>
|
<div class="{{ title_col }}">
|
||||||
</thead>
|
<span class="d-none d-md-inline">Title</span><span class="d-md-none">Page</span>
|
||||||
<tbody>
|
</div>
|
||||||
|
<div class="{{ link_col }} d-none d-md-inline-block">Permalink</div>
|
||||||
|
<div class="{{ upd8_col }} d-none d-md-inline-block">Updated</div>
|
||||||
|
</div>
|
||||||
{% if page_count > 0 %}
|
{% if page_count > 0 %}
|
||||||
{% for pg in pages -%}
|
{% for pg in pages -%}
|
||||||
<tr>
|
<div class="row mwl-table-detail">
|
||||||
<td>
|
<div class="{{ title_col }}">
|
||||||
{{ pg.title }}
|
{{ pg.title }}
|
||||||
{%- if pg.is_default %} <span class="badge bg-success">HOME PAGE</span>{% endif -%}
|
{%- if pg.is_default %} <span class="badge bg-success">HOME PAGE</span>{% endif -%}
|
||||||
{%- if pg.show_in_page_list %} <span class="badge bg-primary">IN PAGE LIST</span> {% endif -%}<br>
|
{%- if pg.show_in_page_list %} <span class="badge bg-primary">IN PAGE LIST</span> {% endif -%}<br>
|
||||||
|
@ -26,25 +29,30 @@
|
||||||
<span class="text-muted"> • </span>
|
<span class="text-muted"> • </span>
|
||||||
{%- capture pg_del %}admin/page/{{ pg.id }}/delete{% endcapture -%}
|
{%- capture pg_del %}admin/page/{{ pg.id }}/delete{% endcapture -%}
|
||||||
{%- capture pg_del_link %}{{ pg_del | relative_link }}{% endcapture -%}
|
{%- capture pg_del_link %}{{ pg_del | relative_link }}{% endcapture -%}
|
||||||
<a href="{{ pg_del_link }}" class="text-danger"
|
<a href="{{ pg_del_link }}" hx-post="{{ pg_del_link }}" class="text-danger"
|
||||||
onclick="return Admin.deletePage('{{ pg.title }}', '{{ pg_del_link }}')">
|
hx-confirm="Are you sure you want to delete the page “{{ pg.title | strip_html | escape }}”? This action cannot be undone.">
|
||||||
Delete
|
Delete
|
||||||
</a>
|
</a>
|
||||||
</small>
|
</small>
|
||||||
</td>
|
</div>
|
||||||
<td>/{% unless pg.is_default %}{{ pg.permalink }}{% endunless %}</td>
|
<div class="{{ link_col }}">
|
||||||
<td>{{ pg.updated_on | date: "MMMM d, yyyy" }}</td>
|
{%- capture pg_link %}/{% unless pg.is_default %}{{ pg.permalink }}{% endunless %}{% endcapture -%}
|
||||||
</tr>
|
<small class="d-md-none">{{ pg_link }}</small><span class="d-none d-md-inline">{{ pg_link }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="{{ upd8_col }}">
|
||||||
|
<small class="d-md-none text-muted">Updated {{ pg.updated_on | date: "MMMM d, yyyy" }}</small>
|
||||||
|
<span class="d-none d-md-inline">{{ pg.updated_on | date: "MMMM d, yyyy" }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr>
|
<div class="row">
|
||||||
<td colspan="3" class="text-muted fst-italic text-center">This web log has no pages</td>
|
<div class="col text-muted fst-italic text-center">This web log has no pages</div>
|
||||||
</tr>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tbody>
|
</form>
|
||||||
</table>
|
|
||||||
{% if page_nbr > 1 or page_count == 25 %}
|
{% if page_nbr > 1 or page_count == 25 %}
|
||||||
<div class="d-flex justify-content-evenly">
|
<div class="d-flex justify-content-evenly pb-3">
|
||||||
<div>
|
<div>
|
||||||
{% if page_nbr > 1 %}
|
{% if page_nbr > 1 %}
|
||||||
{%- capture prev_link %}admin/pages{{ prev_page }}{% endcapture -%}
|
{%- capture prev_link %}admin/pages{{ prev_page }}{% endcapture -%}
|
||||||
|
@ -59,7 +67,4 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form method="post" id="deleteForm">
|
|
||||||
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
|
||||||
</form>
|
|
||||||
</article>
|
</article>
|
||||||
|
|
|
@ -1,28 +1,47 @@
|
||||||
<h2 class="my-3">{{ page_title }}</h2>
|
<h2 class="my-3">{{ page_title }}</h2>
|
||||||
<article>
|
<article>
|
||||||
<a href="{{ "admin/post/new/edit" | relative_link }}" class="btn btn-primary btn-sm mb-3">Write a New Post</a>
|
<a href="{{ "admin/post/new/edit" | relative_link }}" class="btn btn-primary btn-sm mb-3">Write a New Post</a>
|
||||||
<table class="table table-sm table-hover">
|
<form method="post" class="container" hx-target="body">
|
||||||
<thead>
|
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
||||||
<tr>
|
|
||||||
<th scope="col">Date</th>
|
|
||||||
<th scope="col" style="width:300px;">Title</th>
|
|
||||||
<th scope="col">Author</th>
|
|
||||||
<th scope="col">Status</th>
|
|
||||||
<th scope="col">Tags</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{%- assign post_count = model.posts | size -%}
|
{%- assign post_count = model.posts | size -%}
|
||||||
|
{%- assign date_col = "col-xs-12 col-md-3 col-lg-2" -%}
|
||||||
|
{%- assign title_col = "col-xs-12 col-md-7 col-lg-6 col-xl-5 col-xxl-4" -%}
|
||||||
|
{%- assign author_col = "col-xs-12 col-md-2 col-lg-1" -%}
|
||||||
|
{%- assign tag_col = "col-lg-3 col-xl-4 col-xxl-5 d-none d-lg-inline-block" -%}
|
||||||
|
<div class="row mwl-table-heading">
|
||||||
|
<div class="{{ date_col }}">
|
||||||
|
<span class="d-md-none">Post</span><span class="d-none d-md-inline">Date</span>
|
||||||
|
</div>
|
||||||
|
<div class="{{ title_col }} d-none d-md-inline-block">Title</div>
|
||||||
|
<div class="{{ author_col }} d-none d-md-inline-block">Author</div>
|
||||||
|
<div class="{{ tag_col }}">Tags</div>
|
||||||
|
</div>
|
||||||
{%- if post_count > 0 %}
|
{%- if post_count > 0 %}
|
||||||
{% for post in model.posts -%}
|
{% for post in model.posts -%}
|
||||||
<tr>
|
<div class="row mwl-table-detail">
|
||||||
<td class="no-wrap">
|
<div class="{{ date_col }} no-wrap">
|
||||||
{% if post.published_on %}{{ post.published_on | date: "MMMM d, yyyy" }}{% else %}Not Published{% endif %}
|
<small class="d-md-none">
|
||||||
|
{%- if post.published_on -%}
|
||||||
|
Published {{ post.published_on | date: "MMMM d, yyyy" }}
|
||||||
|
{%- else -%}
|
||||||
|
Not Published
|
||||||
|
{%- endif -%}
|
||||||
|
{%- if post.published_on != post.updated_on -%}
|
||||||
|
<em class="text-muted"> (Updated {{ post.updated_on | date: "MMMM d, yyyy" }})</em>
|
||||||
|
{%- endif %}
|
||||||
|
</small>
|
||||||
|
<span class="d-none d-md-inline">
|
||||||
|
{%- if post.published_on -%}
|
||||||
|
{{ post.published_on | date: "MMMM d, yyyy" }}
|
||||||
|
{%- else -%}
|
||||||
|
Not Published
|
||||||
|
{%- endif -%}
|
||||||
{%- if post.published_on != post.updated_on %}<br>
|
{%- if post.published_on != post.updated_on %}<br>
|
||||||
<small class="text-muted"><em>{{ post.updated_on | date: "MMMM d, yyyy" }}</em></small>
|
<small class="text-muted"><em>{{ post.updated_on | date: "MMMM d, yyyy" }}</em></small>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
</td>
|
</span>
|
||||||
<td>
|
</div>
|
||||||
|
<div class="{{ title_col }}">
|
||||||
{{ post.title }}<br>
|
{{ post.title }}<br>
|
||||||
<small>
|
<small>
|
||||||
<a href="{{ post | relative_link }}" target="_blank">View Post</a>
|
<a href="{{ post | relative_link }}" target="_blank">View Post</a>
|
||||||
|
@ -31,24 +50,35 @@
|
||||||
<span class="text-muted"> • </span>
|
<span class="text-muted"> • </span>
|
||||||
{%- capture post_del %}admin/post/{{ post.id }}/delete{% endcapture -%}
|
{%- capture post_del %}admin/post/{{ post.id }}/delete{% endcapture -%}
|
||||||
{%- capture post_del_link %}{{ post_del | relative_link }}{% endcapture -%}
|
{%- capture post_del_link %}{{ post_del | relative_link }}{% endcapture -%}
|
||||||
<a href="{{ post_del_link }}" class="text-danger"
|
<a href="{{ post_del_link }}" hx-post="{{ post_del_link }}" class="text-danger"
|
||||||
onclick="return Admin.deletePost('{{ post.title }}', '{{ post_del_link }}')">
|
hx-confirm="Are you sure you want to delete the page “{{ post.title | strip_html | escape }}”? This action cannot be undone.">
|
||||||
Delete
|
Delete
|
||||||
</a>
|
</a>
|
||||||
</small>
|
</small>
|
||||||
</td>
|
</div>
|
||||||
<td class="no-wrap">{{ model.authors | value: post.author_id }}</td>
|
<div class="{{ author_col }}">
|
||||||
<td>{{ post.status }}</td>
|
{%- assign tag_count = post.tags | size -%}
|
||||||
<td><span class="no-wrap">{{ post.tags | join: "</span>, <span class='no-wrap'>" }}</span></td>
|
<small class="d-md-none">
|
||||||
</tr>
|
Authored by {{ model.authors | value: post.author_id }} |
|
||||||
|
{% if tag_count == 0 -%}
|
||||||
|
No
|
||||||
|
{%- else -%}
|
||||||
|
{{ tag_count }}
|
||||||
|
{%- endif %} Tag{% unless tag_count == 1 %}s{% endunless %}
|
||||||
|
</small>
|
||||||
|
<span class="d-none d-md-inline">{{ model.authors | value: post.author_id }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="{{ tag_col }}">
|
||||||
|
<span class="no-wrap">{{ post.tags | join: "</span>, <span class='no-wrap'>" }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr>
|
<div class="row">
|
||||||
<td colspan="5" class="text-muted fst-italic text-center">This web log has no posts</td>
|
<div class="col text-muted fst-italic text-center">This web log has no posts</div>
|
||||||
</tr>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tbody>
|
</form>
|
||||||
</table>
|
|
||||||
{% if model.newer_link or model.older_link %}
|
{% if model.newer_link or model.older_link %}
|
||||||
<div class="d-flex justify-content-evenly">
|
<div class="d-flex justify-content-evenly">
|
||||||
<div>
|
<div>
|
||||||
|
@ -63,7 +93,4 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form method="post" id="deleteForm">
|
|
||||||
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
|
||||||
</form>
|
|
||||||
</article>
|
</article>
|
||||||
|
|
|
@ -67,21 +67,23 @@
|
||||||
<a class="btn btn-sm btn-secondary" href="{{ 'admin/settings/rss/new/edit' | relative_link }}">
|
<a class="btn btn-sm btn-secondary" href="{{ 'admin/settings/rss/new/edit' | relative_link }}">
|
||||||
Add a New Custom Feed
|
Add a New Custom Feed
|
||||||
</a>
|
</a>
|
||||||
<table class="table table-sm table-hover">
|
<form method="post" class="container" hx-target="body">
|
||||||
<thead>
|
{%- assign source_col = "col-12 col-md-6" -%}
|
||||||
<tr>
|
{%- assign path_col = "col-12 col-md-6" -%}
|
||||||
<th scope="col">Source</th>
|
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
||||||
<th scope="col">Relative Path</th>
|
<div class="row mwl-table-heading">
|
||||||
<th scope="col">Podcast?</th>
|
<div class="{{ source_col }}">
|
||||||
</tr>
|
<span class="d-md-none">Feed</span><span class="d-none d-md-inline">Source</span>
|
||||||
</thead>
|
</div>
|
||||||
<tbody>
|
<div class="{{ path_col }} d-none d-md-inline-block">Relative Path</div>
|
||||||
|
</div>
|
||||||
{%- assign feed_count = custom_feeds | size -%}
|
{%- assign feed_count = custom_feeds | size -%}
|
||||||
{% if feed_count > 0 %}
|
{% if feed_count > 0 %}
|
||||||
{% for feed in custom_feeds %}
|
{% for feed in custom_feeds %}
|
||||||
<tr>
|
<div class="row mwl-table-detail">
|
||||||
<td>
|
<div class="{{ source_col }}">
|
||||||
{{ feed.source }}<br>
|
{{ feed.source }}
|
||||||
|
{%- if feed.is_podcast %} <span class="badge bg-primary">PODCAST</span>{% endif %}<br>
|
||||||
<small>
|
<small>
|
||||||
<a href="{{ feed.path | relative_link }}" target="_blank">View Feed</a>
|
<a href="{{ feed.path | relative_link }}" target="_blank">View Feed</a>
|
||||||
<span class="text-muted"> • </span>
|
<span class="text-muted"> • </span>
|
||||||
|
@ -90,24 +92,22 @@
|
||||||
<span class="text-muted"> • </span>
|
<span class="text-muted"> • </span>
|
||||||
{%- capture feed_del %}admin/settings/rss/{{ feed.id }}/delete{% endcapture -%}
|
{%- capture feed_del %}admin/settings/rss/{{ feed.id }}/delete{% endcapture -%}
|
||||||
{%- capture feed_del_link %}{{ feed_del | relative_link }}{% endcapture -%}
|
{%- capture feed_del_link %}{{ feed_del | relative_link }}{% endcapture -%}
|
||||||
<a href="{{ feed_del_link }}" class="text-danger"
|
<a href="{{ feed_del_link }}" hx-post="{{ feed_del_link }}" class="text-danger"
|
||||||
onclick="return Admin.deleteCustomFeed('{{ feed.source }}', '{{ feed_del_link }}')">
|
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
|
Delete
|
||||||
</a>
|
</a>
|
||||||
</small>
|
</small>
|
||||||
</td>
|
</div>
|
||||||
<td>{{ feed.path }}</td>
|
<div class="{{ path_col }}">
|
||||||
<td>{% if feed.is_podcast %}Yes{% else %}No{% endif %}</td>
|
<small class="d-md-none">Served at {{ feed.path }}</small>
|
||||||
</tr>
|
<span class="d-none d-md-inline">{{ feed.path }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="3" class="text-muted fst-italic text-center">No custom feeds defined</td>
|
<td colspan="3" class="text-muted fst-italic text-center">No custom feeds defined</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<form method="post" id="deleteForm">
|
|
||||||
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
|
||||||
</form>
|
</form>
|
||||||
</article>
|
</article>
|
||||||
|
|
|
@ -1,23 +1,17 @@
|
||||||
<h2 class="my-3">{{ page_title }}</h2>
|
<h5 class="my-3">{{ page_title }}</h5>
|
||||||
<article>
|
<form hx-post="{{ "admin/settings/tag-mapping/save" | relative_link }}" method="post" class="container"
|
||||||
<form action="{{ "admin/settings/tag-mapping/save" | relative_link }}" method="post">
|
hx-target="#tagList" hx-swap="outerHTML show:window:top">
|
||||||
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
||||||
<input type="hidden" name="id" value="{{ model.id }}">
|
<input type="hidden" name="id" value="{{ model.id }}">
|
||||||
<div class="container">
|
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col">
|
<div class="col-6 col-lg-4 offset-lg-2">
|
||||||
<a href="{{ "admin/settings/tag-mappings" | relative_link }}">« Back to Tag Mappings</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-12 col-md-6 col-lg-4 offset-lg-2 pb-3">
|
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input type="text" name="tag" id="tag" class="form-control" placeholder="Tag" autofocus required
|
<input type="text" name="tag" id="tag" class="form-control" placeholder="Tag" autofocus required
|
||||||
value="{{ model.tag }}">
|
value="{{ model.tag }}">
|
||||||
<label for="tag">Tag</label>
|
<label for="tag">Tag</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6 col-lg-4 pb-3">
|
<div class="col-6 col-lg-4">
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input type="text" name="urlValue" id="urlValue" class="form-control" placeholder="URL Value" required
|
<input type="text" name="urlValue" id="urlValue" class="form-control" placeholder="URL Value" required
|
||||||
value="{{ model.url_value }}">
|
value="{{ model.url_value }}">
|
||||||
|
@ -27,9 +21,10 @@
|
||||||
</div>
|
</div>
|
||||||
<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-primary">Save Changes</button>
|
<button type="submit" class="btn btn-sm btn-primary">Save Changes</button>
|
||||||
</div>
|
<a href="{{ "admin/settings/tag-mappings/bare" | relative_link }}" class="btn btn-sm btn-secondary ms-3">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</article>
|
|
||||||
|
|
34
src/MyWebLog/themes/admin/tag-mapping-list-body.liquid
Normal file
34
src/MyWebLog/themes/admin/tag-mapping-list-body.liquid
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<form method="post" class="container" id="tagList" hx-target="this" hx-swap="outerHTML show:window:top">
|
||||||
|
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
||||||
|
<div class="row mwl-table-detail" id="tag_new"></div>
|
||||||
|
{%- assign map_count = mappings | size -%}
|
||||||
|
{% if map_count > 0 -%}
|
||||||
|
{% for map in mappings -%}
|
||||||
|
{%- assign map_id = mapping_ids | value: map.tag -%}
|
||||||
|
<div class="row mwl-table-detail" id="tag_{{ map_id }}">
|
||||||
|
<div class="col no-wrap">
|
||||||
|
{{ map.tag }}<br>
|
||||||
|
<small>
|
||||||
|
{%- capture map_edit %}admin/settings/tag-mapping/{{ map_id }}/edit{% endcapture -%}
|
||||||
|
<a href="{{ map_edit | relative_link }}" hx-target="#tag_{{ map_id }}"
|
||||||
|
hx-swap="innerHTML show:#tag_{{ map_id }}:top">
|
||||||
|
Edit
|
||||||
|
</a>
|
||||||
|
<span class="text-muted"> • </span>
|
||||||
|
{%- capture map_del %}admin/settings/tag-mapping/{{ map_id }}/delete{% endcapture -%}
|
||||||
|
{%- capture map_del_link %}{{ map_del | relative_link }}{% endcapture -%}
|
||||||
|
<a href="{{ map_del_link }}" hx-post="{{ map_del_link }}" class="text-danger"
|
||||||
|
hx-confirm="Are you sure you want to delete the mapping for “{{ map.tag }}”? This action cannot be undone.">
|
||||||
|
Delete
|
||||||
|
</a>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="col">{{ map.url_value }}</div>
|
||||||
|
</div>
|
||||||
|
{%- endfor %}
|
||||||
|
{%- else -%}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col text-muted text-center fst-italic">This web log has no tag mappings</div>
|
||||||
|
</div>
|
||||||
|
{%- endif %}
|
||||||
|
</form>
|
|
@ -1,39 +1,14 @@
|
||||||
<h2 class="my-3">{{ page_title }}</h2>
|
<h2 class="my-3">{{ page_title }}</h2>
|
||||||
<article class="container">
|
<article>
|
||||||
<a href="{{ "admin/settings/tag-mapping/new/edit" | relative_link }}" class="btn btn-primary btn-sm mb-3">
|
<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
|
Add a New Tag Mapping
|
||||||
</a>
|
</a>
|
||||||
<table class="table table-sm table-hover">
|
<div class="container">
|
||||||
<thead>
|
<div class="row mwl-table-heading">
|
||||||
<tr>
|
<div class="col">Tag</div>
|
||||||
<th scope="col">Tag</th>
|
<div class="col">URL Value</div>
|
||||||
<th scope="col">URL Value</th>
|
</div>
|
||||||
</tr>
|
</div>
|
||||||
</thead>
|
{{ tag_mapping_list }}
|
||||||
<tbody>
|
|
||||||
{% for map in mappings -%}
|
|
||||||
{%- assign map_id = mapping_ids | value: map.tag -%}
|
|
||||||
<tr>
|
|
||||||
<td class="no-wrap">
|
|
||||||
{{ map.tag }}<br>
|
|
||||||
<small>
|
|
||||||
{%- capture map_edit %}admin/settings/tag-mapping/{{ map_id }}/edit{% endcapture -%}
|
|
||||||
<a href="{{ map_edit | relative_link }}">Edit</a>
|
|
||||||
<span class="text-muted"> • </span>
|
|
||||||
{%- capture map_del %}admin/settings/tag-mapping/{{ map_id }}/delete{% endcapture -%}
|
|
||||||
{%- capture map_del_link %}{{ map_del | relative_link }}{% endcapture -%}
|
|
||||||
<a href="{{ map_del_link }}" class="text-danger"
|
|
||||||
onclick="return Admin.deleteTagMapping('{{ map.tag }}', '{{ map_del_link }}')">
|
|
||||||
Delete
|
|
||||||
</a>
|
|
||||||
</small>
|
|
||||||
</td>
|
|
||||||
<td>{{ map.url_value }}</td>
|
|
||||||
</tr>
|
|
||||||
{%- endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<form method="post" id="deleteForm">
|
|
||||||
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
|
||||||
</form>
|
|
||||||
</article>
|
</article>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
:root {
|
:root {
|
||||||
--dark-gray: #212529;
|
--dark-gray: #212529;
|
||||||
|
--light-accent: rgba(0, 0, 0, .075);
|
||||||
}
|
}
|
||||||
html {
|
html {
|
||||||
background-color: var(--dark-gray);
|
background-color: var(--dark-gray);
|
||||||
|
@ -72,3 +73,15 @@ a.text-danger:link:hover, a.text-danger:visited:hover {
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
color: white !important;
|
color: white !important;
|
||||||
}
|
}
|
||||||
|
.mwl-table-heading {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
border-bottom: solid 1px var(--bs-dark);
|
||||||
|
}
|
||||||
|
.mwl-table-detail {
|
||||||
|
border-bottom: solid 1px var(--light-accent);
|
||||||
|
}
|
||||||
|
.mwl-table-detail:hover {
|
||||||
|
background-color: var(--light-accent);
|
||||||
|
color: var(--dark-gray);
|
||||||
|
}
|
||||||
|
|
|
@ -181,61 +181,51 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Confirm and delete an item
|
* Show messages that may have come with an htmx response
|
||||||
* @param name The name of the item to be deleted
|
* @param messages The messages from the response
|
||||||
* @param url The URL to which the form should be posted
|
|
||||||
*/
|
*/
|
||||||
deleteItem(name, url) {
|
showMessage(messages) {
|
||||||
if (confirm(`Are you sure you want to delete the ${name}? This action cannot be undone.`)) {
|
const msgs = messages.split(", ")
|
||||||
const form = document.getElementById("deleteForm")
|
msgs.forEach(msg => {
|
||||||
form.action = url
|
const parts = msg.split("|||")
|
||||||
form.submit()
|
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]
|
||||||
|
|
||||||
|
const closeBtn = document.createElement("button")
|
||||||
|
closeBtn.type = "button"
|
||||||
|
closeBtn.className = "btn-close"
|
||||||
|
closeBtn.setAttribute("data-bs-dismiss", "alert")
|
||||||
|
closeBtn.setAttribute("aria-label", "Close")
|
||||||
|
msgDiv.appendChild(closeBtn)
|
||||||
|
|
||||||
|
if (parts.length === 3) {
|
||||||
|
msgDiv.innerHTML += `<hr>${parts[2]}`
|
||||||
}
|
}
|
||||||
return false
|
document.getElementById("msgContainer").appendChild(msgDiv)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Confirm and delete a category
|
* Set all "success" alerts to close after 4 seconds
|
||||||
* @param name The name of the category to be deleted
|
|
||||||
* @param url The URL to which the form should be posted
|
|
||||||
*/
|
*/
|
||||||
deleteCategory(name, url) {
|
dismissSuccesses() {
|
||||||
return this.deleteItem(`category "${name}"`, url)
|
[...document.querySelectorAll(".alert-success")].forEach(alert => {
|
||||||
},
|
setTimeout(() => {
|
||||||
|
(bootstrap.Alert.getInstance(alert) ?? new bootstrap.Alert(alert)).close()
|
||||||
/**
|
}, 4000)
|
||||||
* Confirm and delete a custom RSS feed
|
})
|
||||||
* @param source The source for the feed to be deleted
|
|
||||||
* @param url The URL to which the form should be posted
|
|
||||||
*/
|
|
||||||
deleteCustomFeed(source, url) {
|
|
||||||
return this.deleteItem(`custom RSS feed based on ${source}`, url)
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Confirm and delete a page
|
|
||||||
* @param title The title of the page to be deleted
|
|
||||||
* @param url The URL to which the form should be posted
|
|
||||||
*/
|
|
||||||
deletePage(title, url) {
|
|
||||||
return this.deleteItem(`page "${title}"`, url)
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Confirm and delete a post
|
|
||||||
* @param title The title of the post to be deleted
|
|
||||||
* @param url The URL to which the form should be posted
|
|
||||||
*/
|
|
||||||
deletePost(title, url) {
|
|
||||||
return this.deleteItem(`post "${title}"`, url)
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Confirm and delete a tag mapping
|
|
||||||
* @param tag The tag for which the mapping will be deleted
|
|
||||||
* @param url The URL to which the form should be posted
|
|
||||||
*/
|
|
||||||
deleteTagMapping(tag, url) {
|
|
||||||
return this.deleteItem(`mapping for "${tag}"`, url)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
htmx.on("htmx:afterOnLoad", function (evt) {
|
||||||
|
const hdrs = evt.detail.xhr.getAllResponseHeaders()
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
})
|
Loading…
Reference in New Issue
Block a user