Reassign child cats when deleting parent cat (#27)
- Create common page/post edit field template (#25) - Fix relative URL adjustment throughout - Fix upload name sanitization regex - Create modules within Admin handler module - Enable/disable podcast episode fields on page load - Fix upload destination casing in templates - Tweak default theme to show no posts found on index template - Update Bootstrap to 5.1.3 in default theme
This commit is contained in:
parent
6b49793fbb
commit
33698bd182
|
@ -5,6 +5,16 @@ open System.Threading.Tasks
|
|||
open MyWebLog
|
||||
open MyWebLog.ViewModels
|
||||
|
||||
/// The result of a category deletion attempt
|
||||
type CategoryDeleteResult =
|
||||
/// The category was deleted successfully
|
||||
| CategoryDeleted
|
||||
/// The category was deleted successfully, and its children were reassigned to its parent
|
||||
| ReassignedChildCategories
|
||||
/// The category was not found, so no effort was made to delete it
|
||||
| CategoryNotFound
|
||||
|
||||
|
||||
/// Data functions to support manipulating categories
|
||||
type ICategoryData =
|
||||
|
||||
|
@ -18,7 +28,7 @@ type ICategoryData =
|
|||
abstract member CountTopLevel : WebLogId -> Task<int>
|
||||
|
||||
/// Delete a category (also removes it from posts)
|
||||
abstract member Delete : CategoryId -> WebLogId -> Task<bool>
|
||||
abstract member Delete : CategoryId -> WebLogId -> Task<CategoryDeleteResult>
|
||||
|
||||
/// Find all categories for a web log, sorted alphabetically and grouped by hierarchy
|
||||
abstract member FindAllForView : WebLogId -> Task<DisplayCategory[]>
|
||||
|
|
|
@ -274,7 +274,21 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
|
|||
|
||||
member this.Delete catId webLogId = backgroundTask {
|
||||
match! this.FindById catId webLogId with
|
||||
| Some _ ->
|
||||
| Some cat ->
|
||||
// Reassign any children to the category's parent category
|
||||
let! children = rethink<int> {
|
||||
withTable Table.Category
|
||||
filter (nameof Category.empty.ParentId) catId
|
||||
count
|
||||
result; withRetryDefault conn
|
||||
}
|
||||
if children > 0 then
|
||||
do! rethink {
|
||||
withTable Table.Category
|
||||
filter (nameof Category.empty.ParentId) catId
|
||||
update [ nameof Category.empty.ParentId, cat.ParentId :> obj ]
|
||||
write; withRetryDefault; ignoreResult conn
|
||||
}
|
||||
// Delete the category off all posts where it is assigned
|
||||
do! rethink {
|
||||
withTable Table.Post
|
||||
|
@ -291,8 +305,8 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
|
|||
delete
|
||||
write; withRetryDefault; ignoreResult conn
|
||||
}
|
||||
return true
|
||||
| None -> return false
|
||||
return if children = 0 then CategoryDeleted else ReassignedChildCategories
|
||||
| None -> return CategoryNotFound
|
||||
}
|
||||
|
||||
member _.Restore cats = backgroundTask {
|
||||
|
|
|
@ -122,13 +122,23 @@ type SQLiteCategoryData (conn : SqliteConnection) =
|
|||
/// Delete a category
|
||||
let delete catId webLogId = backgroundTask {
|
||||
match! findById catId webLogId with
|
||||
| Some _ ->
|
||||
| Some cat ->
|
||||
use cmd = conn.CreateCommand ()
|
||||
// Reassign any children to the category's parent category
|
||||
cmd.CommandText <- "SELECT COUNT(id) FROM category WHERE parent_id = @parentId"
|
||||
cmd.Parameters.AddWithValue ("@parentId", CategoryId.toString catId) |> ignore
|
||||
let! children = count cmd
|
||||
if children > 0 then
|
||||
cmd.CommandText <- "UPDATE category SET parent_id = @newParentId WHERE parent_id = @parentId"
|
||||
cmd.Parameters.AddWithValue ("@newParentId", maybe (cat.ParentId |> Option.map CategoryId.toString))
|
||||
|> ignore
|
||||
do! write cmd
|
||||
// Delete the category off all posts where it is assigned
|
||||
cmd.CommandText <- """
|
||||
DELETE FROM post_category
|
||||
WHERE category_id = @id
|
||||
AND post_id IN (SELECT id FROM post WHERE web_log_id = @webLogId)"""
|
||||
cmd.Parameters.Clear ()
|
||||
let catIdParameter = cmd.Parameters.AddWithValue ("@id", CategoryId.toString catId)
|
||||
cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString webLogId) |> ignore
|
||||
do! write cmd
|
||||
|
@ -137,8 +147,8 @@ type SQLiteCategoryData (conn : SqliteConnection) =
|
|||
cmd.Parameters.Clear ()
|
||||
cmd.Parameters.Add catIdParameter |> ignore
|
||||
do! write cmd
|
||||
return true
|
||||
| None -> return false
|
||||
return if children = 0 then CategoryDeleted else ReassignedChildCategories
|
||||
| None -> return CategoryNotFound
|
||||
}
|
||||
|
||||
/// Restore categories from a backup
|
||||
|
|
|
@ -12,6 +12,17 @@ module private Helpers =
|
|||
match (defaultArg (Option.ofObj it) "").Trim () with "" -> None | trimmed -> Some trimmed
|
||||
|
||||
|
||||
/// Helper functions that are needed outside this file
|
||||
[<AutoOpen>]
|
||||
module PublicHelpers =
|
||||
|
||||
/// If the web log is not being served from the domain root, add the path information to relative URLs in page and
|
||||
/// post text
|
||||
let addBaseToRelativeUrls extra (text : string) =
|
||||
if extra = "" then text
|
||||
else text.Replace("href=\"/", $"href=\"{extra}/").Replace ("src=\"/", $"src=\"{extra}/")
|
||||
|
||||
|
||||
/// The model used to display the admin dashboard
|
||||
[<NoComparison; NoEquality>]
|
||||
type DashboardModel =
|
||||
|
@ -147,7 +158,7 @@ type DisplayPage =
|
|||
UpdatedOn = page.UpdatedOn
|
||||
IsInPageList = page.IsInPageList
|
||||
IsDefault = pageId = webLog.DefaultPage
|
||||
Text = if extra = "" then page.Text else page.Text.Replace ("href=\"/", $"href=\"{extra}/")
|
||||
Text = addBaseToRelativeUrls extra page.Text
|
||||
Metadata = page.Metadata
|
||||
}
|
||||
|
||||
|
@ -1061,7 +1072,7 @@ type PostListItem =
|
|||
Permalink = Permalink.toString post.Permalink
|
||||
PublishedOn = post.PublishedOn |> Option.map inTZ |> Option.toNullable
|
||||
UpdatedOn = inTZ post.UpdatedOn
|
||||
Text = if extra = "" then post.Text else post.Text.Replace ("href=\"/", $"href=\"{extra}/")
|
||||
Text = addBaseToRelativeUrls extra post.Text
|
||||
CategoryIds = post.CategoryIds |> List.map CategoryId.toString
|
||||
Tags = post.Tags
|
||||
Episode = post.Episode
|
||||
|
|
|
@ -6,10 +6,11 @@ open Giraffe
|
|||
open MyWebLog
|
||||
open MyWebLog.ViewModels
|
||||
|
||||
// ~~ DASHBOARDS ~~
|
||||
/// ~~ DASHBOARDS ~~
|
||||
module Dashboard =
|
||||
|
||||
// GET /admin/dashboard
|
||||
let dashboard : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||
let user : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||
let getCount (f : WebLogId -> Task<int>) = f ctx.WebLog.Id
|
||||
let data = ctx.Data
|
||||
let posts = getCount (data.Post.CountByStatus Published)
|
||||
|
@ -33,7 +34,7 @@ let dashboard : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
|||
}
|
||||
|
||||
// GET /admin/administration
|
||||
let adminDashboard : HttpHandler = requireAccess Administrator >=> fun next ctx -> task {
|
||||
let admin : 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 ()
|
||||
|
@ -41,7 +42,10 @@ let adminDashboard : HttpHandler = requireAccess Administrator >=> fun next ctx
|
|||
let! hash =
|
||||
hashForPage "myWebLog Administration"
|
||||
|> withAntiCsrf ctx
|
||||
|> addToHash "themes" (themes |> List.map (DisplayTheme.fromTheme WebLogCache.isThemeInUse) |> Array.ofList)
|
||||
|> addToHash "themes" (
|
||||
themes
|
||||
|> List.map (DisplayTheme.fromTheme WebLogCache.isThemeInUse)
|
||||
|> Array.ofList)
|
||||
|> addToHash "cached_themes" (
|
||||
themes
|
||||
|> Seq.ofList
|
||||
|
@ -70,17 +74,20 @@ let adminDashboard : HttpHandler = requireAccess Administrator >=> fun next ctx
|
|||
/// Redirect the user to the admin dashboard
|
||||
let toAdminDashboard : HttpHandler = redirectToGet "admin/administration"
|
||||
|
||||
// ~~ CACHES ~~
|
||||
|
||||
/// ~~ CACHES ~~
|
||||
module Cache =
|
||||
|
||||
// POST /admin/cache/web-log/{id}/refresh
|
||||
let refreshWebLogCache webLogId : HttpHandler = requireAccess Administrator >=> fun next ctx -> task {
|
||||
let refreshWebLog webLogId : HttpHandler = requireAccess Administrator >=> fun next ctx -> task {
|
||||
let data = ctx.Data
|
||||
if webLogId = "all" then
|
||||
do! WebLogCache.fill data
|
||||
for webLog in WebLogCache.all () do
|
||||
do! PageListCache.refresh webLog data
|
||||
do! CategoryCache.refresh webLog.Id data
|
||||
do! addMessage ctx { UserMessage.success with Message = "Successfully refresh web log cache for all web logs" }
|
||||
do! addMessage ctx
|
||||
{ UserMessage.success with Message = "Successfully refresh web log cache for all web logs" }
|
||||
else
|
||||
match! data.WebLog.FindById (WebLogId webLogId) with
|
||||
| Some webLog ->
|
||||
|
@ -95,7 +102,7 @@ let refreshWebLogCache webLogId : HttpHandler = requireAccess Administrator >=>
|
|||
}
|
||||
|
||||
// POST /admin/cache/theme/{id}/refresh
|
||||
let refreshThemeCache themeId : HttpHandler = requireAccess Administrator >=> fun next ctx -> task {
|
||||
let refreshTheme themeId : HttpHandler = requireAccess Administrator >=> fun next ctx -> task {
|
||||
let data = ctx.Data
|
||||
if themeId = "all" then
|
||||
TemplateCache.empty ()
|
||||
|
@ -118,10 +125,14 @@ let refreshThemeCache themeId : HttpHandler = requireAccess Administrator >=> fu
|
|||
return! toAdminDashboard next ctx
|
||||
}
|
||||
|
||||
// ~~ CATEGORIES ~~
|
||||
|
||||
/// ~~ CATEGORIES ~~
|
||||
module Category =
|
||||
|
||||
open MyWebLog.Data
|
||||
|
||||
// GET /admin/categories
|
||||
let listCategories : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||
let all : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||
match! TemplateCache.get adminTheme "category-list-body" ctx.Data with
|
||||
| Ok catListTemplate ->
|
||||
let! hash =
|
||||
|
@ -135,14 +146,14 @@ let listCategories : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx ->
|
|||
}
|
||||
|
||||
// GET /admin/categories/bare
|
||||
let listCategoriesBare : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx ->
|
||||
let bare : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx ->
|
||||
hashForPage "Categories"
|
||||
|> withAntiCsrf ctx
|
||||
|> adminBareView "category-list-body" next ctx
|
||||
|
||||
|
||||
// GET /admin/category/{id}/edit
|
||||
let editCategory catId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||
let edit catId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||
let! result = task {
|
||||
match catId with
|
||||
| "new" -> return Some ("Add a New Category", { Category.empty with Id = CategoryId "new" })
|
||||
|
@ -162,7 +173,7 @@ let editCategory catId : HttpHandler = requireAccess WebLogAdmin >=> fun next ct
|
|||
}
|
||||
|
||||
// POST /admin/category/save
|
||||
let saveCategory : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||
let save : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||
let data = ctx.Data
|
||||
let! model = ctx.BindFormAsync<EditCategoryModel> ()
|
||||
let category =
|
||||
|
@ -180,34 +191,46 @@ let saveCategory : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> t
|
|||
do! (if model.IsNew then data.Category.Add else data.Category.Update) updatedCat
|
||||
do! CategoryCache.update ctx
|
||||
do! addMessage ctx { UserMessage.success with Message = "Category saved successfully" }
|
||||
return! listCategoriesBare next ctx
|
||||
return! bare next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
// POST /admin/category/{id}/delete
|
||||
let deleteCategory catId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||
match! ctx.Data.Category.Delete (CategoryId catId) ctx.WebLog.Id with
|
||||
| true ->
|
||||
let delete catId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||
let! result = ctx.Data.Category.Delete (CategoryId catId) ctx.WebLog.Id
|
||||
match result with
|
||||
| CategoryDeleted
|
||||
| ReassignedChildCategories ->
|
||||
do! CategoryCache.update ctx
|
||||
do! addMessage ctx { UserMessage.success with Message = "Category deleted successfully" }
|
||||
| false -> do! addMessage ctx { UserMessage.error with Message = "Category not found; cannot delete" }
|
||||
return! listCategoriesBare next ctx
|
||||
let detail =
|
||||
match result with
|
||||
| ReassignedChildCategories ->
|
||||
Some "<em>(Its child categories were reassigned to its parent category)</em>"
|
||||
| _ -> None
|
||||
do! addMessage ctx { UserMessage.success with Message = "Category deleted successfully"; Detail = detail }
|
||||
| CategoryNotFound ->
|
||||
do! addMessage ctx { UserMessage.error with Message = "Category not found; cannot delete" }
|
||||
return! bare next ctx
|
||||
}
|
||||
|
||||
|
||||
/// ~~ TAG MAPPINGS ~~
|
||||
module TagMapping =
|
||||
|
||||
open Microsoft.AspNetCore.Http
|
||||
|
||||
// ~~ TAG MAPPINGS ~~
|
||||
|
||||
/// Add tag mappings to the given hash
|
||||
let private withTagMappings (ctx : HttpContext) hash = task {
|
||||
let withTagMappings (ctx : HttpContext) hash = task {
|
||||
let! mappings = ctx.Data.TagMap.FindByWebLog ctx.WebLog.Id
|
||||
return
|
||||
addToHash "mappings" mappings hash
|
||||
|> addToHash "mapping_ids" (mappings |> List.map (fun it -> { Name = it.Tag; Value = TagMapId.toString it.Id }))
|
||||
|> addToHash "mapping_ids" (
|
||||
mappings
|
||||
|> List.map (fun it -> { Name = it.Tag; Value = TagMapId.toString it.Id }))
|
||||
}
|
||||
|
||||
// GET /admin/settings/tag-mappings
|
||||
let tagMappings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||
let all : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||
let! hash =
|
||||
hashForPage ""
|
||||
|> withAntiCsrf ctx
|
||||
|
@ -216,7 +239,7 @@ let tagMappings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> ta
|
|||
}
|
||||
|
||||
// GET /admin/settings/tag-mapping/{id}/edit
|
||||
let editMapping tagMapId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||
let edit tagMapId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||
let isNew = tagMapId = "new"
|
||||
let tagMap =
|
||||
if isNew then someTask { TagMap.empty with Id = TagMapId "new" }
|
||||
|
@ -232,7 +255,7 @@ let editMapping tagMapId : HttpHandler = requireAccess WebLogAdmin >=> fun next
|
|||
}
|
||||
|
||||
// POST /admin/settings/tag-mapping/save
|
||||
let saveMapping : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||
let save : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||
let data = ctx.Data
|
||||
let! model = ctx.BindFormAsync<EditTagMapModel> ()
|
||||
let tagMap =
|
||||
|
@ -242,19 +265,21 @@ let saveMapping : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> ta
|
|||
| Some tm ->
|
||||
do! data.TagMap.Save { tm with Tag = model.Tag.ToLower (); UrlValue = model.UrlValue.ToLower () }
|
||||
do! addMessage ctx { UserMessage.success with Message = "Tag mapping saved successfully" }
|
||||
return! tagMappings next ctx
|
||||
return! all next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
// POST /admin/settings/tag-mapping/{id}/delete
|
||||
let deleteMapping tagMapId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||
let delete tagMapId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||
match! ctx.Data.TagMap.Delete (TagMapId tagMapId) ctx.WebLog.Id with
|
||||
| true -> do! addMessage ctx { UserMessage.success with Message = "Tag mapping deleted successfully" }
|
||||
| false -> do! addMessage ctx { UserMessage.error with Message = "Tag mapping not found; nothing deleted" }
|
||||
return! tagMappings next ctx
|
||||
return! all next ctx
|
||||
}
|
||||
|
||||
// ~~ THEMES ~~
|
||||
|
||||
/// ~~ THEMES ~~
|
||||
module Theme =
|
||||
|
||||
open System
|
||||
open System.IO
|
||||
|
@ -263,7 +288,7 @@ open System.Text.RegularExpressions
|
|||
open MyWebLog.Data
|
||||
|
||||
// GET /admin/theme/list
|
||||
let listThemes : HttpHandler = requireAccess Administrator >=> fun next ctx -> task {
|
||||
let all : HttpHandler = requireAccess Administrator >=> fun next ctx -> task {
|
||||
let! themes = ctx.Data.Theme.All ()
|
||||
return!
|
||||
hashForPage "Themes"
|
||||
|
@ -273,7 +298,7 @@ let listThemes : HttpHandler = requireAccess Administrator >=> fun next ctx -> t
|
|||
}
|
||||
|
||||
// GET /admin/theme/new
|
||||
let addTheme : HttpHandler = requireAccess Administrator >=> fun next ctx ->
|
||||
let add : HttpHandler = requireAccess Administrator >=> fun next ctx ->
|
||||
hashForPage "Upload a Theme File"
|
||||
|> withAntiCsrf ctx
|
||||
|> adminBareView "theme-upload" next ctx
|
||||
|
@ -324,8 +349,8 @@ let private updateAssets themeId (zip : ZipArchive) (data : IData) = backgroundT
|
|||
}
|
||||
}
|
||||
|
||||
/// Get the theme name from the file name given
|
||||
let getThemeIdFromFileName (fileName : string) =
|
||||
/// Derive the theme ID from the file name given
|
||||
let deriveIdFromFileName (fileName : string) =
|
||||
let themeName = fileName.Split(".").[0].ToLowerInvariant().Replace (" ", "-")
|
||||
if themeName.EndsWith "-theme" then
|
||||
if Regex.IsMatch (themeName, """^[a-z0-9\-]+$""") then
|
||||
|
@ -334,7 +359,7 @@ let getThemeIdFromFileName (fileName : string) =
|
|||
else Error "Theme .zip file name must end in \"-theme.zip\""
|
||||
|
||||
/// Load a theme from the given stream, which should contain a ZIP archive
|
||||
let loadThemeFromZip themeId file (data : IData) = backgroundTask {
|
||||
let loadFromZip themeId file (data : IData) = backgroundTask {
|
||||
let! isNew, theme = backgroundTask {
|
||||
match! data.Theme.FindById themeId with
|
||||
| Some t -> return false, t
|
||||
|
@ -351,10 +376,10 @@ let loadThemeFromZip themeId file (data : IData) = backgroundTask {
|
|||
}
|
||||
|
||||
// POST /admin/theme/new
|
||||
let saveTheme : HttpHandler = requireAccess Administrator >=> fun next ctx -> task {
|
||||
let save : HttpHandler = requireAccess Administrator >=> fun next ctx -> task {
|
||||
if ctx.Request.HasFormContentType && ctx.Request.Form.Files.Count > 0 then
|
||||
let themeFile = Seq.head ctx.Request.Form.Files
|
||||
match getThemeIdFromFileName themeFile.FileName with
|
||||
match deriveIdFromFileName themeFile.FileName with
|
||||
| Ok themeId when themeId <> adminTheme ->
|
||||
let data = ctx.Data
|
||||
let! exists = data.Theme.Exists themeId
|
||||
|
@ -364,7 +389,7 @@ let saveTheme : HttpHandler = requireAccess Administrator >=> fun next ctx -> ta
|
|||
// Load the theme to the database
|
||||
use stream = new MemoryStream ()
|
||||
do! themeFile.CopyToAsync stream
|
||||
let! _ = loadThemeFromZip themeId stream data
|
||||
let! _ = loadFromZip themeId stream data
|
||||
do! ThemeAssetCache.refreshTheme themeId data
|
||||
TemplateCache.invalidateTheme themeId
|
||||
// Save the .zip file
|
||||
|
@ -391,31 +416,34 @@ let saveTheme : HttpHandler = requireAccess Administrator >=> fun next ctx -> ta
|
|||
}
|
||||
|
||||
// POST /admin/theme/{id}/delete
|
||||
let deleteTheme themeId : HttpHandler = requireAccess Administrator >=> fun next ctx -> task {
|
||||
let delete themeId : HttpHandler = requireAccess Administrator >=> fun next ctx -> task {
|
||||
let data = ctx.Data
|
||||
match themeId with
|
||||
| "admin" | "default" ->
|
||||
do! addMessage ctx { UserMessage.error with Message = $"You may not delete the {themeId} theme" }
|
||||
return! listThemes next ctx
|
||||
return! all next ctx
|
||||
| it when WebLogCache.isThemeInUse (ThemeId it) ->
|
||||
do! addMessage ctx
|
||||
{ UserMessage.error with
|
||||
Message = $"You may not delete the {themeId} theme, as it is currently in use"
|
||||
}
|
||||
return! listThemes next ctx
|
||||
return! all next ctx
|
||||
| _ ->
|
||||
match! data.Theme.Delete (ThemeId themeId) with
|
||||
| true ->
|
||||
let zippedTheme = $"{themeId}-theme.zip"
|
||||
if File.Exists zippedTheme then File.Delete zippedTheme
|
||||
do! addMessage ctx { UserMessage.success with Message = $"Theme ID {themeId} deleted successfully" }
|
||||
return! listThemes next ctx
|
||||
return! all next ctx
|
||||
| false -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
// ~~ WEB LOG SETTINGS ~~
|
||||
|
||||
/// ~~ WEB LOG SETTINGS ~~
|
||||
module WebLog =
|
||||
|
||||
open System.Collections.Generic
|
||||
open System.IO
|
||||
|
||||
// GET /admin/settings
|
||||
let settings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||
|
@ -442,7 +470,8 @@ let settings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task
|
|||
|> addToHash "themes" (
|
||||
themes
|
||||
|> Seq.ofList
|
||||
|> Seq.map (fun it -> KeyValuePair.Create (ThemeId.toString it.Id, $"{it.Name} (v{it.Version})"))
|
||||
|> 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")
|
||||
|
@ -455,7 +484,7 @@ let settings : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task
|
|||
|> List.map (DisplayCustomFeed.fromFeed (CategoryCache.get ctx))
|
||||
|> Array.ofList)
|
||||
|> addViewContext ctx
|
||||
let! hash' = withTagMappings ctx hash
|
||||
let! hash' = TagMapping.withTagMappings ctx hash
|
||||
return!
|
||||
addToHash "user_list" (userTemplate.Render hash') hash'
|
||||
|> addToHash "tag_mapping_list" (tagMapTemplate.Render hash')
|
||||
|
|
|
@ -124,8 +124,14 @@ let private findPageRevision pgId revDate (ctx : HttpContext) = task {
|
|||
let previewRevision (pgId, revDate) : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||
match! findPageRevision pgId revDate ctx with
|
||||
| Some pg, Some rev when canEdit pg.AuthorId ctx ->
|
||||
let _, extra = WebLog.hostAndPath ctx.WebLog
|
||||
return! {|
|
||||
content = $"""<div class="mwl-revision-preview mb-3">{MarkupText.toHtml rev.Text}</div>"""
|
||||
content =
|
||||
[ """<div class="mwl-revision-preview mb-3">"""
|
||||
(MarkupText.toHtml >> addBaseToRelativeUrls extra) rev.Text
|
||||
"</div>"
|
||||
]
|
||||
|> String.concat ""
|
||||
|}
|
||||
|> makeHash |> adminBareView "" next ctx
|
||||
| Some _, Some _ -> return! Error.notAuthorized next ctx
|
||||
|
|
|
@ -329,8 +329,14 @@ let private findPostRevision postId revDate (ctx : HttpContext) = task {
|
|||
let previewRevision (postId, revDate) : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||
match! findPostRevision postId revDate ctx with
|
||||
| Some post, Some rev when canEdit post.AuthorId ctx ->
|
||||
let _, extra = WebLog.hostAndPath ctx.WebLog
|
||||
return! {|
|
||||
content = $"""<div class="mwl-revision-preview mb-3">{MarkupText.toHtml rev.Text}</div>"""
|
||||
content =
|
||||
[ """<div class="mwl-revision-preview mb-3">"""
|
||||
(MarkupText.toHtml >> addBaseToRelativeUrls extra) rev.Text
|
||||
"</div>"
|
||||
]
|
||||
|> String.concat ""
|
||||
|}
|
||||
|> makeHash |> adminBareView "" next ctx
|
||||
| Some _, Some _ -> return! Error.notAuthorized next ctx
|
||||
|
|
|
@ -106,13 +106,13 @@ let router : HttpHandler = choose [
|
|||
]
|
||||
subRoute "/admin" (requireUser >=> choose [
|
||||
GET_HEAD >=> choose [
|
||||
route "/administration" >=> Admin.adminDashboard
|
||||
route "/administration" >=> Admin.Dashboard.admin
|
||||
subRoute "/categor" (choose [
|
||||
route "ies" >=> Admin.listCategories
|
||||
route "ies/bare" >=> Admin.listCategoriesBare
|
||||
routef "y/%s/edit" Admin.editCategory
|
||||
route "ies" >=> Admin.Category.all
|
||||
route "ies/bare" >=> Admin.Category.bare
|
||||
routef "y/%s/edit" Admin.Category.edit
|
||||
])
|
||||
route "/dashboard" >=> Admin.dashboard
|
||||
route "/dashboard" >=> Admin.Dashboard.user
|
||||
route "/my-info" >=> User.myInfo
|
||||
subRoute "/page" (choose [
|
||||
route "s" >=> Page.all 1
|
||||
|
@ -131,20 +131,20 @@ let router : HttpHandler = choose [
|
|||
routef "/%s/revisions" Post.editRevisions
|
||||
])
|
||||
subRoute "/settings" (choose [
|
||||
route "" >=> Admin.settings
|
||||
route "" >=> Admin.WebLog.settings
|
||||
routef "/rss/%s/edit" Feed.editCustomFeed
|
||||
subRoute "/user" (choose [
|
||||
route "s" >=> User.all
|
||||
routef "/%s/edit" User.edit
|
||||
])
|
||||
subRoute "/tag-mapping" (choose [
|
||||
route "s" >=> Admin.tagMappings
|
||||
routef "/%s/edit" Admin.editMapping
|
||||
route "s" >=> Admin.TagMapping.all
|
||||
routef "/%s/edit" Admin.TagMapping.edit
|
||||
])
|
||||
])
|
||||
subRoute "/theme" (choose [
|
||||
route "/list" >=> Admin.listThemes
|
||||
route "/new" >=> Admin.addTheme
|
||||
route "/list" >=> Admin.Theme.all
|
||||
route "/new" >=> Admin.Theme.add
|
||||
])
|
||||
subRoute "/upload" (choose [
|
||||
route "s" >=> Upload.list
|
||||
|
@ -153,12 +153,12 @@ let router : HttpHandler = choose [
|
|||
]
|
||||
POST >=> validateCsrf >=> choose [
|
||||
subRoute "/cache" (choose [
|
||||
routef "/theme/%s/refresh" Admin.refreshThemeCache
|
||||
routef "/web-log/%s/refresh" Admin.refreshWebLogCache
|
||||
routef "/theme/%s/refresh" Admin.Cache.refreshTheme
|
||||
routef "/web-log/%s/refresh" Admin.Cache.refreshWebLog
|
||||
])
|
||||
subRoute "/category" (choose [
|
||||
route "/save" >=> Admin.saveCategory
|
||||
routef "/%s/delete" Admin.deleteCategory
|
||||
route "/save" >=> Admin.Category.save
|
||||
routef "/%s/delete" Admin.Category.delete
|
||||
])
|
||||
route "/my-info" >=> User.saveMyInfo
|
||||
subRoute "/page" (choose [
|
||||
|
@ -178,15 +178,15 @@ let router : HttpHandler = choose [
|
|||
routef "/%s/revisions/purge" Post.purgeRevisions
|
||||
])
|
||||
subRoute "/settings" (choose [
|
||||
route "" >=> Admin.saveSettings
|
||||
route "" >=> Admin.WebLog.saveSettings
|
||||
subRoute "/rss" (choose [
|
||||
route "" >=> Feed.saveSettings
|
||||
route "/save" >=> Feed.saveCustomFeed
|
||||
routef "/%s/delete" Feed.deleteCustomFeed
|
||||
])
|
||||
subRoute "/tag-mapping" (choose [
|
||||
route "/save" >=> Admin.saveMapping
|
||||
routef "/%s/delete" Admin.deleteMapping
|
||||
route "/save" >=> Admin.TagMapping.save
|
||||
routef "/%s/delete" Admin.TagMapping.delete
|
||||
])
|
||||
subRoute "/user" (choose [
|
||||
route "/save" >=> User.save
|
||||
|
@ -194,8 +194,8 @@ let router : HttpHandler = choose [
|
|||
])
|
||||
])
|
||||
subRoute "/theme" (choose [
|
||||
route "/new" >=> Admin.saveTheme
|
||||
routef "/%s/delete" Admin.deleteTheme
|
||||
route "/new" >=> Admin.Theme.save
|
||||
routef "/%s/delete" Admin.Theme.delete
|
||||
])
|
||||
subRoute "/upload" (choose [
|
||||
route "/save" >=> Upload.save
|
||||
|
|
|
@ -85,7 +85,7 @@ open System.Text.RegularExpressions
|
|||
open MyWebLog.ViewModels
|
||||
|
||||
/// Turn a string into a lowercase URL-safe slug
|
||||
let makeSlug it = ((Regex """\s+""").Replace ((Regex "[^A-z0-9 ]").Replace (it, ""), "-")).ToLowerInvariant ()
|
||||
let makeSlug it = ((Regex """\s+""").Replace ((Regex "[^A-z0-9 -]").Replace (it, ""), "-")).ToLowerInvariant ()
|
||||
|
||||
// GET /admin/uploads
|
||||
let list : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||
|
|
|
@ -51,7 +51,10 @@ let doLogOn : HttpHandler = fun next ctx -> task {
|
|||
AuthenticationProperties (IssuedUtc = DateTimeOffset.UtcNow))
|
||||
do! data.WebLogUser.SetLastSeen user.Id user.WebLogId
|
||||
do! addMessage ctx
|
||||
{ UserMessage.success with Message = $"Logged on successfully | Welcome to {ctx.WebLog.Name}!" }
|
||||
{ UserMessage.success with
|
||||
Message = "Log on successful"
|
||||
Detail = Some $"Welcome to {ctx.WebLog.Name}!"
|
||||
}
|
||||
return!
|
||||
match model.ReturnTo with
|
||||
| Some url -> redirectTo false url next ctx
|
||||
|
|
|
@ -137,13 +137,13 @@ let loadTheme (args : string[]) (sp : IServiceProvider) = task {
|
|||
match args[1].LastIndexOf Path.DirectorySeparatorChar with
|
||||
| -1 -> args[1]
|
||||
| it -> args[1][(it + 1)..]
|
||||
match Handlers.Admin.getThemeIdFromFileName fileName with
|
||||
match Handlers.Admin.Theme.deriveIdFromFileName fileName with
|
||||
| Ok themeId ->
|
||||
let data = sp.GetRequiredService<IData> ()
|
||||
use stream = File.Open (args[1], FileMode.Open)
|
||||
use copy = new MemoryStream ()
|
||||
do! stream.CopyToAsync copy
|
||||
let! theme = Handlers.Admin.loadThemeFromZip themeId copy data
|
||||
let! theme = Handlers.Admin.Theme.loadFromZip themeId copy data
|
||||
let fac = sp.GetRequiredService<ILoggerFactory> ()
|
||||
let log = fac.CreateLogger "MyWebLog.Themes"
|
||||
log.LogInformation $"{theme.Name} v{theme.Version} ({ThemeId.toString theme.Id}) loaded"
|
||||
|
|
32
src/admin-theme/_edit-common.liquid
Normal file
32
src/admin-theme/_edit-common.liquid
Normal file
|
@ -0,0 +1,32 @@
|
|||
<div class="form-floating pb-3">
|
||||
<input type="text" name="Title" id="title" class="form-control" placeholder="Title" autofocus required
|
||||
value="{{ model.title }}">
|
||||
<label for="title">Title</label>
|
||||
</div>
|
||||
<div class="form-floating pb-3">
|
||||
<input type="text" name="Permalink" id="permalink" class="form-control" placeholder="Permalink" required
|
||||
value="{{ model.permalink }}">
|
||||
<label for="permalink">Permalink</label>
|
||||
{%- unless model.is_new %}
|
||||
{%- assign entity_url_base = "admin/" | append: entity | append: "/" | append: entity_id -%}
|
||||
<span class="form-text">
|
||||
<a href="{{ entity_url_base | append: "/permalinks" | relative_link }}">Manage Permalinks</a>
|
||||
<span class="text-muted"> • </span>
|
||||
<a href="{{ entity_url_base | append: "/revisions" | relative_link }}">Manage Revisions</a>
|
||||
</span>
|
||||
{%- endunless -%}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="text">Text</label>
|
||||
<div class="btn-group btn-group-sm" role="group" aria-label="Text format button group">
|
||||
<input type="radio" name="Source" id="source_html" class="btn-check" value="HTML"
|
||||
{%- if model.source == "HTML" %} checked="checked"{% endif %}>
|
||||
<label class="btn btn-sm btn-outline-secondary" for="source_html">HTML</label>
|
||||
<input type="radio" name="Source" id="source_md" class="btn-check" value="Markdown"
|
||||
{%- if model.source == "Markdown" %} checked="checked"{% endif %}>
|
||||
<label class="btn btn-sm btn-outline-secondary" for="source_md">Markdown</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pb-3">
|
||||
<textarea name="Text" id="text" class="form-control" rows="20">{{ model.text }}</textarea>
|
||||
</div>
|
|
@ -2,8 +2,6 @@
|
|||
<article>
|
||||
<fieldset class="container mb-3 pb-0">
|
||||
<legend>Themes</legend>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<a href="{{ "admin/theme/new" | relative_link }}" class="btn btn-primary btn-sm mb-3"
|
||||
hx-target="#theme_new">
|
||||
Upload a New Theme
|
||||
|
@ -18,8 +16,6 @@
|
|||
</div>
|
||||
<div class="row mwl-table-detail" id="theme_new"></div>
|
||||
{{ theme_list }}
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset class="container mb-3 pb-0">
|
||||
{%- assign cache_base_url = "admin/cache/" -%}
|
||||
|
@ -27,10 +23,9 @@
|
|||
<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
|
||||
admin area will keep these up to date; however, if changes occur outside of the system (creating a new web log
|
||||
via CLI, loading an updated theme via CLI, direct data updates, etc.), these options allow for the caches to
|
||||
be refreshed without requiring you to restart the application.
|
||||
myWebLog uses a few caches to ensure that it serves pages as fast as possible.
|
||||
(<a href="https://bitbadger.solutions/open-source/myweblog/advanced.html#cache-management"
|
||||
target="_blank">more information</a>)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -7,21 +7,21 @@
|
|||
<div class="row">
|
||||
<div class="col-12 col-sm-6 col-lg-4 col-xxl-3 offset-xxl-1 mb-3">
|
||||
<div class="form-floating">
|
||||
<input type="text" name="Name" id="name" class="form-control form-control-sm" placeholder="Name" autofocus
|
||||
required value="{{ model.name | escape }}">
|
||||
<input type="text" name="Name" id="name" class="form-control" placeholder="Name" autofocus required
|
||||
value="{{ model.name | escape }}">
|
||||
<label for="name">Name</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-lg-4 col-xxl-3 mb-3">
|
||||
<div class="form-floating">
|
||||
<input type="text" name="Slug" id="slug" class="form-control form-control-sm" placeholder="Slug" required
|
||||
<input type="text" name="Slug" id="slug" class="form-control" placeholder="Slug" required
|
||||
value="{{ model.slug | escape }}">
|
||||
<label for="slug">Slug</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-4 col-xxl-3 offset-xxl-1 mb-3">
|
||||
<div class="form-floating">
|
||||
<select name="ParentId" id="parentId" class="form-control form-control-sm">
|
||||
<select name="ParentId" id="parentId" class="form-control">
|
||||
<option value=""{% if model.parent_id == "" %} selected="selected"{% endif %}>
|
||||
– None –
|
||||
</option>
|
||||
|
@ -38,7 +38,7 @@
|
|||
</div>
|
||||
<div class="col-12 col-xl-10 offset-xl-1 mb-3">
|
||||
<div class="form-floating">
|
||||
<input name="Description" id="description" class="form-control form-control-sm"
|
||||
<input name="Description" id="description" class="form-control"
|
||||
placeholder="A short description of this category" value="{{ model.description | escape }}">
|
||||
<label for="description">Description</label>
|
||||
</div>
|
||||
|
|
|
@ -14,18 +14,6 @@
|
|||
integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
|
||||
crossorigin="anonymous"></script>
|
||||
{{ htmx_script }}
|
||||
<script>
|
||||
const cssLoaded = [...document.styleSheets].filter(it => it.href.indexOf("bootstrap.min.css") > -1).length > 0
|
||||
if (!cssLoaded) {
|
||||
const local = document.createElement("link")
|
||||
local.rel = "stylesheet"
|
||||
local.href = "{{ "themes/admin/bootstrap.min.css" | relative_link }}"
|
||||
document.getElementsByTagName("link")[0].prepend(local)
|
||||
}
|
||||
setTimeout(function () {
|
||||
if (!bootstrap) document.write('<script src=\"{{ "script/bootstrap.bundle.min.js" | relative_link }}\"><\/script>')
|
||||
}, 2000)
|
||||
</script>
|
||||
<script src="{{ "themes/admin/admin.js" | relative_link }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -6,39 +6,9 @@
|
|||
<div class="container">
|
||||
<div class="row mb-3">
|
||||
<div class="col-9">
|
||||
<div class="form-floating pb-3">
|
||||
<input type="text" name="Title" id="title" class="form-control" autofocus required
|
||||
value="{{ model.title }}">
|
||||
<label for="title">Title</label>
|
||||
</div>
|
||||
<div class="form-floating pb-3">
|
||||
<input type="text" name="Permalink" id="permalink" class="form-control" required
|
||||
value="{{ model.permalink }}">
|
||||
<label for="permalink">Permalink</label>
|
||||
{%- unless model.is_new %}
|
||||
<span class="form-text">
|
||||
<a href="{{ "admin/page/" | append: model.page_id | append: "/permalinks" | relative_link }}">
|
||||
Manage Permalinks
|
||||
</a>
|
||||
<span class="text-muted"> • </span>
|
||||
<a href="{{ "admin/page/" | append: model.page_id | append: "/revisions" | relative_link }}">
|
||||
Manage Revisions
|
||||
</a>
|
||||
</span>
|
||||
{% endunless -%}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="text">Text</label>
|
||||
<input type="radio" name="Source" id="source_html" class="btn-check" value="HTML"
|
||||
{%- if model.source == "HTML" %} checked="checked"{% endif %}>
|
||||
<label class="btn btn-sm btn-outline-secondary" for="source_html">HTML</label>
|
||||
<input type="radio" name="Source" id="source_md" class="btn-check" value="Markdown"
|
||||
{%- if model.source == "Markdown" %} checked="checked"{% endif %}>
|
||||
<label class="btn btn-sm btn-outline-secondary" for="source_md">Markdown</label>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<textarea name="Text" id="text" class="form-control">{{ model.text }}</textarea>
|
||||
</div>
|
||||
{%- assign entity = "page" -%}
|
||||
{%- assign entity_id = model.page_id -%}
|
||||
{% include_template "_edit-common" %}
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="form-floating pb-3">
|
||||
|
|
|
@ -6,41 +6,9 @@
|
|||
<div class="container">
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 col-lg-9">
|
||||
<div class="form-floating pb-3">
|
||||
<input type="text" name="Title" id="title" class="form-control" placeholder="Title" autofocus required
|
||||
value="{{ model.title }}">
|
||||
<label for="title">Title</label>
|
||||
</div>
|
||||
<div class="form-floating pb-3">
|
||||
<input type="text" name="Permalink" id="permalink" class="form-control" placeholder="Permalink" required
|
||||
value="{{ model.permalink }}">
|
||||
<label for="permalink">Permalink</label>
|
||||
{%- unless model.is_new %}
|
||||
<span class="form-text">
|
||||
<a href="{{ "admin/post/" | append: model.post_id | append: "/permalinks" | relative_link }}">
|
||||
Manage Permalinks
|
||||
</a>
|
||||
<span class="text-muted"> • </span>
|
||||
<a href="{{ "admin/post/" | append: model.post_id | append: "/revisions" | relative_link }}">
|
||||
Manage Revisions
|
||||
</a>
|
||||
</span>
|
||||
{% endunless -%}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="text">Text</label>
|
||||
<div class="btn-group btn-group-sm" role="group" aria-label="Text format button group">
|
||||
<input type="radio" name="Source" id="source_html" class="btn-check" value="HTML"
|
||||
{%- if model.source == "HTML" %} checked="checked"{% endif %}>
|
||||
<label class="btn btn-sm btn-outline-secondary" for="source_html">HTML</label>
|
||||
<input type="radio" name="Source" id="source_md" class="btn-check" value="Markdown"
|
||||
{%- if model.source == "Markdown" %} checked="checked"{% endif %}>
|
||||
<label class="btn btn-sm btn-outline-secondary" for="source_md">Markdown</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pb-3">
|
||||
<textarea name="Text" id="text" class="form-control" rows="20">{{ model.text }}</textarea>
|
||||
</div>
|
||||
{%- assign entity = "post" -%}
|
||||
{%- assign entity_id = model.post_id -%}
|
||||
{% include_template "_edit-common" %}
|
||||
<div class="form-floating pb-3">
|
||||
<input type="text" name="Tags" id="tags" class="form-control" placeholder="Tags"
|
||||
value="{{ model.tags }}">
|
||||
|
@ -344,3 +312,4 @@
|
|||
</div>
|
||||
</form>
|
||||
</article>
|
||||
<script>window.setTimeout(() => Admin.toggleEpisodeFields(), 500)</script>
|
||||
|
|
|
@ -6,8 +6,6 @@
|
|||
</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">
|
||||
|
@ -54,9 +52,8 @@
|
|||
<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 %}>
|
||||
{%- for pg in pages %}
|
||||
<option value="{{ pg[0] }}"{% if pg[0] == model.default_page %} selected="selected"{% endif %}>
|
||||
{{ pg[1] }}
|
||||
</option>
|
||||
{%- endfor %}
|
||||
|
@ -66,8 +63,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>
|
||||
|
@ -94,8 +91,7 @@
|
|||
<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>
|
||||
|
@ -109,13 +105,9 @@
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset id="users" 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">
|
||||
|
@ -130,13 +122,9 @@
|
|||
</div>
|
||||
</div>
|
||||
{{ user_list }}
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset id="rss-settings" class="container mb-3 pb-0">
|
||||
<legend>RSS Settings</legend>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<form action="{{ "admin/settings/rss" | relative_link }}" method="post">
|
||||
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
||||
<div class="container">
|
||||
|
@ -202,13 +190,11 @@
|
|||
</form>
|
||||
<fieldset class="container mb-3 pb-0">
|
||||
<legend>Custom Feeds</legend>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<a class="btn btn-sm btn-secondary" href="{{ 'admin/settings/rss/new/edit' | relative_link }}">
|
||||
Add a New Custom Feed
|
||||
</a>
|
||||
{%- assign feed_count = custom_feeds | size -%}
|
||||
{% if feed_count > 0 %}
|
||||
{%- 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" -%}
|
||||
|
@ -242,27 +228,19 @@
|
|||
<span class="d-none d-md-inline">{{ feed.path }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
</form>
|
||||
{% else %}
|
||||
{%- else %}
|
||||
<p class="text-muted fst-italic text-center">No custom feeds defined</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{%- endif %}
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset id="tag-mappings" class="container mb-3 pb-0">
|
||||
<legend>Tag Mappings</legend>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<a href="{{ "admin/settings/tag-mapping/new/edit" | relative_link }}" class="btn btn-primary btn-sm mb-3"
|
||||
hx-target="#tag_new">
|
||||
Add a New Tag Mapping
|
||||
</a>
|
||||
{{ tag_mapping_list }}
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</article>
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
<div class="row mwl-table-detail">
|
||||
<div class="col-6">
|
||||
{%- capture badge_class -%}
|
||||
{%- if file.source == "disk" %}secondary{% else %}primary{% endif -%}
|
||||
{%- if file.source == "Disk" %}secondary{% else %}primary{% endif -%}
|
||||
{%- endcapture -%}
|
||||
{%- assign path_and_name = file.path | append: file.name -%}
|
||||
{%- assign blog_rel = upload_path | append: path_and_name -%}
|
||||
|
@ -49,7 +49,7 @@
|
|||
{% if is_web_log_admin %}
|
||||
<span class="text-muted"> • </span>
|
||||
{%- capture delete_url -%}
|
||||
{%- if file.source == "disk" -%}
|
||||
{%- if file.source == "Disk" -%}
|
||||
admin/upload/delete/{{ path_and_name }}
|
||||
{%- else -%}
|
||||
admin/upload/{{ file.id }}/delete
|
||||
|
|
|
@ -13,11 +13,11 @@
|
|||
<div class="col-12 col-md-6 pb-3 d-flex align-self-center justify-content-around">
|
||||
Destination<br>
|
||||
<div class="btn-group" role="group" aria-label="Upload destination button group">
|
||||
<input type="radio" name="Destination" id="destination_db" class="btn-check" value="database"
|
||||
{%- if destination == "database" %} checked="checked"{% endif %}>
|
||||
<input type="radio" name="Destination" id="destination_db" class="btn-check" value="Database"
|
||||
{%- if destination == "Database" %} checked="checked"{% endif %}>
|
||||
<label class="btn btn-outline-primary" for="destination_db">Database</label>
|
||||
<input type="radio" name="Destination" id="destination_disk" class="btn-check" value="disk"
|
||||
{%- if destination == "disk" %} checked="checked"{% endif %}>
|
||||
<input type="radio" name="Destination" id="destination_disk" class="btn-check" value="Disk"
|
||||
{%- if destination == "Disk" %} checked="checked"{% endif %}>
|
||||
<label class="btn btn-outline-secondary" for="destination_disk">Disk</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -2,8 +2,10 @@
|
|||
<h1 class="index-title">{{ page_title }}</h1>
|
||||
{%- if subtitle %}<h4 class="text-muted">{{ subtitle }}</h4>{% endif -%}
|
||||
{% endif %}
|
||||
{%- assign post_count = model.posts | size -%}
|
||||
{%- if post_count > 0 %}
|
||||
<section class="container mt-3" aria-label="The posts for the page">
|
||||
{% for post in model.posts %}
|
||||
{%- for post in model.posts %}
|
||||
<article>
|
||||
<h1>
|
||||
<a href="{{ post | relative_link }}" title="Permanent link to "{{ post.title | escape }}"">
|
||||
|
@ -51,3 +53,8 @@
|
|||
{%- endif -%}
|
||||
</ul>
|
||||
</nav>
|
||||
{%- else %}
|
||||
<article>
|
||||
<p class="text-center mt-3">No posts found</p>
|
||||
</article>
|
||||
{%- endif %}
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
|
||||
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
|
||||
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
|
||||
<title>{{ page_title | strip_html }}{% if page_title %} « {% endif %}{{ web_log.name | strip_html }}</title>
|
||||
{% page_head -%}
|
||||
</head>
|
||||
|
@ -55,8 +55,8 @@
|
|||
<img src="{{ "themes/admin/logo-dark.png" | relative_link }}" alt="myWebLog" width="120" height="34">
|
||||
</div>
|
||||
</footer>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
|
||||
crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
Loading…
Reference in New Issue
Block a user