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:
parent
ff9c08842b
commit
3189681021
@ -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
|
||||
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
|
||||
text <- text.Replace (child.Value, childText)
|
||||
_cache[templatePath] <- Template.Parse (text, SyntaxCompatibility.DotLiquid22)
|
||||
| None -> ()
|
||||
return _cache[templatePath]
|
||||
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 =
|
||||
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)
|
||||
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
|
||||
|
@ -32,38 +32,43 @@ 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 {
|
||||
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"
|
||||
|> withAntiCsrf ctx
|
||||
|> addToHash "themes" (themes |> List.map (DisplayTheme.fromTheme WebLogCache.isThemeInUse) |> Array.ofList)
|
||||
|> addToHash "cached_themes" (
|
||||
themes
|
||||
|> Seq.ofList
|
||||
|> Seq.map (fun it -> [|
|
||||
ThemeId.toString it.Id
|
||||
it.Name
|
||||
cachedTemplates |> List.filter (fun n -> n.StartsWith (ThemeId.toString it.Id)) |> List.length |> string
|
||||
|])
|
||||
|> Array.ofSeq)
|
||||
|> addToHash "web_logs" (
|
||||
WebLogCache.all ()
|
||||
|> Seq.ofList
|
||||
|> Seq.sortBy (fun it -> it.Name)
|
||||
|> Seq.map (fun it -> [| WebLogId.toString it.Id; it.Name; it.UrlBase |])
|
||||
|> Array.ofSeq)
|
||||
|> addViewContext ctx
|
||||
return!
|
||||
addToHash "theme_list" (bodyTemplate.Render hash) hash
|
||||
|> adminView "admin-dashboard" next ctx
|
||||
match! TemplateCache.get adminTheme "theme-list-body" ctx.Data with
|
||||
| Ok bodyTemplate ->
|
||||
let! themes = ctx.Data.Theme.All ()
|
||||
let cachedTemplates = TemplateCache.allNames ()
|
||||
let! hash =
|
||||
hashForPage "myWebLog Administration"
|
||||
|> withAntiCsrf ctx
|
||||
|> addToHash "themes" (themes |> List.map (DisplayTheme.fromTheme WebLogCache.isThemeInUse) |> Array.ofList)
|
||||
|> addToHash "cached_themes" (
|
||||
themes
|
||||
|> Seq.ofList
|
||||
|> Seq.map (fun it -> [|
|
||||
ThemeId.toString it.Id
|
||||
it.Name
|
||||
cachedTemplates
|
||||
|> List.filter (fun n -> n.StartsWith (ThemeId.toString it.Id))
|
||||
|> List.length
|
||||
|> string
|
||||
|])
|
||||
|> Array.ofSeq)
|
||||
|> addToHash "web_logs" (
|
||||
WebLogCache.all ()
|
||||
|> Seq.ofList
|
||||
|> Seq.sortBy (fun it -> it.Name)
|
||||
|> Seq.map (fun it -> [| WebLogId.toString it.Id; it.Name; it.UrlBase |])
|
||||
|> Array.ofSeq)
|
||||
|> addViewContext 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,14 +122,16 @@ 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
|
||||
let! hash =
|
||||
hashForPage "Categories"
|
||||
|> withAntiCsrf ctx
|
||||
|> addViewContext ctx
|
||||
return!
|
||||
addToHash "category_list" (catListTemplate.Render hash) hash
|
||||
|> adminView "category-list" next ctx
|
||||
match! TemplateCache.get adminTheme "category-list-body" ctx.Data with
|
||||
| Ok catListTemplate ->
|
||||
let! hash =
|
||||
hashForPage "Categories"
|
||||
|> withAntiCsrf ctx
|
||||
|> addViewContext 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 {
|
||||
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
|
||||
match! TemplateCache.get adminTheme "tag-mapping-list-body" ctx.Data with
|
||||
| Ok listTemplate ->
|
||||
let! hash = tagMappingHash ctx
|
||||
return!
|
||||
addToHash "tag_mapping_list" (listTemplate.Render hash) hash
|
||||
|> adminView "tag-mapping-list" next ctx
|
||||
| Error message -> return! Error.server message next ctx
|
||||
}
|
||||
|
||||
// GET /admin/settings/tag-mappings/bare
|
||||
@ -421,31 +430,39 @@ open System.Collections.Generic
|
||||
|
||||
// GET /admin/settings
|
||||
let settings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||
let data = ctx.Data
|
||||
let! allPages = data.Page.All ctx.WebLog.Id
|
||||
let! themes = data.Theme.All ()
|
||||
return!
|
||||
hashForPage "Web Log Settings"
|
||||
|> withAntiCsrf ctx
|
||||
|> addToHash ViewContext.Model (SettingsModel.fromWebLog ctx.WebLog)
|
||||
|> addToHash "pages" (
|
||||
seq {
|
||||
KeyValuePair.Create ("posts", "- First Page of Posts -")
|
||||
yield! allPages
|
||||
|> List.sortBy (fun p -> p.Title.ToLower ())
|
||||
|> List.map (fun p -> KeyValuePair.Create (PageId.toString p.Id, p.Title))
|
||||
}
|
||||
|> Array.ofSeq)
|
||||
|> addToHash "themes" (
|
||||
themes
|
||||
|> Seq.ofList
|
||||
|> Seq.map (fun it -> KeyValuePair.Create (ThemeId.toString it.Id, $"{it.Name} (v{it.Version})"))
|
||||
|> Array.ofSeq)
|
||||
|> addToHash "upload_values" [|
|
||||
KeyValuePair.Create (UploadDestination.toString Database, "Database")
|
||||
KeyValuePair.Create (UploadDestination.toString Disk, "Disk")
|
||||
|]
|
||||
|> adminView "settings" next ctx
|
||||
let data = ctx.Data
|
||||
match! TemplateCache.get adminTheme "user-list-body" data with
|
||||
| Ok userTemplate ->
|
||||
let! allPages = data.Page.All ctx.WebLog.Id
|
||||
let! themes = data.Theme.All ()
|
||||
let! users = data.WebLogUser.FindByWebLog ctx.WebLog.Id
|
||||
let! hash =
|
||||
hashForPage "Web Log Settings"
|
||||
|> withAntiCsrf ctx
|
||||
|> addToHash ViewContext.Model (SettingsModel.fromWebLog ctx.WebLog)
|
||||
|> addToHash "pages" (
|
||||
seq {
|
||||
KeyValuePair.Create ("posts", "- First Page of Posts -")
|
||||
yield! allPages
|
||||
|> List.sortBy (fun p -> p.Title.ToLower ())
|
||||
|> List.map (fun p -> KeyValuePair.Create (PageId.toString p.Id, p.Title))
|
||||
}
|
||||
|> Array.ofSeq)
|
||||
|> addToHash "themes" (
|
||||
themes
|
||||
|> Seq.ofList
|
||||
|> Seq.map (fun it -> KeyValuePair.Create (ThemeId.toString it.Id, $"{it.Name} (v{it.Version})"))
|
||||
|> Array.ofSeq)
|
||||
|> addToHash "upload_values" [|
|
||||
KeyValuePair.Create (UploadDestination.toString Database, "Database")
|
||||
KeyValuePair.Create (UploadDestination.toString Disk, "Disk")
|
||||
|]
|
||||
|> addToHash "users" (users |> List.map (DisplayUser.fromUser ctx.WebLog) |> Array.ofList)
|
||||
|> addViewContext ctx
|
||||
return!
|
||||
addToHash "user_list" (userTemplate.Render hash) hash
|
||||
|> adminView "settings" next ctx
|
||||
| Error message -> return! Error.server message next ctx
|
||||
}
|
||||
|
||||
// POST /admin/settings
|
||||
|
@ -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
|
||||
else
|
||||
(setStatusCode 404 >=> text "Not found") 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
|
||||
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
|
||||
|
@ -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 "/dashboard" >=> Admin.dashboard
|
||||
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
|
||||
|
@ -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!
|
||||
|
@ -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 %}
|
||||
|
4
src/admin-theme/_user-list-columns.liquid
Normal file
4
src/admin-theme/_user-list-columns.liquid
Normal 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" -%}
|
@ -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" %}
|
||||
|
@ -1,45 +1,57 @@
|
||||
<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>
|
||||
{%- assign cat_url_base = "admin/category/" | append: cat.id -%}
|
||||
{%- 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 %}
|
||||
<a href="{{ cat_url_base | append: "/edit" | relative_link }}" hx-target="#cat_{{ cat.id }}"
|
||||
hx-swap="innerHTML show:#cat_{{ cat.id }}:top">
|
||||
Edit
|
||||
</a>
|
||||
<span class="text-muted"> • </span>
|
||||
{%- assign cat_del_link = cat_url_base | append: "/delete" | relative_link -%}
|
||||
<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 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>
|
||||
<div class="{{ desc_col }}">
|
||||
{%- if cat.description %}{{ cat.description.value }}{% else %}<em class="text-muted">none</em>{% endif %}
|
||||
<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">
|
||||
{%- if cat.parent_names %}
|
||||
<small class="text-muted">{% for name in cat.parent_names %}{{ name }} ⟩ {% endfor %}</small>
|
||||
{%- endif %}
|
||||
{{ cat.name }}<br>
|
||||
<small>
|
||||
{%- assign cat_url_base = "admin/category/" | append: cat.id -%}
|
||||
{%- 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 %}
|
||||
<a href="{{ cat_url_base | append: "/edit" | relative_link }}" hx-target="#cat_{{ cat.id }}"
|
||||
hx-swap="innerHTML show:#cat_{{ cat.id }}:top">
|
||||
Edit
|
||||
</a>
|
||||
<span class="text-muted"> • </span>
|
||||
{%- assign cat_del_link = cat_url_base | append: "/delete" | relative_link -%}
|
||||
<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 %}
|
||||
</form>
|
||||
{%- else -%}
|
||||
<div id="cat_new">
|
||||
<p class="text-muted fst-italic text-center">This web log has no categores defined</p>
|
||||
</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>
|
||||
{%- endif %}
|
||||
</div>
|
||||
{%- endif %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -2,19 +2,19 @@
|
||||
<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 -%}
|
||||
{%- assign title_col = "col-12 col-md-5" -%}
|
||||
{%- assign link_col = "col-12 col-md-5" -%}
|
||||
{%- assign upd8_col = "col-12 col-md-2" -%}
|
||||
<form method="post" class="container" hx-target="body">
|
||||
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
||||
<div class="row mwl-table-heading">
|
||||
<div class="{{ title_col }}">
|
||||
<span class="d-none d-md-inline">Title</span><span class="d-md-none">Page</span>
|
||||
{% 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" -%}
|
||||
<form method="post" class="container" hx-target="body">
|
||||
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
||||
<div class="row mwl-table-heading">
|
||||
<div class="{{ title_col }}">
|
||||
<span class="d-none d-md-inline">Title</span><span class="d-md-none">Page</span>
|
||||
</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>
|
||||
<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,30 +48,30 @@
|
||||
</div>
|
||||
</div>
|
||||
{%- endfor %}
|
||||
{% else %}
|
||||
<div class="row">
|
||||
<div class="col text-muted fst-italic text-center">This web log has no pages</div>
|
||||
</form>
|
||||
{% if page_nbr > 1 or page_count == 25 %}
|
||||
<div class="d-flex justify-content-evenly mb-3">
|
||||
<div>
|
||||
{% if page_nbr > 1 %}
|
||||
<p>
|
||||
<a class="btn btn-secondary" href="{{ "admin/pages" | append: prev_page | relative_link }}">
|
||||
« Previous
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
{% if page_count == 25 %}
|
||||
<p>
|
||||
<a class="btn btn-secondary" href="{{ "admin/pages" | append: next_page | relative_link }}">
|
||||
Next »
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% if page_nbr > 1 or page_count == 25 %}
|
||||
<div class="d-flex justify-content-evenly pb-3">
|
||||
<div>
|
||||
{% if page_nbr > 1 %}
|
||||
<p>
|
||||
<a class="btn btn-default" href="{{ "admin/pages" | append: prev_page | relative_link }}">
|
||||
« Previous
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
{% if page_count == 25 %}
|
||||
<p>
|
||||
<a class="btn btn-default" href="{{ "admin/pages" | append: next_page | relative_link }}">Next »</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted fst-italic text-center">This web log has no pages</p>
|
||||
{% endif %}
|
||||
</article>
|
||||
|
@ -1,22 +1,22 @@
|
||||
<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>
|
||||
<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" -%}
|
||||
{%- 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>
|
||||
{%- 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 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>
|
||||
<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 %}
|
||||
{% 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>
|
||||
</form>
|
||||
{% if model.newer_link or model.older_link %}
|
||||
<div class="d-flex justify-content-evenly mb-3">
|
||||
<div>
|
||||
{% if model.newer_link %}
|
||||
<p><a class="btn btn-secondary" href="{{ model.newer_link.value }}">« Newer Posts</a></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
{% if model.older_link %}
|
||||
<p><a class="btn btn-secondary" href="{{ model.older_link.value }}">Older Posts »</a></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% if model.newer_link or model.older_link %}
|
||||
<div class="d-flex justify-content-evenly">
|
||||
<div>
|
||||
{% if model.newer_link %}
|
||||
<p><a class="btn btn-default" href="{{ model.newer_link.value }}">« 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 »</a></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted fst-italic text-center">This web log has no posts</p>
|
||||
{% endif %}
|
||||
</article>
|
||||
|
@ -63,50 +63,54 @@
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<h3>Custom Feeds</h3>
|
||||
<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 source_col = "col-12 col-md-6" -%}
|
||||
{%- assign path_col = "col-12 col-md-6" -%}
|
||||
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
||||
<div class="row mwl-table-heading">
|
||||
<div class="{{ source_col }}">
|
||||
<span class="d-md-none">Feed</span><span class="d-none d-md-inline">Source</span>
|
||||
<fieldset class="container mb-3 pb-0">
|
||||
<legend>Custom Feeds</legend>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<a class="btn btn-sm btn-secondary" href="{{ 'admin/settings/rss/new/edit' | relative_link }}">
|
||||
Add a New Custom Feed
|
||||
</a>
|
||||
{%- assign feed_count = custom_feeds | size -%}
|
||||
{% if feed_count > 0 %}
|
||||
<form method="post" class="container g-0" hx-target="body">
|
||||
{%- assign source_col = "col-12 col-md-6" -%}
|
||||
{%- assign path_col = "col-12 col-md-6" -%}
|
||||
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
||||
<div class="row mwl-table-heading">
|
||||
<div class="{{ source_col }}">
|
||||
<span class="d-md-none">Feed</span><span class="d-none d-md-inline">Source</span>
|
||||
</div>
|
||||
<div class="{{ path_col }} d-none d-md-inline-block">Relative Path</div>
|
||||
</div>
|
||||
{% for feed in custom_feeds %}
|
||||
<div class="row mwl-table-detail">
|
||||
<div class="{{ source_col }}">
|
||||
{{ feed.source }}
|
||||
{%- if feed.is_podcast %} <span class="badge bg-primary">PODCAST</span>{% endif %}<br>
|
||||
<small>
|
||||
{%- assign feed_url = "admin/settings/rss/" | append: feed.id -%}
|
||||
<a href="{{ feed.path | relative_link }}" target="_blank">View Feed</a>
|
||||
<span class="text-muted"> • </span>
|
||||
<a href="{{ feed_url | append: "/edit" | relative_link }}">Edit</a>
|
||||
<span class="text-muted"> • </span>
|
||||
{%- assign feed_del_link = feed_url | append: "/delete" | relative_link -%}
|
||||
<a href="{{ feed_del_link }}" hx-post="{{ feed_del_link }}" class="text-danger"
|
||||
hx-confirm="Are you sure you want to delete the custom RSS feed based on {{ feed.source | strip_html | escape }}? This action cannot be undone.">
|
||||
Delete
|
||||
</a>
|
||||
</small>
|
||||
</div>
|
||||
<div class="{{ path_col }}">
|
||||
<small class="d-md-none">Served at {{ feed.path }}</small>
|
||||
<span class="d-none d-md-inline">{{ feed.path }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="text-muted fst-italic text-center">No custom feeds defined</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div 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 }}">
|
||||
{{ feed.source }}
|
||||
{%- if feed.is_podcast %} <span class="badge bg-primary">PODCAST</span>{% endif %}<br>
|
||||
<small>
|
||||
{%- assign feed_url = "admin/settings/rss/" | append: feed.id -%}
|
||||
<a href="{{ feed.path | relative_link }}" target="_blank">View Feed</a>
|
||||
<span class="text-muted"> • </span>
|
||||
<a href="{{ feed_url | append: "/edit" | relative_link }}">Edit</a>
|
||||
<span class="text-muted"> • </span>
|
||||
{%- assign feed_del_link = feed_url | append: "/delete" | relative_link -%}
|
||||
<a href="{{ feed_del_link }}" hx-post="{{ feed_del_link }}" class="text-danger"
|
||||
hx-confirm="Are you sure you want to delete the custom RSS feed based on {{ feed.source | strip_html | escape }}? This action cannot be undone.">
|
||||
Delete
|
||||
</a>
|
||||
</small>
|
||||
</div>
|
||||
<div class="{{ path_col }}">
|
||||
<small class="d-md-none">Served at {{ feed.path }}</small>
|
||||
<span class="d-none d-md-inline">{{ feed.path }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="3" class="text-muted fst-italic text-center">No custom feeds defined</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</form>
|
||||
</fieldset>
|
||||
</article>
|
||||
|
@ -1,106 +1,136 @@
|
||||
<h2 class="my-3">{{ web_log.name }} Settings</h2>
|
||||
<p class="text-muted">
|
||||
Other Settings: <a href="{{ "admin/settings/tag-mappings" | relative_link }}">Tag Mappings</a> •
|
||||
<a href="{{ "admin/settings/rss" | relative_link }}">RSS Settings</a>
|
||||
</p>
|
||||
<article>
|
||||
<form action="{{ "admin/settings" | relative_link }}" method="post">
|
||||
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6 col-xl-4 pb-3">
|
||||
<div class="form-floating">
|
||||
<input type="text" name="Name" id="name" class="form-control" placeholder="Name" required autofocus
|
||||
value="{{ model.name }}">
|
||||
<label for="name">Name</label>
|
||||
<p class="text-muted">
|
||||
Other Settings: <a href="{{ "admin/settings/tag-mappings" | relative_link }}">Tag Mappings</a> •
|
||||
<a href="{{ "admin/settings/rss" | relative_link }}">RSS Settings</a>
|
||||
</p>
|
||||
<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">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6 col-xl-4 pb-3">
|
||||
<div class="form-floating">
|
||||
<input type="text" name="Name" id="name" class="form-control" placeholder="Name" required autofocus
|
||||
value="{{ model.name }}">
|
||||
<label for="name">Name</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 col-xl-4 pb-3">
|
||||
<div class="form-floating">
|
||||
<input type="text" name="Slug" id="slug" class="form-control" placeholder="Slug" required
|
||||
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
|
||||
(<a href="https://bitbadger.solutions/open-source/myweblog/configuring.html#blog-settings"
|
||||
target="_blank">more</a>)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 col-xl-4 pb-3">
|
||||
<div class="form-floating">
|
||||
<input type="text" name="Subtitle" id="subtitle" class="form-control" placeholder="Subtitle"
|
||||
value="{{ model.subtitle }}">
|
||||
<label for="subtitle">Subtitle</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 col-xl-4 offset-xl-1 pb-3">
|
||||
<div class="form-floating">
|
||||
<select name="ThemeId" id="themeId" class="form-control" required>
|
||||
{% for theme in themes -%}
|
||||
<option value="{{ theme[0] }}"{% if model.theme_id == theme[0] %} selected="selected"{% endif %}>
|
||||
{{ theme[1] }}
|
||||
</option>
|
||||
{%- endfor %}
|
||||
</select>
|
||||
<label for="themeId">Theme</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 offset-md-1 col-xl-4 offset-xl-0 pb-3">
|
||||
<div class="form-floating">
|
||||
<select name="DefaultPage" id="defaultPage" class="form-control" required>
|
||||
{% for pg in pages -%}
|
||||
<option value="{{ pg[0] }}"
|
||||
{%- if pg[0] == model.default_page %} selected="selected"{% endif %}>
|
||||
{{ pg[1] }}
|
||||
</option>
|
||||
{%- endfor %}
|
||||
</select>
|
||||
<label for="defaultPage">Default Page</label>
|
||||
</div>
|
||||
</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 }}">
|
||||
<label for="postsPerPage">Posts per Page</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-4 col-xl-3 offset-xl-2 pb-3">
|
||||
<div class="form-floating">
|
||||
<input type="text" name="TimeZone" id="timeZone" class="form-control" placeholder="Time Zone" required
|
||||
value="{{ model.time_zone }}">
|
||||
<label for="timeZone">Time Zone</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 col-xl-2">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" name="AutoHtmx" id="autoHtmx" class="form-check-input" value="true"
|
||||
{%- if model.auto_htmx %} checked="checked"{% endif %}>
|
||||
<label for="autoHtmx" class="form-check-label">Auto-Load htmx</label>
|
||||
</div>
|
||||
<span class="form-text fst-italic">
|
||||
<a href="https://htmx.org" target="_blank" rel="noopener">What is this?</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 col-xl-3 pb-3">
|
||||
<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>
|
||||
{%- endfor %}
|
||||
</select>
|
||||
<label for="uploads">Default Upload Destination</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row pb-3">
|
||||
<div class="col text-center">
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 col-xl-4 pb-3">
|
||||
<div class="form-floating">
|
||||
<input type="text" name="Slug" id="slug" class="form-control" placeholder="Slug" required
|
||||
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
|
||||
(<a href="https://bitbadger.solutions/open-source/myweblog/configuring.html#blog-settings"
|
||||
target="_blank">more</a>)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 col-xl-4 pb-3">
|
||||
<div class="form-floating">
|
||||
<input type="text" name="Subtitle" id="subtitle" class="form-control" placeholder="Subtitle"
|
||||
value="{{ model.subtitle }}">
|
||||
<label for="subtitle">Subtitle</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 col-xl-4 offset-xl-1 pb-3">
|
||||
<div class="form-floating">
|
||||
<select name="ThemeId" id="themeId" class="form-control" required>
|
||||
{% for theme in themes -%}
|
||||
<option value="{{ theme[0] }}"{% if model.theme_id == theme[0] %} selected="selected"{% endif %}>
|
||||
{{ theme[1] }}
|
||||
</option>
|
||||
{%- endfor %}
|
||||
</select>
|
||||
<label for="themeId">Theme</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 offset-md-1 col-xl-4 offset-xl-0 pb-3">
|
||||
<div class="form-floating">
|
||||
<select name="DefaultPage" id="defaultPage" class="form-control" required>
|
||||
{% for pg in pages -%}
|
||||
<option value="{{ pg[0] }}"
|
||||
{%- if pg[0] == model.default_page %} selected="selected"{% endif %}>
|
||||
{{ pg[1] }}
|
||||
</option>
|
||||
{%- endfor %}
|
||||
</select>
|
||||
<label for="defaultPage">Default Page</label>
|
||||
</div>
|
||||
</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 }}">
|
||||
<label for="postsPerPage">Posts per Page</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-4 col-xl-3 offset-xl-2 pb-3">
|
||||
<div class="form-floating">
|
||||
<input type="text" name="TimeZone" id="timeZone" class="form-control" placeholder="Time Zone" required
|
||||
value="{{ model.time_zone }}">
|
||||
<label for="timeZone">Time Zone</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 col-xl-2">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" name="AutoHtmx" id="autoHtmx" class="form-check-input" value="true"
|
||||
{%- if model.auto_htmx %} checked="checked"{% endif %}>
|
||||
<label for="autoHtmx" class="form-check-label">Auto-Load htmx</label>
|
||||
</div>
|
||||
<span class="form-text fst-italic">
|
||||
<a href="https://htmx.org" target="_blank" rel="noopener">What is this?</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 col-xl-3 pb-3">
|
||||
<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>
|
||||
{%- endfor %}
|
||||
</select>
|
||||
<label for="uploads">Default Upload Destination</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row pb-3">
|
||||
<div class="col text-center">
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</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>
|
||||
|
@ -1,33 +1,45 @@
|
||||
<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>
|
||||
{%- assign map_url = "admin/settings/tag-mapping/" | append: map_id -%}
|
||||
<a href="{{ map_url | append: "/edit" | relative_link }}" hx-target="#tag_{{ map_id }}"
|
||||
hx-swap="innerHTML show:#tag_{{ map_id }}:top">
|
||||
Edit
|
||||
</a>
|
||||
<span class="text-muted"> • </span>
|
||||
{%- assign map_del_link = map_url | append: "/delete" | relative_link -%}
|
||||
<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 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>
|
||||
<div class="col">{{ map.url_value }}</div>
|
||||
</div>
|
||||
{%- endfor %}
|
||||
{%- |