Tweak admin UI templates (#25)

- Move user management under web log settings
- Move user self-update to my-info
- Return meaningful error if a template does not exist
- Tweak margins/paddings throughout
- Do not show headings on list pages if lists are empty
- Fix pagination styles for page/post list pages
This commit is contained in:
Daniel J. Summers 2022-07-26 16:28:14 -04:00
parent ff9c08842b
commit 3189681021
23 changed files with 652 additions and 586 deletions

View File

@ -172,18 +172,34 @@ module TemplateCache =
let get (themeId : ThemeId) (templateName : string) (data : IData) = backgroundTask {
let templatePath = $"{ThemeId.toString themeId}/{templateName}"
match _cache.ContainsKey templatePath with
| true -> ()
| true -> return Ok _cache[templatePath]
| false ->
match! data.Theme.FindById themeId with
| Some theme ->
let mutable text = (theme.Templates |> List.find (fun t -> t.Name = templateName)).Text
match theme.Templates |> List.tryFind (fun t -> t.Name = templateName) with
| Some template ->
let mutable text = template.Text
let mutable childNotFound = ""
while hasInclude.IsMatch text do
let child = hasInclude.Match text
let childText = (theme.Templates |> List.find (fun t -> t.Name = child.Groups[1].Value)).Text
let childText =
match theme.Templates |> List.tryFind (fun t -> t.Name = child.Groups[1].Value) with
| Some childTemplate -> childTemplate.Text
| None ->
childNotFound <-
if childNotFound = "" then child.Groups[1].Value
else $"{childNotFound}; {child.Groups[1].Value}"
""
text <- text.Replace (child.Value, childText)
if childNotFound <> "" then
let s = if childNotFound.IndexOf ";" >= 0 then "s" else ""
return Error $"Could not find the child template{s} {childNotFound} required by {templateName}"
else
_cache[templatePath] <- Template.Parse (text, SyntaxCompatibility.DotLiquid22)
| None -> ()
return _cache[templatePath]
return Ok _cache[templatePath]
| None ->
return Error $"Theme ID {ThemeId.toString themeId} does not have a template named {templateName}"
| None -> return Result.Error $"Theme ID {ThemeId.toString themeId} does not exist"
}
/// Get all theme/template names currently cached

View File

@ -32,10 +32,11 @@ let dashboard : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|> adminView "dashboard" next ctx
}
// GET /admin/dashboard/administration
// GET /admin/administration
let adminDashboard : HttpHandler = requireAccess Administrator >=> fun next ctx -> task {
match! TemplateCache.get adminTheme "theme-list-body" ctx.Data with
| Ok bodyTemplate ->
let! themes = ctx.Data.Theme.All ()
let! bodyTemplate = TemplateCache.get adminTheme "theme-list-body" ctx.Data
let cachedTemplates = TemplateCache.allNames ()
let! hash =
hashForPage "myWebLog Administration"
@ -47,7 +48,10 @@ let adminDashboard : HttpHandler = requireAccess Administrator >=> fun next ctx
|> Seq.map (fun it -> [|
ThemeId.toString it.Id
it.Name
cachedTemplates |> List.filter (fun n -> n.StartsWith (ThemeId.toString it.Id)) |> List.length |> string
cachedTemplates
|> List.filter (fun n -> n.StartsWith (ThemeId.toString it.Id))
|> List.length
|> string
|])
|> Array.ofSeq)
|> addToHash "web_logs" (
@ -60,10 +64,11 @@ let adminDashboard : HttpHandler = requireAccess Administrator >=> fun next ctx
return!
addToHash "theme_list" (bodyTemplate.Render hash) hash
|> adminView "admin-dashboard" next ctx
| Error message -> return! Error.server message next ctx
}
/// Redirect the user to the admin dashboard
let toAdminDashboard : HttpHandler = redirectToGet "admin/dashboard/administration"
let toAdminDashboard : HttpHandler = redirectToGet "admin/administration"
// ~~ CACHES ~~
@ -117,7 +122,8 @@ let refreshThemeCache themeId : HttpHandler = requireAccess Administrator >=> fu
// GET /admin/categories
let listCategories : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
let! catListTemplate = TemplateCache.get adminTheme "category-list-body" ctx.Data
match! TemplateCache.get adminTheme "category-list-body" ctx.Data with
| Ok catListTemplate ->
let! hash =
hashForPage "Categories"
|> withAntiCsrf ctx
@ -125,6 +131,7 @@ let listCategories : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx ->
return!
addToHash "category_list" (catListTemplate.Render hash) hash
|> adminView "category-list" next ctx
| Error message -> return! Error.server message next ctx
}
// GET /admin/categories/bare
@ -204,11 +211,13 @@ let private tagMappingHash (ctx : HttpContext) = task {
// 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
let! listTemplate = TemplateCache.get adminTheme "tag-mapping-list-body" ctx.Data
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
@ -422,9 +431,12 @@ open System.Collections.Generic
// GET /admin/settings
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 ()
return!
let! users = data.WebLogUser.FindByWebLog ctx.WebLog.Id
let! hash =
hashForPage "Web Log Settings"
|> withAntiCsrf ctx
|> addToHash ViewContext.Model (SettingsModel.fromWebLog ctx.WebLog)
@ -445,7 +457,12 @@ let settings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task
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
| Error message -> return! Error.server message next ctx
}
// POST /admin/settings

View File

@ -218,23 +218,6 @@ let addViewContext ctx (hash : Hash) = task {
let isHtmx (ctx : HttpContext) =
ctx.Request.IsHtmx && not ctx.Request.IsHtmxRefresh
/// Render a view for the specified theme, using the specified template, layout, and hash
let viewForTheme themeId template next ctx (hash : Hash) = task {
let! hash = addViewContext ctx hash
// 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
// Render view content...
let! contentTemplate = TemplateCache.get themeId template ctx.Data
let _ = addToHash ViewContext.Content (contentTemplate.Render hash) hash
// ...then render that content with its layout
let! layoutTemplate = TemplateCache.get themeId (if isHtmx ctx then "layout-partial" else "layout") ctx.Data
return! htmlString (layoutTemplate.Render hash) next ctx
}
/// Convert messages to headers (used for htmx responses)
let messagesToHeaders (messages : UserMessage array) : HttpHandler =
seq {
@ -249,52 +232,12 @@ let messagesToHeaders (messages : UserMessage array) : HttpHandler =
}
|> Seq.reduce (>=>)
/// Render a bare view for the specified theme, using the specified template and hash
let bareForTheme themeId template next ctx (hash : Hash) = task {
let! hash = addViewContext ctx hash
if not (hash.ContainsKey ViewContext.Content) then
let! contentTemplate = TemplateCache.get themeId template ctx.Data
addToHash ViewContext.Content (contentTemplate.Render hash) hash |> ignore
// Bare templates are rendered with layout-bare
let! layoutTemplate = TemplateCache.get themeId "layout-bare" ctx.Data
return!
(messagesToHeaders (hash[ViewContext.Messages] :?> UserMessage[])
>=> htmlString (layoutTemplate.Render hash))
next ctx
}
/// Return a view for the web log's default theme
let themedView template next ctx hash = task {
let! hash = addViewContext ctx hash
return! viewForTheme (hash[ViewContext.WebLog] :?> WebLog).ThemeId template next ctx hash
}
/// The ID for the admin theme
let adminTheme = ThemeId "admin"
/// Display a view for the admin theme
let adminView template =
viewForTheme adminTheme template
/// Display a bare view for the admin theme
let adminBareView template =
bareForTheme adminTheme template
/// Redirect after doing some action; commits session and issues a temporary redirect
let redirectToGet url : HttpHandler = fun _ ctx -> task {
do! commitSession ctx
return! redirectTo false (WebLog.relativeUrl ctx.WebLog (Permalink url)) earlyReturn ctx
}
/// Validate the anti cross-site request forgery token in the current request
let validateCsrf : HttpHandler = fun next ctx -> task {
match! ctx.AntiForgery.IsRequestValidAsync ctx with
| true -> return! next ctx
| false -> return! RequestErrors.BAD_REQUEST "CSRF token invalid" earlyReturn ctx
}
/// Handlers for error conditions
module Error =
@ -324,9 +267,81 @@ module Error =
let messages = [|
{ UserMessage.error with Message = $"The URL {ctx.Request.Path.Value} was not found" }
|]
(messagesToHeaders messages >=> setStatusCode 404) earlyReturn ctx
RequestErrors.notFound (messagesToHeaders messages) earlyReturn ctx
else RequestErrors.NOT_FOUND "Not found" earlyReturn ctx)
let server message : HttpHandler =
handleContext (fun ctx ->
if isHtmx ctx then
let messages = [| { UserMessage.error with Message = message } |]
ServerErrors.internalError (messagesToHeaders messages) earlyReturn ctx
else ServerErrors.INTERNAL_ERROR message earlyReturn ctx)
/// Render a view for the specified theme, using the specified template, layout, and hash
let viewForTheme themeId template next ctx (hash : Hash) = task {
let! hash = addViewContext ctx hash
// 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
// Render view content...
match! TemplateCache.get themeId template ctx.Data with
| Ok contentTemplate ->
let _ = addToHash ViewContext.Content (contentTemplate.Render hash) hash
// ...then render that content with its layout
match! TemplateCache.get themeId (if isHtmx ctx then "layout-partial" else "layout") ctx.Data with
| Ok layoutTemplate -> return! htmlString (layoutTemplate.Render hash) next ctx
| Error message -> return! Error.server message next ctx
| Error message -> return! Error.server message next ctx
}
/// Render a bare view for the specified theme, using the specified template and hash
let bareForTheme themeId template next ctx (hash : Hash) = task {
let! hash = addViewContext ctx hash
let withContent = task {
if hash.ContainsKey ViewContext.Content then return Ok hash
else
(setStatusCode 404 >=> text "Not found") earlyReturn ctx)
match! TemplateCache.get themeId template ctx.Data with
| Ok contentTemplate -> return Ok (addToHash ViewContext.Content (contentTemplate.Render hash) hash)
| Error message -> return Error message
}
match! withContent with
| Ok completeHash ->
// Bare templates are rendered with layout-bare
match! TemplateCache.get themeId "layout-bare" ctx.Data with
| Ok layoutTemplate ->
return!
(messagesToHeaders (hash[ViewContext.Messages] :?> UserMessage[])
>=> htmlString (layoutTemplate.Render completeHash))
next ctx
| Error message -> return! Error.server message next ctx
| Error message -> return! Error.server message next ctx
}
/// Return a view for the web log's default theme
let themedView template next ctx hash = task {
let! hash = addViewContext ctx hash
return! viewForTheme (hash[ViewContext.WebLog] :?> WebLog).ThemeId template next ctx hash
}
/// The ID for the admin theme
let adminTheme = ThemeId "admin"
/// Display a view for the admin theme
let adminView template =
viewForTheme adminTheme template
/// Display a bare view for the admin theme
let adminBareView template =
bareForTheme adminTheme template
/// Validate the anti cross-site request forgery token in the current request
let validateCsrf : HttpHandler = fun next ctx -> task {
match! ctx.AntiForgery.IsRequestValidAsync ctx with
| true -> return! next ctx
| false -> return! RequestErrors.BAD_REQUEST "CSRF token invalid" earlyReturn ctx
}
/// Require a user to be logged on

View File

@ -106,13 +106,14 @@ let router : HttpHandler = choose [
]
subRoute "/admin" (requireUser >=> choose [
GET_HEAD >=> choose [
route "/administration" >=> Admin.adminDashboard
subRoute "/categor" (choose [
route "ies" >=> Admin.listCategories
route "ies/bare" >=> Admin.listCategoriesBare
routef "y/%s/edit" Admin.editCategory
])
route "/dashboard" >=> Admin.dashboard
route "/dashboard/administration" >=> Admin.adminDashboard
route "/my-info" >=> User.myInfo
subRoute "/page" (choose [
route "s" >=> Page.all 1
routef "s/page/%i" Page.all
@ -134,6 +135,11 @@ let router : HttpHandler = choose [
subRoute "/rss" (choose [
route "" >=> Feed.editSettings
routef "/%s/edit" Feed.editCustomFeed
])
subRoute "/user" (choose [
route "s" >=> User.all
routef "/%s/edit" User.edit
])
subRoute "/tag-mapping" (choose [
route "s" >=> Admin.tagMappings
@ -149,12 +155,6 @@ let router : HttpHandler = choose [
route "s" >=> Upload.list
route "/new" >=> Upload.showNew
])
subRoute "/user" (choose [
route "s" >=> User.all
route "s/bare" >=> User.bare
route "/my-info" >=> User.myInfo
routef "/%s/edit" User.edit
])
]
POST >=> validateCsrf >=> choose [
subRoute "/cache" (choose [
@ -165,6 +165,7 @@ let router : HttpHandler = choose [
route "/save" >=> Admin.saveCategory
routef "/%s/delete" Admin.deleteCategory
])
route "/my-info" >=> User.saveMyInfo
subRoute "/page" (choose [
route "/save" >=> Page.save
route "/permalinks" >=> Page.savePermalinks
@ -192,6 +193,10 @@ let router : HttpHandler = choose [
route "/save" >=> Admin.saveMapping
routef "/%s/delete" Admin.deleteMapping
])
subRoute "/user" (choose [
route "/save" >=> User.save
routef "/%s/delete" User.delete
])
])
subRoute "/theme" (choose [
route "/new" >=> Admin.saveTheme
@ -202,11 +207,6 @@ let router : HttpHandler = choose [
routexp "/delete/(.*)" Upload.deleteFromDisk
routef "/%s/delete" Upload.deleteFromDb
])
subRoute "/user" (choose [
route "/my-info" >=> User.saveMyInfo
route "/save" >=> User.save
routef "/%s/delete" User.delete
])
]
])
GET_HEAD >=> routexp "/category/(.*)" Post.pageOfCategorizedPosts

View File

@ -72,34 +72,18 @@ let logOff : HttpHandler = fun next ctx -> task {
open System.Collections.Generic
open Giraffe.Htmx
open Microsoft.AspNetCore.Http
/// Create the hash needed to display the user list
let private userListHash (ctx : HttpContext) = task {
/// Got no time for URL/form manipulators...
let private goAway : HttpHandler = RequestErrors.BAD_REQUEST "really?"
// GET /admin/settings/users
let all : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
let! users = ctx.Data.WebLogUser.FindByWebLog ctx.WebLog.Id
return!
hashForPage "User Administration"
|> withAntiCsrf ctx
|> addToHash "users" (users |> List.map (DisplayUser.fromUser ctx.WebLog) |> Array.ofList)
|> addViewContext ctx
}
/// Got no time for URL/form manipulators...
let private goAway : HttpHandler = RequestErrors.BAD_REQUEST "really?"
// GET /admin/users
let all : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
let! hash = userListHash ctx
let! tmpl = TemplateCache.get adminTheme "user-list-body" ctx.Data
return!
addToHash "user_list" (tmpl.Render hash) hash
|> adminView "user-list" next ctx
}
// GET /admin/users/bare
let bare : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
let! hash = userListHash ctx
return! adminBareView "user-list-body" next ctx hash
|> adminBareView "user-list-body" next ctx
}
/// Show the edit user page
@ -116,7 +100,7 @@ let private showEdit (model : EditUserModel) : HttpHandler = fun next ctx ->
|]
|> adminBareView "user-edit" next ctx
// GET /admin/user/{id}/edit
// GET /admin/settings/user/{id}/edit
let edit usrId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
let isNew = usrId = "new"
let userId = WebLogUserId usrId
@ -128,7 +112,7 @@ let edit usrId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> tas
| None -> return! Error.notFound next ctx
}
// POST /admin/user/{id}/delete
// POST /admin/settings/user/{id}/delete
let delete userId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
let data = ctx.Data
match! data.WebLogUser.FindById (WebLogUserId userId) ctx.WebLog.Id with
@ -142,14 +126,14 @@ let delete userId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx ->
{ UserMessage.success with
Message = $"User {WebLogUser.displayName user} deleted successfully"
}
return! bare next ctx
return! all next ctx
| Error msg ->
do! addMessage ctx
{ UserMessage.error with
Message = $"User {WebLogUser.displayName user} was not deleted"
Detail = Some msg
}
return! bare next ctx
return! all next ctx
| None -> return! Error.notFound next ctx
}
@ -164,14 +148,14 @@ let private showMyInfo (model : EditMyInfoModel) (user : WebLogUser) : HttpHandl
|> adminView "my-info" next ctx
// GET /admin/user/my-info
// GET /admin/my-info
let myInfo : HttpHandler = requireAccess Author >=> fun next ctx -> task {
match! ctx.Data.WebLogUser.FindById ctx.UserId ctx.WebLog.Id with
| Some user -> return! showMyInfo (EditMyInfoModel.fromUser user) user next ctx
| None -> return! Error.notFound next ctx
}
// POST /admin/user/my-info
// POST /admin/my-info
let saveMyInfo : HttpHandler = requireAccess Author >=> fun next ctx -> task {
let! model = ctx.BindFormAsync<EditMyInfoModel> ()
let data = ctx.Data
@ -194,7 +178,7 @@ let saveMyInfo : HttpHandler = requireAccess Author >=> fun next ctx -> task {
do! data.WebLogUser.Update user
let pwMsg = if model.NewPassword = "" then "" else " and updated your password"
do! addMessage ctx { UserMessage.success with Message = $"Saved your information{pwMsg} successfully" }
return! redirectToGet "admin/user/my-info" next ctx
return! redirectToGet "admin/my-info" next ctx
| Some user ->
do! addMessage ctx { UserMessage.error with Message = "Passwords did not match; no updates made" }
return! showMyInfo { model with NewPassword = ""; NewPasswordConfirm = "" } user next ctx
@ -204,7 +188,7 @@ let saveMyInfo : HttpHandler = requireAccess Author >=> fun next ctx -> task {
// User save is not statically compilable; not sure why, but we'll revisit it at some point
#nowarn "3511"
// POST /admin/user/save
// POST /admin/settings/user/save
let save : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
let! model = ctx.BindFormAsync<EditUserModel> ()
let data = ctx.Data
@ -232,7 +216,7 @@ let save : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
{ UserMessage.success with
Message = $"""{if model.IsNew then "Add" else "Updat"}ed user successfully"""
}
return! bare next ctx
return! all next ctx
| Some _ ->
do! addMessage ctx { UserMessage.error with Message = "The passwords did not match; nothing saved" }
return!

View File

@ -17,11 +17,10 @@
{%- endif %}
{%- if is_web_log_admin %}
{{ "admin/categories" | nav_link: "Categories" }}
{{ "admin/users" | nav_link: "Users" }}
{{ "admin/settings" | nav_link: "Settings" }}
{%- endif %}
{%- if is_administrator %}
{{ "admin/dashboard/administration" | nav_link: "Admin" }}
{{ "admin/administration" | nav_link: "Admin" }}
{%- endif %}
</ul>
{%- endif %}

View File

@ -0,0 +1,4 @@
{%- assign user_col = "col-12 col-md-4 col-xl-3" -%}
{%- assign email_col = "col-12 col-md-4 col-xl-4" -%}
{%- assign cre8_col = "d-none d-xl-block col-xl-2" -%}
{%- assign last_col = "col-12 col-md-4 col-xl-3" -%}

View File

@ -1,6 +1,6 @@
<h2 class="my-3">{{ page_title }}</h2>
<article>
<fieldset class="container mb-3">
<fieldset class="container mb-3 pb-0">
<legend>Themes</legend>
<div class="row">
<div class="col">
@ -8,7 +8,7 @@
hx-target="#theme_new">
Upload a New Theme
</a>
<div class="container">
<div class="container g-0">
{% include_template "_theme-list-columns" %}
<div class="row mwl-table-heading">
<div class="{{ theme_col }}">Theme</div>
@ -21,10 +21,10 @@
</div>
</div>
</fieldset>
<fieldset class="container">
<fieldset class="container mb-3 pb-0">
{%- assign cache_base_url = "admin/cache/" -%}
<legend>Caches</legend>
<div class="row pb-3">
<div class="row pb-2">
<div class="col">
<p>
myWebLog uses a few caches to ensure that it serves pages as fast as possible. Normal actions taken within the
@ -38,31 +38,31 @@
<div class="col-12 col-lg-6 pb-3">
<div class="card">
<header class="card-header text-white bg-secondary">Web Logs</header>
<div class="card-body">
<div class="card-body pb-0">
<h6 class="card-subtitle text-muted pb-3">
These caches include the page list and categories for each web log
</h6>
{%- assign web_log_base_url = cache_base_url | append: "web-log/" -%}
<form method="post" class="container pb-3" hx-boost="false" hx-target="body" hx-swap="innerHTML show:window:top">
<form method="post" class="container g-0" hx-boost="false" hx-target="body"
hx-swap="innerHTML show:window:top">
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
<button type="submit" class="btn btn-sm btn-primary pb-2"
<button type="submit" class="btn btn-sm btn-primary mb-2"
hx-post="{{ web_log_base_url | append: "all/refresh" | relative_link }}">
Refresh All
</button>
<div class="row mwl-table-heading">
<div class="col">Name</div>
<div class="col">URL Base</div>
<div class="col">Web Log</div>
</div>
{%- for web_log in web_logs %}
<div class="row mwl-table-detail">
<div class="col">
{{ web_log[1] }}<br>
<small>
<span class="text-muted">{{ web_log[2] }}</span><br>
{%- assign refresh_url = web_log_base_url | append: web_log[0] | append: "/refresh" | relative_link -%}
<a href="{{ refresh_url }}" hx-post="{{ refresh_url }}">Refresh</a>
</small>
</div>
<div class="col">{{ web_log[2] }}</div>
</div>
{%- endfor %}
</form>
@ -72,21 +72,22 @@
<div class="col-12 col-lg-6 pb-3">
<div class="card">
<header class="card-header text-white bg-secondary">Themes</header>
<div class="card-body">
<div class="card-body pb-0">
<h6 class="card-subtitle text-muted pb-3">
The themes template cache is loaded on demand; refresh a cache with 0 templates will still refresh the
theme asset cache
The theme template cache is filled on demand as pages are displayed; refreshing a theme with no cached
templates will still refresh its asset cache
</h6>
{%- assign theme_base_url = cache_base_url | append: "theme/" -%}
<form method="post" class="container pb-3" hx-boost="false" hx-target="body" hx-swap="innerHTML show:window:top">
<form method="post" class="container g-0" hx-boost="false" hx-target="body"
hx-swap="innerHTML show:window:top">
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
<button type="submit" class="btn btn-sm btn-primary pb-2"
<button type="submit" class="btn btn-sm btn-primary mb-2"
hx-post="{{ theme_base_url | append: "all/refresh" | relative_link }}">
Refresh All
</button>
<div class="row mwl-table-heading">
<div class="col-8">Name</div>
<div class="col-4">Cached Templates</div>
<div class="col-8">Theme</div>
<div class="col-4">Cached</div>
</div>
{%- for theme in cached_themes %}
{% unless theme[0] == "admin" %}

View File

@ -1,10 +1,19 @@
<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>
<div id="catList" class="container">
<div class="row">
<div class="col">
{%- 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" -%}
<div class="container">
<div class="row mwl-table-heading">
<div class="{{ cat_col }}">Category<span class="d-md-none">; Description</span></div>
<div class="{{ desc_col }} d-none d-md-inline-block">Description</div>
</div>
</div>
<form 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 }}">
<div class="row mwl-table-detail" id="cat_new"></div>
{% for cat in categories -%}
<div class="row mwl-table-detail" id="cat_{{ cat.id }}">
<div class="{{ cat_col }} no-wrap">
@ -37,9 +46,12 @@
</div>
</div>
{%- endfor %}
</form>
{%- else -%}
<div class="row">
<div class="col-12 text-muted fst-italic text-center">This web log has no categores defined</div>
<div id="cat_new">
<p class="text-muted fst-italic text-center">This web log has no categores defined</p>
</div>
{%- endif %}
</form>
</div>
</div>
</div>

View File

@ -4,13 +4,5 @@
hx-target="#cat_new">
Add a New Category
</a>
<div class="container">
{%- 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" -%}
<div class="row mwl-table-heading">
<div class="{{ cat_col }}">Category<span class="d-md-none">; Description</span></div>
<div class="{{ desc_col }} d-none d-md-inline-block">Description</div>
</div>
</div>
{{ category_list }}
</article>

View File

@ -1,6 +1,6 @@
<h2 class="my-3">{{ page_title }}</h2>
<article>
<form action="{{ "admin/user/my-info" | relative_link }}" method="post">
<form action="{{ "admin/my-info" | relative_link }}" method="post">
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
<div class="d-flex flex-row flex-wrap justify-content-around">
<div class="text-center mb-3 lh-sm">

View File

@ -2,6 +2,7 @@
<article>
<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 -%}
{% if page_count > 0 %}
{%- assign title_col = "col-12 col-md-5" -%}
{%- assign link_col = "col-12 col-md-5" -%}
{%- assign upd8_col = "col-12 col-md-2" -%}
@ -14,7 +15,6 @@
<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 %}
{% for pg in pages -%}
<div class="row mwl-table-detail">
<div class="{{ title_col }}">
@ -48,18 +48,13 @@
</div>
</div>
{%- endfor %}
{% else %}
<div class="row">
<div class="col text-muted fst-italic text-center">This web log has no pages</div>
</div>
{% endif %}
</form>
{% if page_nbr > 1 or page_count == 25 %}
<div class="d-flex justify-content-evenly pb-3">
<div class="d-flex justify-content-evenly mb-3">
<div>
{% if page_nbr > 1 %}
<p>
<a class="btn btn-default" href="{{ "admin/pages" | append: prev_page | relative_link }}">
<a class="btn btn-secondary" href="{{ "admin/pages" | append: prev_page | relative_link }}">
&laquo; Previous
</a>
</p>
@ -68,10 +63,15 @@
<div class="text-right">
{% if page_count == 25 %}
<p>
<a class="btn btn-default" href="{{ "admin/pages" | append: next_page | relative_link }}">Next &raquo;</a>
<a class="btn btn-secondary" href="{{ "admin/pages" | append: next_page | relative_link }}">
Next &raquo;
</a>
</p>
{% endif %}
</div>
</div>
{% endif %}
{% else %}
<p class="text-muted fst-italic text-center">This web log has no pages</p>
{% endif %}
</article>

View File

@ -1,9 +1,10 @@
<h2 class="my-3">{{ page_title }}</h2>
<article>
<a href="{{ "admin/post/new/edit" | relative_link }}" class="btn btn-primary btn-sm mb-3">Write a New Post</a>
{%- assign post_count = model.posts | size -%}
{%- if post_count > 0 %}
<form method="post" class="container" hx-target="body">
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
{%- 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" -%}
@ -16,7 +17,6 @@
<div class="{{ author_col }} d-none d-md-inline-block">Author</div>
<div class="{{ tag_col }}">Tags</div>
</div>
{%- if post_count > 0 %}
{% for post in model.posts -%}
<div class="row mwl-table-detail">
<div class="{{ date_col }} no-wrap">
@ -77,24 +77,22 @@
</div>
</div>
{%- endfor %}
{% else %}
<div class="row">
<div class="col text-muted fst-italic text-center">This web log has no posts</div>
</div>
{% endif %}
</form>
{% if model.newer_link or model.older_link %}
<div class="d-flex justify-content-evenly">
<div class="d-flex justify-content-evenly mb-3">
<div>
{% if model.newer_link %}
<p><a class="btn btn-default" href="{{ model.newer_link.value }}">&laquo; Newer Posts</a></p>
<p><a class="btn btn-secondary" href="{{ model.newer_link.value }}">&laquo; Newer Posts</a></p>
{% endif %}
</div>
<div class="text-right">
{% if model.older_link %}
<p><a class="btn btn-default" href="{{ model.older_link.value }}">Older Posts &raquo;</a></p>
<p><a class="btn btn-secondary" href="{{ model.older_link.value }}">Older Posts &raquo;</a></p>
{% endif %}
</div>
</div>
{% endif %}
{% else %}
<p class="text-muted fst-italic text-center">This web log has no posts</p>
{% endif %}
</article>

View File

@ -63,11 +63,16 @@
</div>
</div>
</form>
<h3>Custom Feeds</h3>
<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>
<form method="post" class="container" hx-target="body">
{%- 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 }}">
@ -77,8 +82,6 @@
</div>
<div class="{{ path_col }} d-none d-md-inline-block">Relative Path</div>
</div>
{%- assign feed_count = custom_feeds | size -%}
{% if feed_count > 0 %}
{% for feed in custom_feeds %}
<div class="row mwl-table-detail">
<div class="{{ source_col }}">
@ -103,10 +106,11 @@
</div>
</div>
{% endfor %}
{% else %}
<tr>
<td colspan="3" class="text-muted fst-italic text-center">No custom feeds defined</td>
</tr>
{% endif %}
</form>
{% else %}
<p class="text-muted fst-italic text-center">No custom feeds defined</p>
{% endif %}
</div>
</div>
</fieldset>
</article>

View File

@ -1,9 +1,13 @@
<h2 class="my-3">{{ web_log.name }} Settings</h2>
<article>
<p class="text-muted">
Other Settings: <a href="{{ "admin/settings/tag-mappings" | relative_link }}">Tag Mappings</a> &bull;
<a href="{{ "admin/settings/rss" | relative_link }}">RSS Settings</a>
</p>
<article>
<fieldset class="container mb-3">
<legend>Web Log Settings</legend>
<div class="row">
<div class="col">
<form action="{{ "admin/settings" | relative_link }}" method="post">
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
<div class="container">
@ -21,7 +25,8 @@
value="{{ model.slug }}">
<label for="slug">Slug</label>
<span class="form-text">
<span class="badge rounded-pill bg-warning text-dark">WARNING</span> changing this value may break links
<span class="badge rounded-pill bg-warning text-dark">WARNING</span> changing this value may break
links
(<a href="https://bitbadger.solutions/open-source/myweblog/configuring.html#blog-settings"
target="_blank">more</a>)
</span>
@ -61,8 +66,8 @@
</div>
<div class="col-12 col-md-4 col-xl-2 pb-3">
<div class="form-floating">
<input type="number" name="PostsPerPage" id="postsPerPage" class="form-control" min="0" max="50" required
value="{{ model.posts_per_page }}">
<input type="number" name="PostsPerPage" id="postsPerPage" class="form-control" min="0" max="50"
required value="{{ model.posts_per_page }}">
<label for="postsPerPage">Posts per Page</label>
</div>
</div>
@ -89,7 +94,8 @@
<div class="form-floating">
<select name="Uploads" id="uploads" class="form-control">
{%- for it in upload_values %}
<option value="{{ it[0] }}"{% if model.uploads == it[0] %} selected{% endif %}>{{ it[1] }}</option>
<option value="{{ it[0] }}"
{%- if model.uploads == it[0] %} selected{% endif %}>{{ it[1] }}</option>
{%- endfor %}
</select>
<label for="uploads">Default Upload Destination</label>
@ -103,4 +109,28 @@
</div>
</div>
</form>
</div>
</div>
</fieldset>
<fieldset class="container mb-3 pb-0">
<legend>Users</legend>
<div class="row">
<div class="col">
{% include_template "_user-list-columns" %}
<a href="{{ "admin/settings/user/new/edit" | relative_link }}" class="btn btn-primary btn-sm mb-3"
hx-target="#user_new">
Add a New User
</a>
<div class="container g-0">
<div class="row mwl-table-heading">
<div class="{{ user_col }}">User<span class="d-md-none">; Full Name / E-mail; Last Log On</span></div>
<div class="{{ email_col }} d-none d-md-inline-block">Full Name / E-mail</div>
<div class="{{ cre8_col }}">Created</div>
<div class="{{ last_col }} d-none d-md-block">Last Log On</div>
</div>
</div>
{{ user_list }}
</div>
</div>
</fieldset>
</article>

View File

@ -1,8 +1,17 @@
<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>
<div id="tagList" class="container">
<div class="row">
<div class="col">
{%- assign map_count = mappings | size -%}
{% if map_count > 0 -%}
<div class="container">
<div class="row mwl-table-heading">
<div class="col">Tag</div>
<div class="col">URL Value</div>
</div>
</div>
<form method="post" class="container" hx-target="#tagList" 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>
{% for map in mappings -%}
{%- assign map_id = mapping_ids | value: map.tag -%}
<div class="row mwl-table-detail" id="tag_{{ map_id }}">
@ -25,9 +34,12 @@
<div class="col">{{ map.url_value }}</div>
</div>
{%- endfor %}
</form>
{%- else -%}
<div class="row">
<div class="col text-muted text-center fst-italic">This web log has no tag mappings</div>
<div id="tag_new">
<p class="text-muted text-center fst-italic">This web log has no tag mappings</p>
</div>
{%- endif %}
</form>
</div>
</div>
</div>

View File

@ -4,11 +4,5 @@
hx-target="#tag_new">
Add a New Tag Mapping
</a>
<div class="container">
<div class="row mwl-table-heading">
<div class="col">Tag</div>
<div class="col">URL Value</div>
</div>
</div>
{{ tag_mapping_list }}
</article>

View File

@ -1,4 +1,4 @@
<form method="post" id="themeList" class="container" hx-target="this" hx-swap="outerHTML show:window:top">
<form method="post" id="themeList" class="container g-0" hx-target="this" hx-swap="outerHTML show:window:top">
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
{% include_template "_theme-list-columns" %}
{% for theme in themes -%}

View File

@ -1,5 +1,5 @@
<div class="col">
<h5>{{ page_title }}</h5>
<h5 class="mt-2">{{ page_title }}</h5>
<form action="{{ "admin/theme/new" | relative_link }}" method="post" class="container" enctype="multipart/form-data"
hx-boost="false">
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">

View File

@ -7,15 +7,15 @@
<form method="post" class="container" hx-target="body">
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
<div class="row">
<div class="col text-muted text-center"><em>Uploaded files served from</em><br>{{ upload_base }}</div>
<div class="col text-center"><em class="text-muted">Uploaded files served from</em><br>{{ upload_base }}</div>
</div>
{%- assign file_count = files | size -%}
{%- if file_count > 0 %}
<div class="row mwl-table-heading">
<div class="col-6">File Name</div>
<div class="col-3">Path</div>
<div class="col-3">File Date/Time</div>
</div>
{%- assign file_count = files | size -%}
{%- if file_count > 0 %}
{% for file in files %}
<div class="row mwl-table-detail">
<div class="col-6">
@ -69,7 +69,7 @@
{% endfor %}
{%- else -%}
<div class="row">
<div class="col text-muted fst-italic text-center">This web log has uploaded files</div>
<div class="col text-muted fst-italic text-center"><br>This web log has uploaded files</div>
</div>
{%- endif %}
</form>

View File

@ -1,13 +1,13 @@
<div class="col-12">
<h5 class="my-3">{{ page_title }}</h5>
<form hx-post="{{ "admin/user/save" | relative_link }}" method="post" class="container"
<form hx-post="{{ "admin/settings/user/save" | relative_link }}" method="post" class="container"
hx-target="#userList" hx-swap="outerHTML show:window:top">
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
<input type="hidden" name="Id" value="{{ model.id }}">
<div class="row">
<div class="col-12 col-md-5 col-lg-3 col-xxl-2 offset-xxl-1 mb-3">
<div class="form-floating">
<select name="AccessLevel" id="accessLevel" class="form-control" required>
<select name="AccessLevel" id="accessLevel" class="form-control" required autofocus>
{%- for level in access_levels %}
<option value="{{ level[0] }}"{% if model.access_level == level[0] %} selected{% endif %}>
{{ level[1] }}
@ -88,7 +88,14 @@
<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/users/bare" | relative_link }}" class="btn btn-sm btn-secondary ms-3">Cancel</a>
{% if model.is_new %}
<button type="button" class="btn btn-sm btn-secondary ms-3"
onclick="document.getElementById('user_new').innerHTML = ''">
Cancel
</button>
{% else %}
<a href="{{ "admin/settings/users" | relative_link }}" class="btn btn-sm btn-secondary ms-3">Cancel</a>
{% endif %}
</div>
</div>
</form>

View File

@ -1,10 +1,10 @@
<form method="post" id="userList" class="container" hx-target="this" hx-swap="outerHTML show:window:top">
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
<div id="userList">
<div class="container g-0">
<div class="row mwl-table-detail" id="user_new"></div>
{%- assign user_col = "col-12 col-md-4 col-xl-3" -%}
{%- assign email_col = "col-12 col-md-4 col-xl-4" -%}
{%- assign cre8_col = "d-none d-xl-block col-xl-2" -%}
{%- assign last_col = "col-12 col-md-4 col-xl-3" -%}
</div>
<form method="post" id="userList" class="container g-0" hx-target="this" hx-swap="outerHTML show:window:top">
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
{% include_template "_user-list-columns" %}
{%- assign badge = "ms-2 badge bg" -%}
{% for user in users -%}
<div class="row mwl-table-detail" id="user_{{ user.id }}">
@ -21,7 +21,7 @@
{%- endif %}<br>
{%- unless is_administrator == false and user.access_level == "Administrator" %}
<small>
{%- assign user_url_base = "admin/user/" | append: user.id -%}
{%- assign user_url_base = "admin/settings/user/" | append: user.id -%}
<a href="{{ user_url_base | append: "/edit" | relative_link }}" hx-target="#user_{{ user.id }}"
hx-swap="innerHTML show:#user_{{ user.id }}:top">
Edit
@ -58,3 +58,4 @@
</div>
{%- endfor %}
</form>
</div>

View File

@ -1,20 +0,0 @@
<h2 class="my-3">{{ page_title }}</h2>
<article>
<a href="{{ "admin/user/new/edit" | relative_link }}" class="btn btn-primary btn-sm mb-3"
hx-target="#user_new">
Add a New User
</a>
<div class="container">
{%- assign user_col = "col-12 col-md-4 col-xl-3" -%}
{%- assign email_col = "col-12 col-md-4 col-xl-4" -%}
{%- assign cre8_col = "d-none d-xl-block col-xl-2" -%}
{%- assign last_col = "col-12 col-md-4 col-xl-3" -%}
<div class="row mwl-table-heading">
<div class="{{ user_col }}">User<span class="d-md-none">; Details; Last Log On</span></div>
<div class="{{ email_col }} d-none d-md-inline-block">Details</div>
<div class="{{ cre8_col }}">Created</div>
<div class="{{ last_col }} d-none d-md-block">Last Log On</div>
</div>
</div>
{{ user_list }}
</article>