Complete page / post revision maint (#13)
- Fix log on redirection - Move page handlers to its own file - Add version to admin area footer - Move generator to HttpContext extension property
This commit is contained in:
parent
039d09aed5
commit
d290e6e8a6
|
@ -9,8 +9,12 @@ module Extensions =
|
||||||
|
|
||||||
open System.Security.Claims
|
open System.Security.Claims
|
||||||
open Microsoft.AspNetCore.Antiforgery
|
open Microsoft.AspNetCore.Antiforgery
|
||||||
|
open Microsoft.Extensions.Configuration
|
||||||
open Microsoft.Extensions.DependencyInjection
|
open Microsoft.Extensions.DependencyInjection
|
||||||
|
|
||||||
|
/// Hold variable for the configured generator string
|
||||||
|
let mutable private generatorString : string option = None
|
||||||
|
|
||||||
type HttpContext with
|
type HttpContext with
|
||||||
|
|
||||||
/// The anti-CSRF service
|
/// The anti-CSRF service
|
||||||
|
@ -22,6 +26,18 @@ module Extensions =
|
||||||
/// The data implementation
|
/// The data implementation
|
||||||
member this.Data = this.RequestServices.GetRequiredService<IData> ()
|
member this.Data = this.RequestServices.GetRequiredService<IData> ()
|
||||||
|
|
||||||
|
/// The generator string
|
||||||
|
member this.Generator =
|
||||||
|
match generatorString with
|
||||||
|
| Some gen -> gen
|
||||||
|
| None ->
|
||||||
|
let cfg = this.RequestServices.GetRequiredService<IConfiguration> ()
|
||||||
|
generatorString <-
|
||||||
|
match Option.ofObj cfg["Generator"] with
|
||||||
|
| Some gen -> Some gen
|
||||||
|
| None -> Some "generator not configured"
|
||||||
|
generatorString.Value
|
||||||
|
|
||||||
/// The user ID for the current request
|
/// The user ID for the current request
|
||||||
member this.UserId =
|
member this.UserId =
|
||||||
WebLogUserId (this.User.Claims |> Seq.find (fun c -> c.Type = ClaimTypes.NameIdentifier)).Value
|
WebLogUserId (this.User.Claims |> Seq.find (fun c -> c.Type = ClaimTypes.NameIdentifier)).Value
|
||||||
|
|
|
@ -118,218 +118,8 @@ let deleteCategory catId : HttpHandler = fun next ctx -> task {
|
||||||
return! listCategoriesBare next ctx
|
return! listCategoriesBare next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- PAGES --
|
|
||||||
|
|
||||||
// GET /admin/pages
|
|
||||||
// GET /admin/pages/page/{pageNbr}
|
|
||||||
let listPages pageNbr : HttpHandler = fun next ctx -> task {
|
|
||||||
let webLog = ctx.WebLog
|
|
||||||
let! pages = ctx.Data.Page.findPageOfPages webLog.id pageNbr
|
|
||||||
return!
|
|
||||||
Hash.FromAnonymousObject {|
|
|
||||||
page_title = "Pages"
|
|
||||||
csrf = ctx.CsrfTokenSet
|
|
||||||
pages = pages |> List.map (DisplayPage.fromPageMinimal webLog)
|
|
||||||
page_nbr = pageNbr
|
|
||||||
prev_page = if pageNbr = 2 then "" else $"/page/{pageNbr - 1}"
|
|
||||||
next_page = $"/page/{pageNbr + 1}"
|
|
||||||
|}
|
|
||||||
|> viewForTheme "admin" "page-list" next ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /admin/page/{id}/edit
|
|
||||||
let editPage pgId : HttpHandler = fun next ctx -> task {
|
|
||||||
let! result = task {
|
|
||||||
match pgId with
|
|
||||||
| "new" -> return Some ("Add a New Page", { Page.empty with id = PageId "new" })
|
|
||||||
| _ ->
|
|
||||||
match! ctx.Data.Page.findFullById (PageId pgId) ctx.WebLog.id with
|
|
||||||
| Some page -> return Some ("Edit Page", page)
|
|
||||||
| None -> return None
|
|
||||||
}
|
|
||||||
match result with
|
|
||||||
| Some (title, page) ->
|
|
||||||
let model = EditPageModel.fromPage page
|
|
||||||
let! templates = templatesForTheme ctx "page"
|
|
||||||
return!
|
|
||||||
Hash.FromAnonymousObject {|
|
|
||||||
page_title = title
|
|
||||||
csrf = ctx.CsrfTokenSet
|
|
||||||
model = model
|
|
||||||
metadata = Array.zip model.metaNames model.metaValues
|
|
||||||
|> Array.mapi (fun idx (name, value) -> [| string idx; name; value |])
|
|
||||||
templates = templates
|
|
||||||
|}
|
|
||||||
|> viewForTheme "admin" "page-edit" next ctx
|
|
||||||
| None -> return! Error.notFound next ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST /admin/page/{id}/delete
|
|
||||||
let deletePage pgId : HttpHandler = fun next ctx -> task {
|
|
||||||
let webLog = ctx.WebLog
|
|
||||||
match! ctx.Data.Page.delete (PageId pgId) webLog.id with
|
|
||||||
| true ->
|
|
||||||
do! PageListCache.update ctx
|
|
||||||
do! addMessage ctx { UserMessage.success with message = "Page deleted successfully" }
|
|
||||||
| false -> do! addMessage ctx { UserMessage.error with message = "Page not found; nothing deleted" }
|
|
||||||
return! redirectToGet "admin/pages" next ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /admin/page/{id}/permalinks
|
|
||||||
let editPagePermalinks pgId : HttpHandler = fun next ctx -> task {
|
|
||||||
match! ctx.Data.Page.findFullById (PageId pgId) ctx.WebLog.id with
|
|
||||||
| Some pg ->
|
|
||||||
return!
|
|
||||||
Hash.FromAnonymousObject {|
|
|
||||||
page_title = "Manage Prior Permalinks"
|
|
||||||
csrf = ctx.CsrfTokenSet
|
|
||||||
model = ManagePermalinksModel.fromPage pg
|
|
||||||
|}
|
|
||||||
|> viewForTheme "admin" "permalinks" next ctx
|
|
||||||
| None -> return! Error.notFound next ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST /admin/page/permalinks
|
|
||||||
let savePagePermalinks : HttpHandler = fun next ctx -> task {
|
|
||||||
let webLog = ctx.WebLog
|
|
||||||
let! model = ctx.BindFormAsync<ManagePermalinksModel> ()
|
|
||||||
let links = model.prior |> Array.map Permalink |> List.ofArray
|
|
||||||
match! ctx.Data.Page.updatePriorPermalinks (PageId model.id) webLog.id links with
|
|
||||||
| true ->
|
|
||||||
do! addMessage ctx { UserMessage.success with message = "Page permalinks saved successfully" }
|
|
||||||
return! redirectToGet $"admin/page/{model.id}/permalinks" next ctx
|
|
||||||
| false -> return! Error.notFound next ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /admin/page/{id}/revisions
|
|
||||||
let editPageRevisions pgId : HttpHandler = fun next ctx -> task {
|
|
||||||
let webLog = ctx.WebLog
|
|
||||||
match! ctx.Data.Page.findFullById (PageId pgId) webLog.id with
|
|
||||||
| Some pg ->
|
|
||||||
return!
|
|
||||||
Hash.FromAnonymousObject {|
|
|
||||||
page_title = "Manage Page Revisions"
|
|
||||||
csrf = ctx.CsrfTokenSet
|
|
||||||
model = ManageRevisionsModel.fromPage webLog pg
|
|
||||||
|}
|
|
||||||
|> viewForTheme "admin" "revisions" next ctx
|
|
||||||
| None -> return! Error.notFound next ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /admin/page/{id}/revisions/purge
|
|
||||||
let purgePageRevisions pgId : HttpHandler = fun next ctx -> task {
|
|
||||||
let webLog = ctx.WebLog
|
|
||||||
let data = ctx.Data
|
|
||||||
match! data.Page.findFullById (PageId pgId) webLog.id with
|
|
||||||
| Some pg ->
|
|
||||||
do! data.Page.update { pg with revisions = [ List.head pg.revisions ] }
|
|
||||||
do! addMessage ctx { UserMessage.success with message = "Prior revisions purged successfully" }
|
|
||||||
return! redirectToGet $"admin/page/{pgId}/revisions" next ctx
|
|
||||||
| None -> return! Error.notFound next ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
open Microsoft.AspNetCore.Http
|
open Microsoft.AspNetCore.Http
|
||||||
|
|
||||||
/// Find the page and the requested revision
|
|
||||||
let private findPageRevision pgId revDate (ctx : HttpContext) = task {
|
|
||||||
match! ctx.Data.Page.findFullById (PageId pgId) ctx.WebLog.id with
|
|
||||||
| Some pg ->
|
|
||||||
let asOf = parseToUtc revDate
|
|
||||||
return Some pg, pg.revisions |> List.tryFind (fun r -> r.asOf = asOf)
|
|
||||||
| None -> return None, None
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /admin/page/{id}/revision/{revision-date}/preview
|
|
||||||
let previewPageRevision (pgId, revDate) : HttpHandler = fun next ctx -> task {
|
|
||||||
match! findPageRevision pgId revDate ctx with
|
|
||||||
| Some _, Some rev ->
|
|
||||||
return! bareForTheme "admin" "" next ctx (Hash.FromAnonymousObject {| content = MarkupText.toHtml rev.text |})
|
|
||||||
| None, _
|
|
||||||
| _, None -> return! Error.notFound next ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
open System
|
|
||||||
|
|
||||||
// POST /admin/page/{id}/revision/{revision-date}/restore
|
|
||||||
let restorePageRevision (pgId, revDate) : HttpHandler = fun next ctx -> task {
|
|
||||||
match! findPageRevision pgId revDate ctx with
|
|
||||||
| Some pg, Some rev ->
|
|
||||||
do! ctx.Data.Page.update
|
|
||||||
{ pg with
|
|
||||||
revisions = { rev with asOf = DateTime.UtcNow }
|
|
||||||
:: (pg.revisions |> List.filter (fun r -> r.asOf <> rev.asOf))
|
|
||||||
}
|
|
||||||
do! addMessage ctx { UserMessage.success with message = "Revision restored successfully" }
|
|
||||||
return! redirectToGet $"admin/page/{pgId}/revisions" next ctx
|
|
||||||
| None, _
|
|
||||||
| _, None -> return! Error.notFound next ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST /admin/page/{id}/revision/{revision-date}/delete
|
|
||||||
let deletePageRevision (pgId, revDate) : HttpHandler = fun next ctx -> task {
|
|
||||||
match! findPageRevision pgId revDate ctx with
|
|
||||||
| Some pg, Some rev ->
|
|
||||||
do! ctx.Data.Page.update { pg with revisions = pg.revisions |> List.filter (fun r -> r.asOf <> rev.asOf) }
|
|
||||||
do! addMessage ctx { UserMessage.success with message = "Revision deleted successfully" }
|
|
||||||
return! bareForTheme "admin" "" next ctx (Hash.FromAnonymousObject {| content = "" |})
|
|
||||||
| None, _
|
|
||||||
| _, None -> return! Error.notFound next ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
#nowarn "3511"
|
|
||||||
|
|
||||||
// POST /admin/page/save
|
|
||||||
let savePage : HttpHandler = fun next ctx -> task {
|
|
||||||
let! model = ctx.BindFormAsync<EditPageModel> ()
|
|
||||||
let webLog = ctx.WebLog
|
|
||||||
let data = ctx.Data
|
|
||||||
let now = DateTime.UtcNow
|
|
||||||
let! pg = task {
|
|
||||||
match model.pageId with
|
|
||||||
| "new" ->
|
|
||||||
return Some
|
|
||||||
{ Page.empty with
|
|
||||||
id = PageId.create ()
|
|
||||||
webLogId = webLog.id
|
|
||||||
authorId = ctx.UserId
|
|
||||||
publishedOn = now
|
|
||||||
}
|
|
||||||
| pgId -> return! data.Page.findFullById (PageId pgId) webLog.id
|
|
||||||
}
|
|
||||||
match pg with
|
|
||||||
| Some page ->
|
|
||||||
let updateList = page.showInPageList <> model.isShownInPageList
|
|
||||||
let revision = { asOf = now; text = MarkupText.parse $"{model.source}: {model.text}" }
|
|
||||||
// Detect a permalink change, and add the prior one to the prior list
|
|
||||||
let page =
|
|
||||||
match Permalink.toString page.permalink with
|
|
||||||
| "" -> page
|
|
||||||
| link when link = model.permalink -> page
|
|
||||||
| _ -> { page with priorPermalinks = page.permalink :: page.priorPermalinks }
|
|
||||||
let page =
|
|
||||||
{ page with
|
|
||||||
title = model.title
|
|
||||||
permalink = Permalink model.permalink
|
|
||||||
updatedOn = now
|
|
||||||
showInPageList = model.isShownInPageList
|
|
||||||
template = match model.template with "" -> None | tmpl -> Some tmpl
|
|
||||||
text = MarkupText.toHtml revision.text
|
|
||||||
metadata = Seq.zip model.metaNames model.metaValues
|
|
||||||
|> Seq.filter (fun it -> fst it > "")
|
|
||||||
|> Seq.map (fun it -> { name = fst it; value = snd it })
|
|
||||||
|> Seq.sortBy (fun it -> $"{it.name.ToLower ()} {it.value.ToLower ()}")
|
|
||||||
|> List.ofSeq
|
|
||||||
revisions = match page.revisions |> List.tryHead with
|
|
||||||
| Some r when r.text = revision.text -> page.revisions
|
|
||||||
| _ -> revision :: page.revisions
|
|
||||||
}
|
|
||||||
do! (if model.pageId = "new" then data.Page.add else data.Page.update) page
|
|
||||||
if updateList then do! PageListCache.update ctx
|
|
||||||
do! addMessage ctx { UserMessage.success with message = "Page saved successfully" }
|
|
||||||
return! redirectToGet $"admin/page/{PageId.toString page.id}/edit" next ctx
|
|
||||||
| None -> return! Error.notFound next ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- TAG MAPPINGS --
|
// -- TAG MAPPINGS --
|
||||||
|
|
||||||
/// Get the hash necessary to render the tag mapping list
|
/// Get the hash necessary to render the tag mapping list
|
||||||
|
@ -407,6 +197,7 @@ let deleteMapping tagMapId : HttpHandler = fun next ctx -> task {
|
||||||
|
|
||||||
// -- THEMES --
|
// -- THEMES --
|
||||||
|
|
||||||
|
open System
|
||||||
open System.IO
|
open System.IO
|
||||||
open System.IO.Compression
|
open System.IO.Compression
|
||||||
open System.Text.RegularExpressions
|
open System.Text.RegularExpressions
|
||||||
|
|
|
@ -382,7 +382,7 @@ let createFeed (feedType : FeedType) posts : HttpHandler = fun next ctx -> backg
|
||||||
setTitleAndDescription feedType webLog cats feed
|
setTitleAndDescription feedType webLog cats feed
|
||||||
|
|
||||||
feed.LastUpdatedTime <- (List.head posts).updatedOn |> DateTimeOffset
|
feed.LastUpdatedTime <- (List.head posts).updatedOn |> DateTimeOffset
|
||||||
feed.Generator <- generator ctx
|
feed.Generator <- ctx.Generator
|
||||||
feed.Items <- posts |> Seq.ofList |> Seq.map toItem
|
feed.Items <- posts |> Seq.ofList |> Seq.map toItem
|
||||||
feed.Language <- "en"
|
feed.Language <- "en"
|
||||||
feed.Id <- WebLog.absoluteUrl webLog link
|
feed.Id <- WebLog.absoluteUrl webLog link
|
||||||
|
|
|
@ -52,24 +52,6 @@ let messages (ctx : HttpContext) = task {
|
||||||
| None -> return [||]
|
| None -> return [||]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hold variable for the configured generator string
|
|
||||||
let mutable private generatorString : string option = None
|
|
||||||
|
|
||||||
open Microsoft.Extensions.Configuration
|
|
||||||
open Microsoft.Extensions.DependencyInjection
|
|
||||||
|
|
||||||
/// Get the generator string
|
|
||||||
let generator (ctx : HttpContext) =
|
|
||||||
match generatorString with
|
|
||||||
| Some gen -> gen
|
|
||||||
| None ->
|
|
||||||
let cfg = ctx.RequestServices.GetRequiredService<IConfiguration> ()
|
|
||||||
generatorString <-
|
|
||||||
match Option.ofObj cfg["Generator"] with
|
|
||||||
| Some gen -> Some gen
|
|
||||||
| None -> Some "generator not configured"
|
|
||||||
generatorString.Value
|
|
||||||
|
|
||||||
open MyWebLog
|
open MyWebLog
|
||||||
open DotLiquid
|
open DotLiquid
|
||||||
|
|
||||||
|
@ -94,7 +76,7 @@ let private populateHash hash ctx = task {
|
||||||
hash.Add ("page_list", PageListCache.get ctx)
|
hash.Add ("page_list", PageListCache.get ctx)
|
||||||
hash.Add ("current_page", ctx.Request.Path.Value.Substring 1)
|
hash.Add ("current_page", ctx.Request.Path.Value.Substring 1)
|
||||||
hash.Add ("messages", messages)
|
hash.Add ("messages", messages)
|
||||||
hash.Add ("generator", generator ctx)
|
hash.Add ("generator", ctx.Generator)
|
||||||
hash.Add ("htmx_script", htmxScript)
|
hash.Add ("htmx_script", htmxScript)
|
||||||
|
|
||||||
do! commitSession ctx
|
do! commitSession ctx
|
||||||
|
@ -219,6 +201,7 @@ open System.Globalization
|
||||||
let parseToUtc (date : string) =
|
let parseToUtc (date : string) =
|
||||||
DateTime.Parse (date, null, DateTimeStyles.AdjustToUniversal)
|
DateTime.Parse (date, null, DateTimeStyles.AdjustToUniversal)
|
||||||
|
|
||||||
|
open Microsoft.Extensions.DependencyInjection
|
||||||
open Microsoft.Extensions.Logging
|
open Microsoft.Extensions.Logging
|
||||||
|
|
||||||
/// Log level for debugging
|
/// Log level for debugging
|
||||||
|
|
221
src/MyWebLog/Handlers/Page.fs
Normal file
221
src/MyWebLog/Handlers/Page.fs
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
/// Handlers to manipulate pages
|
||||||
|
module MyWebLog.Handlers.Page
|
||||||
|
|
||||||
|
open DotLiquid
|
||||||
|
open Giraffe
|
||||||
|
open MyWebLog
|
||||||
|
open MyWebLog.ViewModels
|
||||||
|
|
||||||
|
// GET /admin/pages
|
||||||
|
// GET /admin/pages/page/{pageNbr}
|
||||||
|
let all pageNbr : HttpHandler = fun next ctx -> task {
|
||||||
|
let webLog = ctx.WebLog
|
||||||
|
let! pages = ctx.Data.Page.findPageOfPages webLog.id pageNbr
|
||||||
|
return!
|
||||||
|
Hash.FromAnonymousObject {|
|
||||||
|
page_title = "Pages"
|
||||||
|
csrf = ctx.CsrfTokenSet
|
||||||
|
pages = pages |> List.map (DisplayPage.fromPageMinimal webLog)
|
||||||
|
page_nbr = pageNbr
|
||||||
|
prev_page = if pageNbr = 2 then "" else $"/page/{pageNbr - 1}"
|
||||||
|
next_page = $"/page/{pageNbr + 1}"
|
||||||
|
|}
|
||||||
|
|> viewForTheme "admin" "page-list" next ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /admin/page/{id}/edit
|
||||||
|
let edit pgId : HttpHandler = fun next ctx -> task {
|
||||||
|
let! result = task {
|
||||||
|
match pgId with
|
||||||
|
| "new" -> return Some ("Add a New Page", { Page.empty with id = PageId "new" })
|
||||||
|
| _ ->
|
||||||
|
match! ctx.Data.Page.findFullById (PageId pgId) ctx.WebLog.id with
|
||||||
|
| Some page -> return Some ("Edit Page", page)
|
||||||
|
| None -> return None
|
||||||
|
}
|
||||||
|
match result with
|
||||||
|
| Some (title, page) ->
|
||||||
|
let model = EditPageModel.fromPage page
|
||||||
|
let! templates = templatesForTheme ctx "page"
|
||||||
|
return!
|
||||||
|
Hash.FromAnonymousObject {|
|
||||||
|
page_title = title
|
||||||
|
csrf = ctx.CsrfTokenSet
|
||||||
|
model = model
|
||||||
|
metadata = Array.zip model.metaNames model.metaValues
|
||||||
|
|> Array.mapi (fun idx (name, value) -> [| string idx; name; value |])
|
||||||
|
templates = templates
|
||||||
|
|}
|
||||||
|
|> viewForTheme "admin" "page-edit" next ctx
|
||||||
|
| None -> return! Error.notFound next ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /admin/page/{id}/delete
|
||||||
|
let delete pgId : HttpHandler = fun next ctx -> task {
|
||||||
|
let webLog = ctx.WebLog
|
||||||
|
match! ctx.Data.Page.delete (PageId pgId) webLog.id with
|
||||||
|
| true ->
|
||||||
|
do! PageListCache.update ctx
|
||||||
|
do! addMessage ctx { UserMessage.success with message = "Page deleted successfully" }
|
||||||
|
| false -> do! addMessage ctx { UserMessage.error with message = "Page not found; nothing deleted" }
|
||||||
|
return! redirectToGet "admin/pages" next ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /admin/page/{id}/permalinks
|
||||||
|
let editPermalinks pgId : HttpHandler = fun next ctx -> task {
|
||||||
|
match! ctx.Data.Page.findFullById (PageId pgId) ctx.WebLog.id with
|
||||||
|
| Some pg ->
|
||||||
|
return!
|
||||||
|
Hash.FromAnonymousObject {|
|
||||||
|
page_title = "Manage Prior Permalinks"
|
||||||
|
csrf = ctx.CsrfTokenSet
|
||||||
|
model = ManagePermalinksModel.fromPage pg
|
||||||
|
|}
|
||||||
|
|> viewForTheme "admin" "permalinks" next ctx
|
||||||
|
| None -> return! Error.notFound next ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /admin/page/permalinks
|
||||||
|
let savePermalinks : HttpHandler = fun next ctx -> task {
|
||||||
|
let webLog = ctx.WebLog
|
||||||
|
let! model = ctx.BindFormAsync<ManagePermalinksModel> ()
|
||||||
|
let links = model.prior |> Array.map Permalink |> List.ofArray
|
||||||
|
match! ctx.Data.Page.updatePriorPermalinks (PageId model.id) webLog.id links with
|
||||||
|
| true ->
|
||||||
|
do! addMessage ctx { UserMessage.success with message = "Page permalinks saved successfully" }
|
||||||
|
return! redirectToGet $"admin/page/{model.id}/permalinks" next ctx
|
||||||
|
| false -> return! Error.notFound next ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /admin/page/{id}/revisions
|
||||||
|
let editRevisions pgId : HttpHandler = fun next ctx -> task {
|
||||||
|
let webLog = ctx.WebLog
|
||||||
|
match! ctx.Data.Page.findFullById (PageId pgId) webLog.id with
|
||||||
|
| Some pg ->
|
||||||
|
return!
|
||||||
|
Hash.FromAnonymousObject {|
|
||||||
|
page_title = "Manage Page Revisions"
|
||||||
|
csrf = ctx.CsrfTokenSet
|
||||||
|
model = ManageRevisionsModel.fromPage webLog pg
|
||||||
|
|}
|
||||||
|
|> viewForTheme "admin" "revisions" next ctx
|
||||||
|
| None -> return! Error.notFound next ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /admin/page/{id}/revisions/purge
|
||||||
|
let purgeRevisions pgId : HttpHandler = fun next ctx -> task {
|
||||||
|
let webLog = ctx.WebLog
|
||||||
|
let data = ctx.Data
|
||||||
|
match! data.Page.findFullById (PageId pgId) webLog.id with
|
||||||
|
| Some pg ->
|
||||||
|
do! data.Page.update { pg with revisions = [ List.head pg.revisions ] }
|
||||||
|
do! addMessage ctx { UserMessage.success with message = "Prior revisions purged successfully" }
|
||||||
|
return! redirectToGet $"admin/page/{pgId}/revisions" next ctx
|
||||||
|
| None -> return! Error.notFound next ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
open Microsoft.AspNetCore.Http
|
||||||
|
|
||||||
|
/// Find the page and the requested revision
|
||||||
|
let private findPageRevision pgId revDate (ctx : HttpContext) = task {
|
||||||
|
match! ctx.Data.Page.findFullById (PageId pgId) ctx.WebLog.id with
|
||||||
|
| Some pg ->
|
||||||
|
let asOf = parseToUtc revDate
|
||||||
|
return Some pg, pg.revisions |> List.tryFind (fun r -> r.asOf = asOf)
|
||||||
|
| None -> return None, None
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /admin/page/{id}/revision/{revision-date}/preview
|
||||||
|
let previewRevision (pgId, revDate) : HttpHandler = fun next ctx -> task {
|
||||||
|
match! findPageRevision pgId revDate ctx with
|
||||||
|
| Some _, Some rev ->
|
||||||
|
return!
|
||||||
|
Hash.FromAnonymousObject {|
|
||||||
|
content = $"""<div class="mwl-revision-preview mb-3">{MarkupText.toHtml rev.text}</div>"""
|
||||||
|
|}
|
||||||
|
|> bareForTheme "admin" "" next ctx
|
||||||
|
| None, _
|
||||||
|
| _, None -> return! Error.notFound next ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
open System
|
||||||
|
|
||||||
|
// POST /admin/page/{id}/revision/{revision-date}/restore
|
||||||
|
let restoreRevision (pgId, revDate) : HttpHandler = fun next ctx -> task {
|
||||||
|
match! findPageRevision pgId revDate ctx with
|
||||||
|
| Some pg, Some rev ->
|
||||||
|
do! ctx.Data.Page.update
|
||||||
|
{ pg with
|
||||||
|
revisions = { rev with asOf = DateTime.UtcNow }
|
||||||
|
:: (pg.revisions |> List.filter (fun r -> r.asOf <> rev.asOf))
|
||||||
|
}
|
||||||
|
do! addMessage ctx { UserMessage.success with message = "Revision restored successfully" }
|
||||||
|
return! redirectToGet $"admin/page/{pgId}/revisions" next ctx
|
||||||
|
| None, _
|
||||||
|
| _, None -> return! Error.notFound next ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /admin/page/{id}/revision/{revision-date}/delete
|
||||||
|
let deleteRevision (pgId, revDate) : HttpHandler = fun next ctx -> task {
|
||||||
|
match! findPageRevision pgId revDate ctx with
|
||||||
|
| Some pg, Some rev ->
|
||||||
|
do! ctx.Data.Page.update { pg with revisions = pg.revisions |> List.filter (fun r -> r.asOf <> rev.asOf) }
|
||||||
|
do! addMessage ctx { UserMessage.success with message = "Revision deleted successfully" }
|
||||||
|
return! bareForTheme "admin" "" next ctx (Hash.FromAnonymousObject {| content = "" |})
|
||||||
|
| None, _
|
||||||
|
| _, None -> return! Error.notFound next ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
#nowarn "3511"
|
||||||
|
|
||||||
|
// POST /admin/page/save
|
||||||
|
let save : HttpHandler = fun next ctx -> task {
|
||||||
|
let! model = ctx.BindFormAsync<EditPageModel> ()
|
||||||
|
let webLog = ctx.WebLog
|
||||||
|
let data = ctx.Data
|
||||||
|
let now = DateTime.UtcNow
|
||||||
|
let! pg = task {
|
||||||
|
match model.pageId with
|
||||||
|
| "new" ->
|
||||||
|
return Some
|
||||||
|
{ Page.empty with
|
||||||
|
id = PageId.create ()
|
||||||
|
webLogId = webLog.id
|
||||||
|
authorId = ctx.UserId
|
||||||
|
publishedOn = now
|
||||||
|
}
|
||||||
|
| pgId -> return! data.Page.findFullById (PageId pgId) webLog.id
|
||||||
|
}
|
||||||
|
match pg with
|
||||||
|
| Some page ->
|
||||||
|
let updateList = page.showInPageList <> model.isShownInPageList
|
||||||
|
let revision = { asOf = now; text = MarkupText.parse $"{model.source}: {model.text}" }
|
||||||
|
// Detect a permalink change, and add the prior one to the prior list
|
||||||
|
let page =
|
||||||
|
match Permalink.toString page.permalink with
|
||||||
|
| "" -> page
|
||||||
|
| link when link = model.permalink -> page
|
||||||
|
| _ -> { page with priorPermalinks = page.permalink :: page.priorPermalinks }
|
||||||
|
let page =
|
||||||
|
{ page with
|
||||||
|
title = model.title
|
||||||
|
permalink = Permalink model.permalink
|
||||||
|
updatedOn = now
|
||||||
|
showInPageList = model.isShownInPageList
|
||||||
|
template = match model.template with "" -> None | tmpl -> Some tmpl
|
||||||
|
text = MarkupText.toHtml revision.text
|
||||||
|
metadata = Seq.zip model.metaNames model.metaValues
|
||||||
|
|> Seq.filter (fun it -> fst it > "")
|
||||||
|
|> Seq.map (fun it -> { name = fst it; value = snd it })
|
||||||
|
|> Seq.sortBy (fun it -> $"{it.name.ToLower ()} {it.value.ToLower ()}")
|
||||||
|
|> List.ofSeq
|
||||||
|
revisions = match page.revisions |> List.tryHead with
|
||||||
|
| Some r when r.text = revision.text -> page.revisions
|
||||||
|
| _ -> revision :: page.revisions
|
||||||
|
}
|
||||||
|
do! (if model.pageId = "new" then data.Page.add else data.Page.update) page
|
||||||
|
if updateList then do! PageListCache.update ctx
|
||||||
|
do! addMessage ctx { UserMessage.success with message = "Page saved successfully" }
|
||||||
|
return! redirectToGet $"admin/page/{PageId.toString page.id}/edit" next ctx
|
||||||
|
| None -> return! Error.notFound next ctx
|
||||||
|
}
|
|
@ -256,6 +256,15 @@ let edit postId : HttpHandler = fun next ctx -> task {
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST /admin/post/{id}/delete
|
||||||
|
let delete postId : HttpHandler = fun next ctx -> task {
|
||||||
|
let webLog = ctx.WebLog
|
||||||
|
match! ctx.Data.Post.delete (PostId postId) webLog.id with
|
||||||
|
| true -> do! addMessage ctx { UserMessage.success with message = "Post deleted successfully" }
|
||||||
|
| false -> do! addMessage ctx { UserMessage.error with message = "Post not found; nothing deleted" }
|
||||||
|
return! redirectToGet "admin/posts" next ctx
|
||||||
|
}
|
||||||
|
|
||||||
// GET /admin/post/{id}/permalinks
|
// GET /admin/post/{id}/permalinks
|
||||||
let editPermalinks postId : HttpHandler = fun next ctx -> task {
|
let editPermalinks postId : HttpHandler = fun next ctx -> task {
|
||||||
match! ctx.Data.Post.findFullById (PostId postId) ctx.WebLog.id with
|
match! ctx.Data.Post.findFullById (PostId postId) ctx.WebLog.id with
|
||||||
|
@ -282,13 +291,79 @@ let savePermalinks : HttpHandler = fun next ctx -> task {
|
||||||
| false -> return! Error.notFound next ctx
|
| false -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /admin/post/{id}/delete
|
// GET /admin/post/{id}/revisions
|
||||||
let delete postId : HttpHandler = fun next ctx -> task {
|
let editRevisions postId : HttpHandler = fun next ctx -> task {
|
||||||
let webLog = ctx.WebLog
|
match! ctx.Data.Post.findFullById (PostId postId) ctx.WebLog.id with
|
||||||
match! ctx.Data.Post.delete (PostId postId) webLog.id with
|
| Some post ->
|
||||||
| true -> do! addMessage ctx { UserMessage.success with message = "Post deleted successfully" }
|
return!
|
||||||
| false -> do! addMessage ctx { UserMessage.error with message = "Post not found; nothing deleted" }
|
Hash.FromAnonymousObject {|
|
||||||
return! redirectToGet "admin/posts" next ctx
|
page_title = "Manage Post Revisions"
|
||||||
|
csrf = ctx.CsrfTokenSet
|
||||||
|
model = ManageRevisionsModel.fromPost ctx.WebLog post
|
||||||
|
|}
|
||||||
|
|> viewForTheme "admin" "revisions" next ctx
|
||||||
|
| None -> return! Error.notFound next ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /admin/post/{id}/revisions/purge
|
||||||
|
let purgeRevisions postId : HttpHandler = fun next ctx -> task {
|
||||||
|
let data = ctx.Data
|
||||||
|
match! data.Post.findFullById (PostId postId) ctx.WebLog.id with
|
||||||
|
| Some post ->
|
||||||
|
do! data.Post.update { post with revisions = [ List.head post.revisions ] }
|
||||||
|
do! addMessage ctx { UserMessage.success with message = "Prior revisions purged successfully" }
|
||||||
|
return! redirectToGet $"admin/post/{postId}/revisions" next ctx
|
||||||
|
| None -> return! Error.notFound next ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
open Microsoft.AspNetCore.Http
|
||||||
|
|
||||||
|
/// Find the post and the requested revision
|
||||||
|
let private findPostRevision postId revDate (ctx : HttpContext) = task {
|
||||||
|
match! ctx.Data.Post.findFullById (PostId postId) ctx.WebLog.id with
|
||||||
|
| Some post ->
|
||||||
|
let asOf = parseToUtc revDate
|
||||||
|
return Some post, post.revisions |> List.tryFind (fun r -> r.asOf = asOf)
|
||||||
|
| None -> return None, None
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /admin/post/{id}/revision/{revision-date}/preview
|
||||||
|
let previewRevision (postId, revDate) : HttpHandler = fun next ctx -> task {
|
||||||
|
match! findPostRevision postId revDate ctx with
|
||||||
|
| Some _, Some rev ->
|
||||||
|
return!
|
||||||
|
Hash.FromAnonymousObject {|
|
||||||
|
content = $"""<div class="mwl-revision-preview mb-3">{MarkupText.toHtml rev.text}</div>"""
|
||||||
|
|}
|
||||||
|
|> bareForTheme "admin" "" next ctx
|
||||||
|
| None, _
|
||||||
|
| _, None -> return! Error.notFound next ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /admin/post/{id}/revision/{revision-date}/restore
|
||||||
|
let restoreRevision (postId, revDate) : HttpHandler = fun next ctx -> task {
|
||||||
|
match! findPostRevision postId revDate ctx with
|
||||||
|
| Some post, Some rev ->
|
||||||
|
do! ctx.Data.Post.update
|
||||||
|
{ post with
|
||||||
|
revisions = { rev with asOf = DateTime.UtcNow }
|
||||||
|
:: (post.revisions |> List.filter (fun r -> r.asOf <> rev.asOf))
|
||||||
|
}
|
||||||
|
do! addMessage ctx { UserMessage.success with message = "Revision restored successfully" }
|
||||||
|
return! redirectToGet $"admin/post/{postId}/revisions" next ctx
|
||||||
|
| None, _
|
||||||
|
| _, None -> return! Error.notFound next ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /admin/post/{id}/revision/{revision-date}/delete
|
||||||
|
let deleteRevision (postId, revDate) : HttpHandler = fun next ctx -> task {
|
||||||
|
match! findPostRevision postId revDate ctx with
|
||||||
|
| Some post, Some rev ->
|
||||||
|
do! ctx.Data.Post.update { post with revisions = post.revisions |> List.filter (fun r -> r.asOf <> rev.asOf) }
|
||||||
|
do! addMessage ctx { UserMessage.success with message = "Revision deleted successfully" }
|
||||||
|
return! bareForTheme "admin" "" next ctx (Hash.FromAnonymousObject {| content = "" |})
|
||||||
|
| None, _
|
||||||
|
| _, None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
#nowarn "3511"
|
#nowarn "3511"
|
||||||
|
|
|
@ -29,7 +29,7 @@ module CatchAll =
|
||||||
// Current post
|
// Current post
|
||||||
match data.Post.findByPermalink permalink webLog.id |> await with
|
match data.Post.findByPermalink permalink webLog.id |> await with
|
||||||
| Some post ->
|
| Some post ->
|
||||||
debug (fun () -> $"Found post by permalink")
|
debug (fun () -> "Found post by permalink")
|
||||||
let model = Post.preparePostList webLog [ post ] Post.ListType.SinglePost "" 1 1 ctx data |> await
|
let model = Post.preparePostList webLog [ post ] Post.ListType.SinglePost "" 1 1 ctx data |> await
|
||||||
model.Add ("page_title", post.title)
|
model.Add ("page_title", post.title)
|
||||||
yield fun next ctx -> themedView (defaultArg post.template "single-post") next ctx model
|
yield fun next ctx -> themedView (defaultArg post.template "single-post") next ctx model
|
||||||
|
@ -37,7 +37,7 @@ module CatchAll =
|
||||||
// Current page
|
// Current page
|
||||||
match data.Page.findByPermalink permalink webLog.id |> await with
|
match data.Page.findByPermalink permalink webLog.id |> await with
|
||||||
| Some page ->
|
| Some page ->
|
||||||
debug (fun () -> $"Found page by permalink")
|
debug (fun () -> "Found page by permalink")
|
||||||
yield fun next ctx ->
|
yield fun next ctx ->
|
||||||
Hash.FromAnonymousObject {|
|
Hash.FromAnonymousObject {|
|
||||||
page_title = page.title
|
page_title = page.title
|
||||||
|
@ -50,7 +50,7 @@ module CatchAll =
|
||||||
// RSS feed
|
// RSS feed
|
||||||
match Feed.deriveFeedType ctx textLink with
|
match Feed.deriveFeedType ctx textLink with
|
||||||
| Some (feedType, postCount) ->
|
| Some (feedType, postCount) ->
|
||||||
debug (fun () -> $"Found RSS feed")
|
debug (fun () -> "Found RSS feed")
|
||||||
yield Feed.generate feedType postCount
|
yield Feed.generate feedType postCount
|
||||||
| None -> ()
|
| None -> ()
|
||||||
// Post differing only by trailing slash
|
// Post differing only by trailing slash
|
||||||
|
@ -58,28 +58,28 @@ module CatchAll =
|
||||||
Permalink (if textLink.EndsWith "/" then textLink[1..textLink.Length - 2] else $"{textLink[1..]}/")
|
Permalink (if textLink.EndsWith "/" then textLink[1..textLink.Length - 2] else $"{textLink[1..]}/")
|
||||||
match data.Post.findByPermalink altLink webLog.id |> await with
|
match data.Post.findByPermalink altLink webLog.id |> await with
|
||||||
| Some post ->
|
| Some post ->
|
||||||
debug (fun () -> $"Found post by trailing-slash-agnostic permalink")
|
debug (fun () -> "Found post by trailing-slash-agnostic permalink")
|
||||||
yield redirectTo true (WebLog.relativeUrl webLog post.permalink)
|
yield redirectTo true (WebLog.relativeUrl webLog post.permalink)
|
||||||
| None -> ()
|
| None -> ()
|
||||||
// Page differing only by trailing slash
|
// Page differing only by trailing slash
|
||||||
match data.Page.findByPermalink altLink webLog.id |> await with
|
match data.Page.findByPermalink altLink webLog.id |> await with
|
||||||
| Some page ->
|
| Some page ->
|
||||||
debug (fun () -> $"Found page by trailing-slash-agnostic permalink")
|
debug (fun () -> "Found page by trailing-slash-agnostic permalink")
|
||||||
yield redirectTo true (WebLog.relativeUrl webLog page.permalink)
|
yield redirectTo true (WebLog.relativeUrl webLog page.permalink)
|
||||||
| None -> ()
|
| None -> ()
|
||||||
// Prior post
|
// Prior post
|
||||||
match data.Post.findCurrentPermalink [ permalink; altLink ] webLog.id |> await with
|
match data.Post.findCurrentPermalink [ permalink; altLink ] webLog.id |> await with
|
||||||
| Some link ->
|
| Some link ->
|
||||||
debug (fun () -> $"Found post by prior permalink")
|
debug (fun () -> "Found post by prior permalink")
|
||||||
yield redirectTo true (WebLog.relativeUrl webLog link)
|
yield redirectTo true (WebLog.relativeUrl webLog link)
|
||||||
| None -> ()
|
| None -> ()
|
||||||
// Prior page
|
// Prior page
|
||||||
match data.Page.findCurrentPermalink [ permalink; altLink ] webLog.id |> await with
|
match data.Page.findCurrentPermalink [ permalink; altLink ] webLog.id |> await with
|
||||||
| Some link ->
|
| Some link ->
|
||||||
debug (fun () -> $"Found page by prior permalink")
|
debug (fun () -> "Found page by prior permalink")
|
||||||
yield redirectTo true (WebLog.relativeUrl webLog link)
|
yield redirectTo true (WebLog.relativeUrl webLog link)
|
||||||
| None -> ()
|
| None -> ()
|
||||||
debug (fun () -> $"No content found")
|
debug (fun () -> "No content found")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET {all-of-the-above}
|
// GET {all-of-the-above}
|
||||||
|
@ -119,18 +119,20 @@ let router : HttpHandler = choose [
|
||||||
])
|
])
|
||||||
route "/dashboard" >=> Admin.dashboard
|
route "/dashboard" >=> Admin.dashboard
|
||||||
subRoute "/page" (choose [
|
subRoute "/page" (choose [
|
||||||
route "s" >=> Admin.listPages 1
|
route "s" >=> Page.all 1
|
||||||
routef "s/page/%i" Admin.listPages
|
routef "s/page/%i" Page.all
|
||||||
routef "/%s/edit" Admin.editPage
|
routef "/%s/edit" Page.edit
|
||||||
routef "/%s/permalinks" Admin.editPagePermalinks
|
routef "/%s/permalinks" Page.editPermalinks
|
||||||
routef "/%s/revision/%s/preview" Admin.previewPageRevision
|
routef "/%s/revision/%s/preview" Page.previewRevision
|
||||||
routef "/%s/revisions" Admin.editPageRevisions
|
routef "/%s/revisions" Page.editRevisions
|
||||||
])
|
])
|
||||||
subRoute "/post" (choose [
|
subRoute "/post" (choose [
|
||||||
route "s" >=> Post.all 1
|
route "s" >=> Post.all 1
|
||||||
routef "s/page/%i" Post.all
|
routef "s/page/%i" Post.all
|
||||||
routef "/%s/edit" Post.edit
|
routef "/%s/edit" Post.edit
|
||||||
routef "/%s/permalinks" Post.editPermalinks
|
routef "/%s/permalinks" Post.editPermalinks
|
||||||
|
routef "/%s/revision/%s/preview" Post.previewRevision
|
||||||
|
routef "/%s/revisions" Post.editRevisions
|
||||||
])
|
])
|
||||||
subRoute "/settings" (choose [
|
subRoute "/settings" (choose [
|
||||||
route "" >=> Admin.settings
|
route "" >=> Admin.settings
|
||||||
|
@ -157,17 +159,20 @@ let router : HttpHandler = choose [
|
||||||
routef "/%s/delete" Admin.deleteCategory
|
routef "/%s/delete" Admin.deleteCategory
|
||||||
])
|
])
|
||||||
subRoute "/page" (choose [
|
subRoute "/page" (choose [
|
||||||
route "/save" >=> Admin.savePage
|
route "/save" >=> Page.save
|
||||||
route "/permalinks" >=> Admin.savePagePermalinks
|
route "/permalinks" >=> Page.savePermalinks
|
||||||
routef "/%s/delete" Admin.deletePage
|
routef "/%s/delete" Page.delete
|
||||||
routef "/%s/revision/%s/delete" Admin.deletePageRevision
|
routef "/%s/revision/%s/delete" Page.deleteRevision
|
||||||
routef "/%s/revision/%s/restore" Admin.restorePageRevision
|
routef "/%s/revision/%s/restore" Page.restoreRevision
|
||||||
routef "/%s/revisions/purge" Admin.purgePageRevisions
|
routef "/%s/revisions/purge" Page.purgeRevisions
|
||||||
])
|
])
|
||||||
subRoute "/post" (choose [
|
subRoute "/post" (choose [
|
||||||
route "/save" >=> Post.save
|
route "/save" >=> Post.save
|
||||||
route "/permalinks" >=> Post.savePermalinks
|
route "/permalinks" >=> Post.savePermalinks
|
||||||
routef "/%s/delete" Post.delete
|
routef "/%s/delete" Post.delete
|
||||||
|
routef "/%s/revision/%s/delete" Post.deleteRevision
|
||||||
|
routef "/%s/revision/%s/restore" Post.restoreRevision
|
||||||
|
routef "/%s/revisions/purge" Post.purgeRevisions
|
||||||
])
|
])
|
||||||
subRoute "/settings" (choose [
|
subRoute "/settings" (choose [
|
||||||
route "" >=> Admin.saveSettings
|
route "" >=> Admin.saveSettings
|
||||||
|
|
|
@ -40,9 +40,8 @@ open Microsoft.AspNetCore.Authentication.Cookies
|
||||||
|
|
||||||
// POST /user/log-on
|
// POST /user/log-on
|
||||||
let doLogOn : HttpHandler = fun next ctx -> task {
|
let doLogOn : HttpHandler = fun next ctx -> task {
|
||||||
let! model = ctx.BindFormAsync<LogOnModel> ()
|
let! model = ctx.BindFormAsync<LogOnModel> ()
|
||||||
let webLog = ctx.WebLog
|
match! ctx.Data.WebLogUser.findByEmail model.emailAddress ctx.WebLog.id with
|
||||||
match! ctx.Data.WebLogUser.findByEmail model.emailAddress webLog.id with
|
|
||||||
| Some user when user.passwordHash = hashedPassword model.password user.userName user.salt ->
|
| Some user when user.passwordHash = hashedPassword model.password user.userName user.salt ->
|
||||||
let claims = seq {
|
let claims = seq {
|
||||||
Claim (ClaimTypes.NameIdentifier, WebLogUserId.toString user.id)
|
Claim (ClaimTypes.NameIdentifier, WebLogUserId.toString user.id)
|
||||||
|
@ -55,8 +54,8 @@ let doLogOn : HttpHandler = fun next ctx -> task {
|
||||||
do! ctx.SignInAsync (identity.AuthenticationType, ClaimsPrincipal identity,
|
do! ctx.SignInAsync (identity.AuthenticationType, ClaimsPrincipal identity,
|
||||||
AuthenticationProperties (IssuedUtc = DateTimeOffset.UtcNow))
|
AuthenticationProperties (IssuedUtc = DateTimeOffset.UtcNow))
|
||||||
do! addMessage ctx
|
do! addMessage ctx
|
||||||
{ UserMessage.success with message = $"Logged on successfully | Welcome to {webLog.name}!" }
|
{ UserMessage.success with message = $"Logged on successfully | Welcome to {ctx.WebLog.name}!" }
|
||||||
return! redirectToGet (defaultArg model.returnTo "admin/dashboard") next ctx
|
return! redirectToGet (defaultArg (model.returnTo |> Option.map (fun it -> it[1..])) "admin/dashboard") next ctx
|
||||||
| _ ->
|
| _ ->
|
||||||
do! addMessage ctx { UserMessage.error with message = "Log on attempt unsuccessful" }
|
do! addMessage ctx { UserMessage.error with message = "Log on attempt unsuccessful" }
|
||||||
return! logOn model.returnTo next ctx
|
return! logOn model.returnTo next ctx
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
<Compile Include="Handlers\Helpers.fs" />
|
<Compile Include="Handlers\Helpers.fs" />
|
||||||
<Compile Include="Handlers\Admin.fs" />
|
<Compile Include="Handlers\Admin.fs" />
|
||||||
<Compile Include="Handlers\Feed.fs" />
|
<Compile Include="Handlers\Feed.fs" />
|
||||||
|
<Compile Include="Handlers\Page.fs" />
|
||||||
<Compile Include="Handlers\Post.fs" />
|
<Compile Include="Handlers\Post.fs" />
|
||||||
<Compile Include="Handlers\User.fs" />
|
<Compile Include="Handlers\User.fs" />
|
||||||
<Compile Include="Handlers\Upload.fs" />
|
<Compile Include="Handlers\Upload.fs" />
|
||||||
|
|
|
@ -48,6 +48,8 @@
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 text-end">
|
<div class="col-xs-12 text-end">
|
||||||
|
{%- assign version = generator | split: " " -%}
|
||||||
|
<small class="me-1 align-baseline">v{{ version[1] }}</small>
|
||||||
<img src="{{ "themes/admin/logo-light.png" | relative_link }}" alt="myWebLog" width="120" height="34">
|
<img src="{{ "themes/admin/logo-light.png" | relative_link }}" alt="myWebLog" width="120" height="34">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -16,8 +16,15 @@
|
||||||
value="{{ model.permalink }}">
|
value="{{ model.permalink }}">
|
||||||
<label for="permalink">Permalink</label>
|
<label for="permalink">Permalink</label>
|
||||||
{%- if model.post_id != "new" %}
|
{%- if model.post_id != "new" %}
|
||||||
{%- capture perm_edit %}admin/post/{{ model.post_id }}/permalinks{% endcapture -%}
|
<span class="form-text">
|
||||||
<span class="form-text"><a href="{{ perm_edit | relative_link }}">Manage Permalinks</a></span>
|
<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>
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<form method="post" hx-target="body">
|
<form method="post" hx-target="body">
|
||||||
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
||||||
<input type="hidden" name="id" value="{{ model.id }}">
|
<input type="hidden" name="id" value="{{ model.id }}">
|
||||||
<div class="container">
|
<div class="container mb-3">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<p style="line-height:1.2rem;">
|
<p style="line-height:1.2rem;">
|
||||||
|
@ -29,11 +29,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
<div class="row mwl-table-heading">
|
||||||
|
<div class="col">Revision</div>
|
||||||
|
</div>
|
||||||
{% for rev in model.revisions %}
|
{% for rev in model.revisions %}
|
||||||
{%- assign as_of_string = rev.as_of | date: "o" -%}
|
{%- assign as_of_string = rev.as_of | date: "o" -%}
|
||||||
{%- assign as_of_id = "rev_" | append: as_of_string | replace: "\.", "_" | replace: ":", "-" -%}
|
{%- assign as_of_id = "rev_" | append: as_of_string | replace: "\.", "_" | replace: ":", "-" -%}
|
||||||
<div id="{{ as_of_id }}" class="row mb-3">
|
<div id="{{ as_of_id }}" class="row pb-3 mwl-table-detail">
|
||||||
<div class="col-12 mb-3">
|
<div class="col-12 mb-1">
|
||||||
{{ rev.as_of_local | date: "MMMM d, yyyy" }} at {{ rev.as_of_local | date: "h:mmtt" | downcase }}
|
{{ rev.as_of_local | date: "MMMM d, yyyy" }} at {{ rev.as_of_local | date: "h:mmtt" | downcase }}
|
||||||
<span class="badge bg-secondary text-uppercase ms-2">{{ rev.format }}</span>
|
<span class="badge bg-secondary text-uppercase ms-2">{{ rev.format }}</span>
|
||||||
{%- if forloop.first %}
|
{%- if forloop.first %}
|
||||||
|
@ -50,20 +53,14 @@
|
||||||
<span class="text-muted"> • </span>
|
<span class="text-muted"> • </span>
|
||||||
<a href="{{ rev_restore }}" hx-post="{{ rev_restore }}">Restore as Current</a>
|
<a href="{{ rev_restore }}" hx-post="{{ rev_restore }}">Restore as Current</a>
|
||||||
<span class="text-muted"> • </span>
|
<span class="text-muted"> • </span>
|
||||||
<a href="{{ rev_delete }}" hx-post="{{ rev_delete }}" hx-target="#{{ as_of_id }}" hx-swap="outerHtml"
|
<a href="{{ rev_delete }}" hx-post="{{ rev_delete }}" hx-target="#{{ as_of_id }}" hx-swap="outerHTML"
|
||||||
class="text-danger">
|
class="text-danger">
|
||||||
Delete
|
Delete
|
||||||
</a>
|
</a>
|
||||||
</small>
|
</small>
|
||||||
{% endunless %}
|
{% endunless %}
|
||||||
</div>
|
</div>
|
||||||
{% unless forloop.first %}
|
{% unless forloop.first %}<div id="{{ as_of_id }}_preview" class="col-12"></div>{% endunless %}
|
||||||
<div class="col-12 mb-3">
|
|
||||||
<div id="{{ as_of_id }}_preview" class="mwl-revision-preview">
|
|
||||||
<span class="text-muted fst-italic">preview not loaded</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endunless %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -87,4 +87,8 @@ a.text-danger:link:hover, a.text-danger:visited:hover {
|
||||||
}
|
}
|
||||||
.mwl-revision-preview {
|
.mwl-revision-preview {
|
||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
|
overflow: auto;
|
||||||
|
border: solid 1px black;
|
||||||
|
border-radius: .5rem;
|
||||||
|
padding: .5rem;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user