Migrate more templates to GVE
This commit is contained in:
parent
5f114c7955
commit
b99cd5b94b
|
@ -204,26 +204,6 @@ type DisplayPage = {
|
|||
}
|
||||
|
||||
|
||||
/// Information about a revision used for display
|
||||
[<NoComparison; NoEquality>]
|
||||
type DisplayRevision = {
|
||||
/// The as-of date/time for the revision
|
||||
AsOf: DateTime
|
||||
|
||||
/// The as-of date/time for the revision in the web log's local time zone
|
||||
AsOfLocal: DateTime
|
||||
|
||||
/// The format of the text of the revision
|
||||
Format: string
|
||||
} with
|
||||
|
||||
/// Create a display revision from an actual revision
|
||||
static member FromRevision (webLog: WebLog) (rev : Revision) =
|
||||
{ AsOf = rev.AsOf.ToDateTimeUtc()
|
||||
AsOfLocal = webLog.LocalTime rev.AsOf
|
||||
Format = rev.Text.SourceType }
|
||||
|
||||
|
||||
open System.IO
|
||||
|
||||
/// Information about a theme used for display
|
||||
|
@ -1180,22 +1160,22 @@ type ManageRevisionsModel = {
|
|||
CurrentTitle: string
|
||||
|
||||
/// The revisions for the page or post
|
||||
Revisions: DisplayRevision array
|
||||
Revisions: Revision list
|
||||
} with
|
||||
|
||||
/// Create a revision model from a page
|
||||
static member FromPage webLog (page: Page) =
|
||||
static member FromPage (page: Page) =
|
||||
{ Id = string page.Id
|
||||
Entity = "page"
|
||||
CurrentTitle = page.Title
|
||||
Revisions = page.Revisions |> List.map (DisplayRevision.FromRevision webLog) |> Array.ofList }
|
||||
Revisions = page.Revisions }
|
||||
|
||||
/// Create a revision model from a post
|
||||
static member FromPost webLog (post: Post) =
|
||||
static member FromPost (post: Post) =
|
||||
{ Id = string post.Id
|
||||
Entity = "post"
|
||||
CurrentTitle = post.Title
|
||||
Revisions = post.Revisions |> List.map (DisplayRevision.FromRevision webLog) |> Array.ofList }
|
||||
Revisions = post.Revisions }
|
||||
|
||||
|
||||
/// View model for posts in a list
|
||||
|
|
|
@ -179,17 +179,6 @@ let displayPageTests = testList "DisplayPage" [
|
|||
]
|
||||
]
|
||||
|
||||
/// Unit tests for the DisplayRevision type
|
||||
let displayRevisionTests = test "DisplayRevision.FromRevision succeeds" {
|
||||
let model =
|
||||
DisplayRevision.FromRevision
|
||||
{ WebLog.Empty with TimeZone = "Etc/GMT+1" }
|
||||
{ Text = Html "howdy"; AsOf = Noda.epoch }
|
||||
Expect.equal model.AsOf (Noda.epoch.ToDateTimeUtc()) "AsOf not filled properly"
|
||||
Expect.equal model.AsOfLocal ((Noda.epoch - Duration.FromHours 1).ToDateTimeUtc()) "AsOfLocal not filled properly"
|
||||
Expect.equal model.Format "HTML" "Format not filled properly"
|
||||
}
|
||||
|
||||
open System.IO
|
||||
|
||||
/// Unit tests for the DisplayTheme type
|
||||
|
@ -1346,7 +1335,6 @@ let all = testList "ViewModels" [
|
|||
displayChapterTests
|
||||
displayCustomFeedTests
|
||||
displayPageTests
|
||||
displayRevisionTests
|
||||
displayThemeTests
|
||||
displayUploadTests
|
||||
displayUserTests
|
||||
|
|
|
@ -228,16 +228,10 @@ let register () =
|
|||
typeof<CustomFeed>; typeof<Episode>; typeof<Episode option>; typeof<MetaItem>; typeof<Page>
|
||||
typeof<RedirectRule>; typeof<RssOptions>; typeof<TagMap>; typeof<UploadDestination>; typeof<WebLog>
|
||||
// View models
|
||||
typeof<AppViewContext>; typeof<DashboardModel>; typeof<DisplayCategory>
|
||||
typeof<DisplayChapter>; typeof<DisplayCustomFeed>; typeof<DisplayPage>
|
||||
typeof<DisplayRevision>; typeof<DisplayTheme>; typeof<DisplayUpload>
|
||||
typeof<DisplayUser>; typeof<EditCategoryModel>; typeof<EditChapterModel>
|
||||
typeof<EditCustomFeedModel>; typeof<EditMyInfoModel>; typeof<EditPageModel>
|
||||
typeof<EditPostModel>; typeof<EditRedirectRuleModel>; typeof<EditRssModel>
|
||||
typeof<EditTagMapModel>; typeof<EditUserModel>; typeof<LogOnModel>
|
||||
typeof<ManageChaptersModel>; typeof<ManagePermalinksModel>; typeof<ManageRevisionsModel>
|
||||
typeof<PostDisplay>; typeof<PostListItem>; typeof<SettingsModel>
|
||||
typeof<UserMessage>
|
||||
typeof<AppViewContext>; typeof<DisplayCategory>; typeof<DisplayCustomFeed>; typeof<DisplayPage>
|
||||
typeof<DisplayTheme>; typeof<DisplayUpload>; typeof<DisplayUser>; typeof<EditCategoryModel>
|
||||
typeof<EditCustomFeedModel>; typeof<EditPageModel>; typeof<EditPostModel>; typeof<EditRssModel>
|
||||
typeof<PostDisplay>; typeof<PostListItem>; typeof<SettingsModel>; typeof<UserMessage>
|
||||
// Framework types
|
||||
typeof<AntiforgeryTokenSet>; typeof<DateTime option>; typeof<int option>; typeof<KeyValuePair>
|
||||
typeof<MetaItem list>; typeof<string list>; typeof<string option>; typeof<TagMap list>
|
||||
|
|
|
@ -27,45 +27,35 @@ module Dashboard =
|
|||
ListedPages = listed
|
||||
Categories = cats
|
||||
TopLevelCategories = topCats }
|
||||
return! adminPage "Dashboard" false (Views.Admin.dashboard model) next ctx
|
||||
return! adminPage "Dashboard" false next ctx (Views.Admin.dashboard model)
|
||||
}
|
||||
|
||||
// GET /admin/administration
|
||||
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()
|
||||
let cachedTemplates = TemplateCache.allNames ()
|
||||
let! hash =
|
||||
hashForPage "myWebLog Administration"
|
||||
|> withAntiCsrf ctx
|
||||
|> addToHash "themes" (
|
||||
themes
|
||||
|> List.map (DisplayTheme.FromTheme WebLogCache.isThemeInUse)
|
||||
|> Array.ofList)
|
||||
|> addToHash "cached_themes" (
|
||||
themes
|
||||
|> Seq.ofList
|
||||
|> Seq.map (fun it -> [|
|
||||
string it.Id
|
||||
it.Name
|
||||
cachedTemplates
|
||||
|> List.filter _.StartsWith(string it.Id)
|
||||
|> List.length
|
||||
|> string
|
||||
|])
|
||||
|> Array.ofSeq)
|
||||
|> addToHash "web_logs" (
|
||||
WebLogCache.all ()
|
||||
|> Seq.ofList
|
||||
|> Seq.sortBy _.Name
|
||||
|> Seq.map (fun it -> [| string it.Id; it.Name; it.UrlBase |])
|
||||
|> Array.ofSeq)
|
||||
|> addViewContext ctx
|
||||
return!
|
||||
addToHash "theme_list" (bodyTemplate.Render hash) hash
|
||||
|> adminView "admin-dashboard" next ctx
|
||||
| Error message -> return! Error.server message next ctx
|
||||
let! themes = ctx.Data.Theme.All()
|
||||
let cachedTemplates = TemplateCache.allNames ()
|
||||
return!
|
||||
hashForPage "myWebLog Administration"
|
||||
|> withAntiCsrf ctx
|
||||
|> addToHash "cached_themes" (
|
||||
themes
|
||||
|> Seq.ofList
|
||||
|> Seq.map (fun it -> [|
|
||||
string it.Id
|
||||
it.Name
|
||||
cachedTemplates
|
||||
|> List.filter _.StartsWith(string it.Id)
|
||||
|> List.length
|
||||
|> string
|
||||
|])
|
||||
|> Array.ofSeq)
|
||||
|> addToHash "web_logs" (
|
||||
WebLogCache.all ()
|
||||
|> Seq.ofList
|
||||
|> Seq.sortBy _.Name
|
||||
|> Seq.map (fun it -> [| string it.Id; it.Name; it.UrlBase |])
|
||||
|> Array.ofSeq)
|
||||
|> adminView "admin-dashboard" next ctx
|
||||
}
|
||||
|
||||
/// Redirect the user to the admin dashboard
|
||||
|
@ -215,11 +205,11 @@ module RedirectRules =
|
|||
|
||||
// GET /admin/settings/redirect-rules
|
||||
let all : HttpHandler = fun next ctx ->
|
||||
adminPage "Redirect Rules" true (Views.Admin.redirectList ctx.WebLog.RedirectRules) next ctx
|
||||
adminPage "Redirect Rules" true next ctx (Views.Admin.redirectList ctx.WebLog.RedirectRules)
|
||||
|
||||
// GET /admin/settings/redirect-rules/[index]
|
||||
let edit idx : HttpHandler = fun next ctx ->
|
||||
let titleAndModel =
|
||||
let titleAndView =
|
||||
if idx = -1 then
|
||||
Some ("Add", Views.Admin.redirectEdit (EditRedirectRuleModel.FromRule -1 RedirectRule.Empty))
|
||||
else
|
||||
|
@ -228,8 +218,8 @@ module RedirectRules =
|
|||
None
|
||||
else
|
||||
Some ("Edit", (Views.Admin.redirectEdit (EditRedirectRuleModel.FromRule idx (List.item idx rules))))
|
||||
match titleAndModel with
|
||||
| Some (title, model) -> adminBarePage $"{title} Redirect Rule" true model next ctx
|
||||
match titleAndView with
|
||||
| Some (title, view) -> adminBarePage $"{title} Redirect Rule" true next ctx view
|
||||
| None -> Error.notFound next ctx
|
||||
|
||||
/// Update the web log's redirect rules in the database, the request web log, and the web log cache
|
||||
|
@ -294,7 +284,7 @@ module TagMapping =
|
|||
// GET /admin/settings/tag-mappings
|
||||
let all : HttpHandler = fun next ctx -> task {
|
||||
let! mappings = ctx.Data.TagMap.FindByWebLog ctx.WebLog.Id
|
||||
return! adminBarePage "Tag Mapping List" true (Views.Admin.tagMapList mappings) next ctx
|
||||
return! adminBarePage "Tag Mapping List" true next ctx (Views.Admin.tagMapList mappings)
|
||||
}
|
||||
|
||||
// GET /admin/settings/tag-mapping/{id}/edit
|
||||
|
@ -306,9 +296,8 @@ module TagMapping =
|
|||
match! tagMap with
|
||||
| Some tm ->
|
||||
return!
|
||||
adminBarePage
|
||||
(if isNew then "Add Tag Mapping" else $"Mapping for {tm.Tag} Tag") true
|
||||
(Views.Admin.tagMapEdit (EditTagMapModel.FromMapping tm)) next ctx
|
||||
Views.Admin.tagMapEdit (EditTagMapModel.FromMapping tm)
|
||||
|> adminBarePage (if isNew then "Add Tag Mapping" else $"Mapping for {tm.Tag} Tag") true next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
|
@ -349,17 +338,13 @@ module Theme =
|
|||
let all : HttpHandler = requireAccess Administrator >=> fun next ctx -> task {
|
||||
let! themes = ctx.Data.Theme.All ()
|
||||
return!
|
||||
hashForPage "Themes"
|
||||
|> withAntiCsrf ctx
|
||||
|> addToHash "themes" (themes |> List.map (DisplayTheme.FromTheme WebLogCache.isThemeInUse) |> Array.ofList)
|
||||
|> adminBareView "theme-list-body" next ctx
|
||||
Views.Admin.themeList (List.map (DisplayTheme.FromTheme WebLogCache.isThemeInUse) themes)
|
||||
|> adminBarePage "Themes" true next ctx
|
||||
}
|
||||
|
||||
// GET /admin/theme/new
|
||||
let add : HttpHandler = requireAccess Administrator >=> fun next ctx ->
|
||||
hashForPage "Upload a Theme File"
|
||||
|> withAntiCsrf ctx
|
||||
|> adminBareView "theme-upload" next ctx
|
||||
adminBarePage "Upload a Theme File" true next ctx Views.Admin.themeUpload
|
||||
|
||||
/// Update the name and version for a theme based on the version.txt file, if present
|
||||
let private updateNameAndVersion (theme: Theme) (zip: ZipArchive) = backgroundTask {
|
||||
|
|
|
@ -282,8 +282,9 @@ module Error =
|
|||
let notAuthorized : HttpHandler = fun next ctx ->
|
||||
if ctx.Request.Method = "GET" then
|
||||
let redirectUrl = $"user/log-on?returnUrl={WebUtility.UrlEncode ctx.Request.Path}"
|
||||
if isHtmx ctx then (withHxRedirect redirectUrl >=> redirectToGet redirectUrl) next ctx
|
||||
else redirectToGet redirectUrl next ctx
|
||||
(next, ctx)
|
||||
||> if isHtmx ctx then withHxRedirect redirectUrl >=> withHxRetarget "body" >=> redirectToGet redirectUrl
|
||||
else redirectToGet redirectUrl
|
||||
else
|
||||
if isHtmx ctx then
|
||||
let messages = [|
|
||||
|
@ -370,7 +371,7 @@ let adminBareView template =
|
|||
bareForTheme adminTheme template
|
||||
|
||||
/// Display a page for an admin endpoint
|
||||
let adminPage pageTitle includeCsrf (content: AppViewContext -> XmlNode list) : HttpHandler = fun next ctx -> task {
|
||||
let adminPage pageTitle includeCsrf next ctx (content: AppViewContext -> XmlNode list) = task {
|
||||
let! messages = getCurrentMessages ctx
|
||||
let appCtx = generateViewContext pageTitle messages includeCsrf ctx
|
||||
let layout = if isHtmx ctx then Layout.partial else Layout.full
|
||||
|
@ -378,7 +379,7 @@ let adminPage pageTitle includeCsrf (content: AppViewContext -> XmlNode list) :
|
|||
}
|
||||
|
||||
/// Display a bare page for an admin endpoint
|
||||
let adminBarePage pageTitle includeCsrf (content: AppViewContext -> XmlNode list) : HttpHandler = fun next ctx -> task {
|
||||
let adminBarePage pageTitle includeCsrf next ctx (content: AppViewContext -> XmlNode list) = task {
|
||||
let! messages = getCurrentMessages ctx
|
||||
let appCtx = generateViewContext pageTitle messages includeCsrf ctx
|
||||
return!
|
||||
|
@ -471,13 +472,12 @@ let getCategoryIds slug ctx =
|
|||
|> Seq.map (fun c -> CategoryId c.Id)
|
||||
|> List.ofSeq
|
||||
|
||||
open System
|
||||
open System.Globalization
|
||||
open NodaTime
|
||||
|
||||
/// Parse a date/time to UTC
|
||||
let parseToUtc (date: string) =
|
||||
Instant.FromDateTimeUtc(DateTime.Parse(date, null, DateTimeStyles.AdjustToUniversal))
|
||||
let parseToUtc (date: string) : Instant =
|
||||
let result = roundTrip.Parse date
|
||||
if result.Success then result.Value else raise result.Exception
|
||||
|
||||
open Microsoft.Extensions.DependencyInjection
|
||||
open Microsoft.Extensions.Logging
|
||||
|
|
|
@ -66,10 +66,9 @@ let editPermalinks pgId : HttpHandler = requireAccess Author >=> fun next ctx ->
|
|||
match! ctx.Data.Page.FindFullById (PageId pgId) ctx.WebLog.Id with
|
||||
| Some pg when canEdit pg.AuthorId ctx ->
|
||||
return!
|
||||
hashForPage "Manage Prior Permalinks"
|
||||
|> withAntiCsrf ctx
|
||||
|> addToHash ViewContext.Model (ManagePermalinksModel.FromPage pg)
|
||||
|> adminView "permalinks" next ctx
|
||||
ManagePermalinksModel.FromPage pg
|
||||
|> Views.Helpers.managePermalinks
|
||||
|> adminPage "Manage Prior Permalinks" true next ctx
|
||||
| Some _ -> return! Error.notAuthorized next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
@ -95,15 +94,14 @@ let editRevisions pgId : HttpHandler = requireAccess Author >=> fun next ctx ->
|
|||
match! ctx.Data.Page.FindFullById (PageId pgId) ctx.WebLog.Id with
|
||||
| Some pg when canEdit pg.AuthorId ctx ->
|
||||
return!
|
||||
hashForPage "Manage Page Revisions"
|
||||
|> withAntiCsrf ctx
|
||||
|> addToHash ViewContext.Model (ManageRevisionsModel.FromPage ctx.WebLog pg)
|
||||
|> adminView "revisions" next ctx
|
||||
ManageRevisionsModel.FromPage pg
|
||||
|> Views.Helpers.manageRevisions
|
||||
|> adminPage "Manage Page Revisions" true next ctx
|
||||
| Some _ -> return! Error.notAuthorized next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
// GET /admin/page/{id}/revisions/purge
|
||||
// DELETE /admin/page/{id}/revisions
|
||||
let purgeRevisions pgId : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||
let data = ctx.Data
|
||||
match! data.Page.FindFullById (PageId pgId) ctx.WebLog.Id with
|
||||
|
@ -158,7 +156,7 @@ let restoreRevision (pgId, revDate) : HttpHandler = requireAccess Author >=> fun
|
|||
| _, None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
// POST /admin/page/{id}/revision/{revision-date}/delete
|
||||
// DELETE /admin/page/{id}/revision/{revision-date}
|
||||
let deleteRevision (pgId, revDate) : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||
match! findPageRevision pgId revDate ctx with
|
||||
| Some pg, Some rev when canEdit pg.AuthorId ctx ->
|
||||
|
|
|
@ -254,7 +254,7 @@ let all pageNbr : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
|||
let data = ctx.Data
|
||||
let! posts = data.Post.FindPageOfPosts ctx.WebLog.Id pageNbr 25
|
||||
let! hash = preparePostList ctx.WebLog posts AdminList "" pageNbr 25 data
|
||||
return! adminPage "Posts" true (Views.Post.list (hash[ViewContext.Model] :?> PostDisplay)) next ctx
|
||||
return! adminPage "Posts" true next ctx (Views.Post.list (hash[ViewContext.Model] :?> PostDisplay))
|
||||
}
|
||||
|
||||
// GET /admin/post/{id}/edit
|
||||
|
@ -305,10 +305,9 @@ let editPermalinks postId : HttpHandler = requireAccess Author >=> fun next ctx
|
|||
match! ctx.Data.Post.FindFullById (PostId postId) ctx.WebLog.Id with
|
||||
| Some post when canEdit post.AuthorId ctx ->
|
||||
return!
|
||||
hashForPage "Manage Prior Permalinks"
|
||||
|> withAntiCsrf ctx
|
||||
|> addToHash ViewContext.Model (ManagePermalinksModel.FromPost post)
|
||||
|> adminView "permalinks" next ctx
|
||||
ManagePermalinksModel.FromPost post
|
||||
|> Views.Helpers.managePermalinks
|
||||
|> adminPage "Manage Prior Permalinks" true next ctx
|
||||
| Some _ -> return! Error.notAuthorized next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
@ -334,15 +333,14 @@ let editRevisions postId : HttpHandler = requireAccess Author >=> fun next ctx -
|
|||
match! ctx.Data.Post.FindFullById (PostId postId) ctx.WebLog.Id with
|
||||
| Some post when canEdit post.AuthorId ctx ->
|
||||
return!
|
||||
hashForPage "Manage Post Revisions"
|
||||
|> withAntiCsrf ctx
|
||||
|> addToHash ViewContext.Model (ManageRevisionsModel.FromPost ctx.WebLog post)
|
||||
|> adminView "revisions" next ctx
|
||||
ManageRevisionsModel.FromPost post
|
||||
|> Views.Helpers.manageRevisions
|
||||
|> adminPage "Manage Post Revisions" true next ctx
|
||||
| Some _ -> return! Error.notAuthorized next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
// GET /admin/post/{id}/revisions/purge
|
||||
// DELETE /admin/post/{id}/revisions
|
||||
let purgeRevisions postId : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||
let data = ctx.Data
|
||||
match! data.Post.FindFullById (PostId postId) ctx.WebLog.Id with
|
||||
|
@ -398,7 +396,7 @@ let restoreRevision (postId, revDate) : HttpHandler = requireAccess Author >=> f
|
|||
| _, None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
// POST /admin/post/{id}/revision/{revision-date}/delete
|
||||
// DELETE /admin/post/{id}/revision/{revision-date}
|
||||
let deleteRevision (postId, revDate) : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||
match! findPostRevision postId revDate ctx with
|
||||
| Some post, Some rev when canEdit post.AuthorId ctx ->
|
||||
|
@ -418,7 +416,8 @@ let manageChapters postId : HttpHandler = requireAccess Author >=> fun next ctx
|
|||
&& Option.isSome post.Episode.Value.Chapters
|
||||
&& canEdit post.AuthorId ctx ->
|
||||
return!
|
||||
adminPage "Manage Chapters" true (Views.Post.chapters false (ManageChaptersModel.Create post)) next ctx
|
||||
Views.Post.chapters false (ManageChaptersModel.Create post)
|
||||
|> adminPage "Manage Chapters" true next ctx
|
||||
| Some _ | None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
|
@ -437,9 +436,8 @@ let editChapter (postId, index) : HttpHandler = requireAccess Author >=> fun nex
|
|||
match chapter with
|
||||
| Some chap ->
|
||||
return!
|
||||
adminBarePage
|
||||
(if index = -1 then "Add a Chapter" else "Edit Chapter") true
|
||||
(Views.Post.chapterEdit (EditChapterModel.FromChapter post.Id index chap)) next ctx
|
||||
Views.Post.chapterEdit (EditChapterModel.FromChapter post.Id index chap)
|
||||
|> adminBarePage (if index = -1 then "Add a Chapter" else "Edit Chapter") true next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
| Some _ | None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
@ -466,9 +464,8 @@ let saveChapter (postId, index) : HttpHandler = requireAccess Author >=> fun nex
|
|||
do! data.Post.Update updatedPost
|
||||
do! addMessage ctx { UserMessage.Success with Message = "Chapter saved successfully" }
|
||||
return!
|
||||
adminPage
|
||||
"Manage Chapters" true
|
||||
(Views.Post.chapterList form.AddAnother (ManageChaptersModel.Create updatedPost)) next ctx
|
||||
Views.Post.chapterList form.AddAnother (ManageChaptersModel.Create updatedPost)
|
||||
|> adminPage "Manage Chapters" true next ctx
|
||||
with
|
||||
| ex -> return! Error.server ex.Message next ctx
|
||||
else return! Error.notFound next ctx
|
||||
|
@ -491,9 +488,8 @@ let deleteChapter (postId, index) : HttpHandler = requireAccess Author >=> fun n
|
|||
do! data.Post.Update updatedPost
|
||||
do! addMessage ctx { UserMessage.Success with Message = "Chapter deleted successfully" }
|
||||
return!
|
||||
adminPage
|
||||
"Manage Chapters" true (Views.Post.chapterList false (ManageChaptersModel.Create updatedPost)) next
|
||||
ctx
|
||||
Views.Post.chapterList false (ManageChaptersModel.Create updatedPost)
|
||||
|> adminPage "Manage Chapters" true next ctx
|
||||
else return! Error.notFound next ctx
|
||||
| Some _ | None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
|
|
@ -176,17 +176,13 @@ let router : HttpHandler = choose [
|
|||
route "/save" >=> Page.save
|
||||
route "/permalinks" >=> Page.savePermalinks
|
||||
routef "/%s/delete" Page.delete
|
||||
routef "/%s/revision/%s/delete" Page.deleteRevision
|
||||
routef "/%s/revision/%s/restore" Page.restoreRevision
|
||||
routef "/%s/revisions/purge" Page.purgeRevisions
|
||||
])
|
||||
subRoute "/post" (choose [
|
||||
route "/save" >=> Post.save
|
||||
route "/permalinks" >=> Post.savePermalinks
|
||||
routef "/%s/chapter/%i" Post.saveChapter
|
||||
routef "/%s/revision/%s/delete" Post.deleteRevision
|
||||
routef "/%s/revision/%s/restore" Post.restoreRevision
|
||||
routef "/%s/revisions/purge" Post.purgeRevisions
|
||||
])
|
||||
subRoute "/settings" (requireAccess WebLogAdmin >=> choose [
|
||||
route "" >=> Admin.WebLog.saveSettings
|
||||
|
@ -214,9 +210,15 @@ let router : HttpHandler = choose [
|
|||
])
|
||||
]
|
||||
DELETE >=> validateCsrf >=> choose [
|
||||
subRoute "/page" (choose [
|
||||
routef "/%s/revision/%s" Page.deleteRevision
|
||||
routef "/%s/revisions" Page.purgeRevisions
|
||||
])
|
||||
subRoute "/post" (choose [
|
||||
routef "/%s" Post.delete
|
||||
routef "/%s/chapter/%i" Post.deleteChapter
|
||||
routef "/%s" Post.delete
|
||||
routef "/%s/chapter/%i" Post.deleteChapter
|
||||
routef "/%s/revision/%s" Post.deleteRevision
|
||||
routef "/%s/revisions" Post.purgeRevisions
|
||||
])
|
||||
subRoute "/settings" (requireAccess WebLogAdmin >=> choose [
|
||||
routef "/user/%s" User.delete
|
||||
|
|
|
@ -35,7 +35,7 @@ let logOn returnUrl : HttpHandler = fun next ctx ->
|
|||
match returnUrl with
|
||||
| Some _ -> returnUrl
|
||||
| None -> if ctx.Request.Query.ContainsKey "returnUrl" then Some ctx.Request.Query["returnUrl"].[0] else None
|
||||
adminPage "Log On" true (Views.User.logOn { LogOnModel.Empty with ReturnTo = returnTo }) next ctx
|
||||
adminPage "Log On" true next ctx (Views.User.logOn { LogOnModel.Empty with ReturnTo = returnTo })
|
||||
|
||||
|
||||
open System.Security.Claims
|
||||
|
@ -91,12 +91,12 @@ let private goAway : HttpHandler = RequestErrors.BAD_REQUEST "really?"
|
|||
// GET /admin/settings/users
|
||||
let all : HttpHandler = fun next ctx -> task {
|
||||
let! users = ctx.Data.WebLogUser.FindByWebLog ctx.WebLog.Id
|
||||
return! adminBarePage "User Administration" true (Views.User.userList users) next ctx
|
||||
return! adminBarePage "User Administration" true next ctx (Views.User.userList users)
|
||||
}
|
||||
|
||||
/// Show the edit user page
|
||||
let private showEdit (model: EditUserModel) : HttpHandler = fun next ctx ->
|
||||
adminBarePage (if model.IsNew then "Add a New User" else "Edit User") true (Views.User.edit model) next ctx
|
||||
adminBarePage (if model.IsNew then "Add a New User" else "Edit User") true next ctx (Views.User.edit model)
|
||||
|
||||
// GET /admin/settings/user/{id}/edit
|
||||
let edit usrId : HttpHandler = fun next ctx -> task {
|
||||
|
@ -137,7 +137,9 @@ let delete userId : HttpHandler = fun next ctx -> task {
|
|||
let myInfo : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||
match! ctx.Data.WebLogUser.FindById ctx.UserId ctx.WebLog.Id with
|
||||
| Some user ->
|
||||
return! adminPage "Edit Your Information" true (Views.User.myInfo (EditMyInfoModel.FromUser user) user) next ctx
|
||||
return!
|
||||
Views.User.myInfo (EditMyInfoModel.FromUser user) user
|
||||
|> adminPage "Edit Your Information" true next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
|
@ -161,9 +163,8 @@ let saveMyInfo : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
|||
| Some user ->
|
||||
do! addMessage ctx { UserMessage.Error with Message = "Passwords did not match; no updates made" }
|
||||
return!
|
||||
adminPage
|
||||
"Edit Your Information" true
|
||||
(Views.User.myInfo { model with NewPassword = ""; NewPasswordConfirm = "" } user) next ctx
|
||||
Views.User.myInfo { model with NewPassword = ""; NewPasswordConfirm = "" } user
|
||||
|> adminPage "Edit Your Information" true next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
|
|
|
@ -293,3 +293,91 @@ let tagMapList (model: TagMap list) app =
|
|||
]
|
||||
]
|
||||
|> List.singleton
|
||||
|
||||
|
||||
/// Display a list of themes
|
||||
let themeList (model: DisplayTheme list) app =
|
||||
let themeCol = "col-12 col-md-6"
|
||||
let slugCol = "d-none d-md-block col-md-3"
|
||||
let tmplCol = "d-none d-md-block col-md-3"
|
||||
div [ _id "theme_panel" ] [
|
||||
a [ _href (relUrl app "admin/theme/new"); _class "btn btn-primary btn-sm mb-3"; _hxTarget "#theme_new" ] [
|
||||
raw "Upload a New Theme"
|
||||
]
|
||||
div [ _class "container g-0" ] [
|
||||
div [ _class "row mwl-table-heading" ] [
|
||||
div [ _class themeCol ] [ raw "Theme" ]
|
||||
div [ _class slugCol ] [ raw "Slug" ]
|
||||
div [ _class tmplCol ] [ raw "Templates" ]
|
||||
]
|
||||
]
|
||||
div [ _class "row mwl-table-detail"; _id "theme_new" ] []
|
||||
form [ _method "post"; _id "themeList"; _class "container g-0"; _hxTarget "#theme_panel"
|
||||
_hxSwap $"{HxSwap.OuterHtml} show:window:top" ] [
|
||||
antiCsrf app
|
||||
for theme in model do
|
||||
let url = relUrl app $"admin/theme/{theme.Id}"
|
||||
div [ _class "row mwl-table-detail"; _id $"theme_{theme.Id}" ] [
|
||||
div [ _class $"{themeCol} no-wrap" ] [
|
||||
txt theme.Name
|
||||
if theme.IsInUse then span [ _class "badge bg-primary ms-2" ] [ raw "IN USE" ]
|
||||
if not theme.IsOnDisk then
|
||||
span [ _class "badge bg-warning text-dark ms-2" ] [ raw "NOT ON DISK" ]
|
||||
br []
|
||||
small [] [
|
||||
span [ _class "text-muted" ] [ txt $"v{theme.Version}" ]
|
||||
if not (theme.IsInUse || theme.Id = "default") then
|
||||
span [ _class "text-muted" ] [ raw " • " ]
|
||||
a [ _href url; _hxDelete url; _class "text-danger"
|
||||
_hxConfirm $"Are you sure you want to delete the theme “{theme.Name}”? This action cannot be undone." ] [
|
||||
raw "Delete"
|
||||
]
|
||||
span [ _class "d-md-none text-muted" ] [
|
||||
br []; raw "Slug: "; txt theme.Id; raw $" • {theme.TemplateCount} Templates"
|
||||
]
|
||||
]
|
||||
]
|
||||
div [ _class slugCol ] [ txt (string theme.Id) ]
|
||||
div [ _class tmplCol ] [ txt (string theme.TemplateCount) ]
|
||||
]
|
||||
]
|
||||
]
|
||||
|> List.singleton
|
||||
|
||||
|
||||
/// Form to allow a theme to be uploaded
|
||||
let themeUpload app =
|
||||
div [ _class "col" ] [
|
||||
h5 [ _class "mt-2" ] [ raw app.PageTitle ]
|
||||
form [ _action (relUrl app "admin/theme/new"); _method "post"; _class "container"
|
||||
_enctype "multipart/form-data"; _hxNoBoost ] [
|
||||
antiCsrf app
|
||||
div [ _class "row " ] [
|
||||
div [ _class "col-12 col-sm-6 pb-3" ] [
|
||||
div [ _class "form-floating" ] [
|
||||
input [ _type "file"; _id "file"; _name "file"; _class "form-control"; _accept ".zip"
|
||||
_placeholder "Theme File"; _required ]
|
||||
label [ _for "file" ] [ raw "Theme File" ]
|
||||
]
|
||||
]
|
||||
div [ _class "col-12 col-sm-6 pb-3 d-flex justify-content-center align-items-center" ] [
|
||||
div [ _class "form-check form-switch pb-2" ] [
|
||||
input [ _type "checkbox"; _name "DoOverwrite"; _id "doOverwrite"; _class "form-check-input"
|
||||
_value "true" ]
|
||||
label [ _for "doOverwrite"; _class "form-check-label" ] [ raw "Overwrite" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
div [ _class "row pb-3" ] [
|
||||
div [ _class "col text-center" ] [
|
||||
button [ _type "submit"; _class "btn btn-sm btn-primary" ] [ raw "Upload Theme" ]; raw " "
|
||||
button [ _type "button"; _class "btn btn-sm btn-secondary ms-3"
|
||||
_onclick "document.getElementById('theme_new').innerHTML = ''" ] [
|
||||
raw "Cancel"
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|> List.singleton
|
||||
|
|
@ -232,3 +232,139 @@ module Layout =
|
|||
title [] []
|
||||
yield! content app
|
||||
]
|
||||
|
||||
|
||||
// ~~ SHARED TEMPLATES BETWEEN POSTS AND PAGES
|
||||
open Giraffe.Htmx.Common
|
||||
|
||||
/// The round-trip instant pattern
|
||||
let roundTrip = InstantPattern.CreateWithInvariantCulture "uuuu'-'MM'-'dd'T'HH':'mm':'ss'.'fffffff"
|
||||
|
||||
/// Capitalize the first letter in the given string
|
||||
let private capitalize (it: string) =
|
||||
$"{(string it[0]).ToUpper()}{it[1..]}"
|
||||
|
||||
/// Form to manage permalinks for pages or posts
|
||||
let managePermalinks (model: ManagePermalinksModel) app = [
|
||||
let baseUrl = relUrl app $"admin/{model.Entity}/"
|
||||
let linkDetail idx link =
|
||||
div [ _id $"link_%i{idx}"; _class "row mb-3" ] [
|
||||
div [ _class "col-1 text-center align-self-center" ] [
|
||||
button [ _type "button"; _class "btn btn-sm btn-danger"
|
||||
_onclick $"Admin.removePermalink({idx})" ] [
|
||||
raw "−"
|
||||
]
|
||||
]
|
||||
div [ _class "col-11" ] [
|
||||
div [ _class "form-floating" ] [
|
||||
input [ _type "text"; _name "Prior"; _id $"prior_{idx}"; _class "form-control"; _placeholder "Link"
|
||||
_value link ]
|
||||
label [ _for $"prior_{idx}" ] [ raw "Link" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
h2 [ _class "my-3" ] [ raw app.PageTitle ]
|
||||
article [] [
|
||||
form [ _action $"{baseUrl}permalinks"; _method "post"; _class "container" ] [
|
||||
antiCsrf app
|
||||
input [ _type "hidden"; _name "Id"; _value model.Id ]
|
||||
div [ _class "row" ] [
|
||||
div [ _class "col" ] [
|
||||
p [ _style "line-height:1.2rem;" ] [
|
||||
strong [] [ txt model.CurrentTitle ]; br []
|
||||
small [ _class "text-muted" ] [
|
||||
span [ _class "fst-italic" ] [ txt model.CurrentPermalink ]; br []
|
||||
a [ _href $"{baseUrl}{model.Id}/edit" ] [
|
||||
raw $"« Back to Edit {capitalize model.Entity}"
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
div [ _class "row mb-3" ] [
|
||||
div [ _class "col" ] [
|
||||
button [ _type "button"; _class "btn btn-sm btn-secondary"; _onclick "Admin.addPermalink()" ] [
|
||||
raw "Add a Permalink"
|
||||
]
|
||||
]
|
||||
]
|
||||
div [ _class "row mb-3" ] [
|
||||
div [ _class "col" ] [
|
||||
div [ _id "permalinks"; _class "container g-0" ] [
|
||||
yield! Array.mapi linkDetail model.Prior
|
||||
script [] [
|
||||
raw """document.addEventListener(\"DOMContentLoaded\", """
|
||||
raw $"() => Admin.setPermalinkIndex({model.Prior.Length}))"
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
div [ _class "row pb-3" ] [
|
||||
div [ _class "col " ] [
|
||||
button [ _type "submit"; _class "btn btn-primary" ] [ raw "Save Changes" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
/// Form to manage revisions for pages or posts
|
||||
let manageRevisions (model: ManageRevisionsModel) app = [
|
||||
let revUrlBase = relUrl app $"admin/{model.Entity}/{model.Id}/revision"
|
||||
let revDetail idx (rev: Revision) =
|
||||
let asOfString = roundTrip.Format rev.AsOf
|
||||
let asOfId = $"""rev_{asOfString.Replace(".", "_").Replace(":", "-")}"""
|
||||
div [ _id asOfId; _class "row pb-3 mwl-table-detail" ] [
|
||||
div [ _class "col-12 mb-1" ] [
|
||||
longDate app rev.AsOf; raw " at "; shortTime app rev.AsOf; raw " "
|
||||
span [ _class "badge bg-secondary text-uppercase ms-2" ] [ txt (string rev.Text.SourceType) ]
|
||||
if idx = 0 then span [ _class "badge bg-primary text-uppercase ms-2" ] [ raw "Current Revision" ]
|
||||
br []
|
||||
if idx > 0 then
|
||||
let revUrlPrefix = $"{revUrlBase}/{asOfString}"
|
||||
let revRestore = $"{revUrlPrefix}/restore"
|
||||
small [] [
|
||||
a [ _href $"{revUrlPrefix}/preview"; _hxTarget $"#{asOfId}_preview" ] [ raw "Preview" ]
|
||||
span [ _class "text-muted" ] [ raw " • " ]
|
||||
a [ _href revRestore; _hxPost revRestore ] [ raw "Restore as Current" ]
|
||||
span [ _class "text-muted" ] [ raw " • " ]
|
||||
a [ _href revUrlPrefix; _hxDelete revUrlPrefix; _hxTarget $"#{asOfId}"
|
||||
_hxSwap HxSwap.OuterHtml; _class "text-danger" ] [
|
||||
raw "Delete"
|
||||
]
|
||||
]
|
||||
]
|
||||
if idx > 0 then div [ _id $"{asOfId}_preview"; _class "col-12" ] []
|
||||
]
|
||||
|
||||
h2 [ _class "my-3" ] [ raw app.PageTitle ]
|
||||
article [] [
|
||||
form [ _method "post"; _hxTarget "body"; _class "container mb-3" ] [
|
||||
antiCsrf app
|
||||
input [ _type "hidden"; _name "Id"; _value model.Id ]
|
||||
div [ _class "row" ] [
|
||||
div [ _class "col" ] [
|
||||
p [ _style "line-height:1.2rem;" ] [
|
||||
strong [] [ txt model.CurrentTitle ]; br []
|
||||
small [ _class "text-muted" ] [
|
||||
a [ _href (relUrl app $"admin/{model.Entity}/{model.Id}/edit") ] [
|
||||
raw $"« Back to Edit {(string model.Entity[0]).ToUpper()}{model.Entity[1..]}"
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
if model.Revisions.Length > 1 then
|
||||
div [ _class "row mb-3" ] [
|
||||
div [ _class "col" ] [
|
||||
button [ _type "button"; _class "btn btn-sm btn-danger"; _hxDelete $"{revUrlBase}s/purge"
|
||||
_hxConfirm "This will remove all revisions but the current one; are you sure this is what you wish to do?" ] [
|
||||
raw "Delete All Prior Revisions"
|
||||
]
|
||||
]
|
||||
]
|
||||
div [ _class "row mwl-table-heading" ] [ div [ _class "col" ] [ raw "Revision" ] ]
|
||||
yield! List.mapi revDetail model.Revisions
|
||||
]
|
||||
]
|
||||
]
|
||||
|
|
|
@ -13,7 +13,7 @@ let edit (model: EditUserModel) app =
|
|||
div [ _class "col-12" ] [
|
||||
h5 [ _class "my-3" ] [ txt app.PageTitle ]
|
||||
form [ _hxPost (relUrl app "admin/settings/user/save"); _method "post"; _class "container"
|
||||
_hxTarget "#userList"; _hxSwap $"{HxSwap.OuterHtml} show:window:top" ] [
|
||||
_hxTarget "#user_panel"; _hxSwap $"{HxSwap.OuterHtml} show:window:top" ] [
|
||||
antiCsrf app
|
||||
input [ _type "hidden"; _name "Id"; _value model.Id ]
|
||||
div [ _class "row" ] [
|
||||
|
@ -163,56 +163,77 @@ let logOn (model: LogOnModel) (app: AppViewContext) = [
|
|||
|
||||
/// The list of users for a web log (part of web log settings page)
|
||||
let userList (model: WebLogUser list) app =
|
||||
let badge = "ms-2 badge bg"
|
||||
div [ _id "userList" ] [
|
||||
div [ _class "container g-0" ] [
|
||||
div [ _class "row mwl-table-detail"; _id "user_new" ] []
|
||||
]
|
||||
form [ _method "post"; _class "container g-0"; _hxTarget "this"
|
||||
_hxSwap $"{HxSwap.OuterHtml} show:window:top" ] [
|
||||
antiCsrf app
|
||||
for user in model do
|
||||
div [ _class "row mwl-table-detail"; _id $"user_{user.Id}" ] [
|
||||
div [ _class "col-12 col-md-4 col-xl-3 no-wrap" ] [
|
||||
txt user.PreferredName; raw " "
|
||||
match user.AccessLevel with
|
||||
| Administrator -> span [ _class $"{badge}-success" ] [ raw "ADMINISTRATOR" ]
|
||||
| WebLogAdmin -> span [ _class $"{badge}-primary" ] [ raw "WEB LOG ADMIN" ]
|
||||
| Editor -> span [ _class $"{badge}-secondary" ] [ raw "EDITOR" ]
|
||||
| Author -> span [ _class $"{badge}-dark" ] [ raw "AUTHOR" ]
|
||||
br []
|
||||
if app.IsAdministrator || (app.IsWebLogAdmin && not (user.AccessLevel = Administrator)) then
|
||||
let userUrl = relUrl app $"admin/settings/user/{user.Id}"
|
||||
small [] [
|
||||
a [ _href $"{userUrl}/edit"; _hxTarget $"#user_{user.Id}"
|
||||
_hxSwap $"{HxSwap.InnerHtml} show:#user_{user.Id}:top" ] [
|
||||
raw "Edit"
|
||||
]
|
||||
if app.UserId.Value <> user.Id then
|
||||
span [ _class "text-muted" ] [ raw " • " ]
|
||||
a [ _href userUrl; _hxDelete userUrl; _class "text-danger"
|
||||
_hxConfirm $"Are you sure you want to delete the user “{user.PreferredName}”? This action cannot be undone. (This action will not succeed if the user has authored any posts or pages.)" ] [
|
||||
raw "Delete"
|
||||
]
|
||||
let userCol = "col-12 col-md-4 col-xl-3"
|
||||
let emailCol = "col-12 col-md-4 col-xl-4"
|
||||
let cre8Col = "d-none d-xl-block col-xl-2"
|
||||
let lastCol = "col-12 col-md-4 col-xl-3"
|
||||
let badge = "ms-2 badge bg"
|
||||
let userDetail (user: WebLogUser) =
|
||||
div [ _class "row mwl-table-detail"; _id $"user_{user.Id}" ] [
|
||||
div [ _class $"{userCol} no-wrap" ] [
|
||||
txt user.PreferredName; raw " "
|
||||
match user.AccessLevel with
|
||||
| Administrator -> span [ _class $"{badge}-success" ] [ raw "ADMINISTRATOR" ]
|
||||
| WebLogAdmin -> span [ _class $"{badge}-primary" ] [ raw "WEB LOG ADMIN" ]
|
||||
| Editor -> span [ _class $"{badge}-secondary" ] [ raw "EDITOR" ]
|
||||
| Author -> span [ _class $"{badge}-dark" ] [ raw "AUTHOR" ]
|
||||
br []
|
||||
if app.IsAdministrator || (app.IsWebLogAdmin && not (user.AccessLevel = Administrator)) then
|
||||
let userUrl = relUrl app $"admin/settings/user/{user.Id}"
|
||||
small [] [
|
||||
a [ _href $"{userUrl}/edit"; _hxTarget $"#user_{user.Id}"
|
||||
_hxSwap $"{HxSwap.InnerHtml} show:#user_{user.Id}:top" ] [
|
||||
raw "Edit"
|
||||
]
|
||||
if app.UserId.Value <> user.Id then
|
||||
span [ _class "text-muted" ] [ raw " • " ]
|
||||
a [ _href userUrl; _hxDelete userUrl; _class "text-danger"
|
||||
_hxConfirm $"Are you sure you want to delete the user “{user.PreferredName}”? This action cannot be undone. (This action will not succeed if the user has authored any posts or pages.)" ] [
|
||||
raw "Delete"
|
||||
]
|
||||
]
|
||||
div [ _class "col-12 col-md-4 col-xl-4" ] [
|
||||
txt $"{user.FirstName} {user.LastName}"; br []
|
||||
small [ _class "text-muted" ] [
|
||||
txt user.Email
|
||||
if Option.isSome user.Url then
|
||||
br []; txt user.Url.Value
|
||||
]
|
||||
]
|
||||
div [ _class "d-none d-xl-block col-xl-2" ] [
|
||||
if user.CreatedOn = Noda.epoch then raw "N/A" else longDate app user.CreatedOn
|
||||
]
|
||||
div [ _class "col-12 col-md-4 col-xl-3" ] [
|
||||
match user.LastSeenOn with
|
||||
| Some it -> longDate app it; raw " at "; shortTime app it
|
||||
| None -> raw "--"
|
||||
]
|
||||
]
|
||||
div [ _class emailCol ] [
|
||||
txt $"{user.FirstName} {user.LastName}"; br []
|
||||
small [ _class "text-muted" ] [
|
||||
txt user.Email
|
||||
if Option.isSome user.Url then
|
||||
br []; txt user.Url.Value
|
||||
]
|
||||
]
|
||||
div [ _class "d-none d-xl-block col-xl-2" ] [
|
||||
if user.CreatedOn = Noda.epoch then raw "N/A" else longDate app user.CreatedOn
|
||||
]
|
||||
div [ _class "col-12 col-md-4 col-xl-3" ] [
|
||||
match user.LastSeenOn with
|
||||
| Some it -> longDate app it; raw " at "; shortTime app it
|
||||
| None -> raw "--"
|
||||
]
|
||||
]
|
||||
div [ _id "user_panel" ] [
|
||||
a [ _href (relUrl app "admin/settings/user/new/edit"); _class "btn btn-primary btn-sm mb-3"
|
||||
_hxTarget "#user_new" ] [
|
||||
raw "Add a New User"
|
||||
]
|
||||
div [ _class "container g-0" ] [
|
||||
div [ _class "row mwl-table-heading" ] [
|
||||
div [ _class userCol ] [
|
||||
raw "User"; span [ _class "d-md-none" ] [ raw "; Full Name / E-mail; Last Log On" ]
|
||||
]
|
||||
div [ _class $"{emailCol} d-none d-md-inline-block" ] [ raw "Full Name / E-mail" ]
|
||||
div [ _class cre8Col ] [ raw "Created" ]
|
||||
div [ _class $"{lastCol} d-none d-md-block" ] [ raw "Last Log On" ]
|
||||
]
|
||||
]
|
||||
div [ _id "userList" ] [
|
||||
div [ _class "container g-0" ] [
|
||||
div [ _class "row mwl-table-detail"; _id "user_new" ] []
|
||||
]
|
||||
form [ _method "post"; _class "container g-0"; _hxTarget "#user_panel"
|
||||
_hxSwap $"{HxSwap.OuterHtml} show:window:top" ] [
|
||||
antiCsrf app
|
||||
yield! List.map userDetail model
|
||||
]
|
||||
]
|
||||
]
|
||||
|> List.singleton
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
{%- assign theme_col = "col-12 col-md-6" -%}
|
||||
{%- assign slug_col = "d-none d-md-block col-md-3" -%}
|
||||
{%- assign tmpl_col = "d-none d-md-block col-md-3" -%}
|
|
@ -1,4 +0,0 @@
|
|||
{%- assign user_col = "col-12 col-md-4 col-xl-3" -%}
|
||||
{%- assign email_col = "col-12 col-md-4 col-xl-4" -%}
|
||||
{%- assign cre8_col = "d-none d-xl-block col-xl-2" -%}
|
||||
{%- assign last_col = "col-12 col-md-4 col-xl-3" -%}
|
|
@ -2,19 +2,7 @@
|
|||
<article>
|
||||
<fieldset class="container mb-3 pb-0">
|
||||
<legend>Themes</legend>
|
||||
<a href="{{ "admin/theme/new" | relative_link }}" class="btn btn-primary btn-sm mb-3" hx-target=#theme_new>
|
||||
Upload a New Theme
|
||||
</a>
|
||||
<div class="container g-0">
|
||||
{% include_template "_theme-list-columns" %}
|
||||
<div class="row mwl-table-heading">
|
||||
<div class="{{ theme_col }}">Theme</div>
|
||||
<div class="{{ slug_col }} d-none d-md-inline-block">Slug</div>
|
||||
<div class="{{ tmpl_col }} d-none d-md-inline-block">Templates</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mwl-table-detail" id="theme_new"></div>
|
||||
{{ theme_list }}
|
||||
<span hx-get="{{ "admin/theme/list" | relative_link }}" hx-trigger="load" hx-swap="outerHTML"></span>
|
||||
</fieldset>
|
||||
<fieldset class="container mb-3 pb-0">
|
||||
{%- assign cache_base_url = "admin/cache/" -%}
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
<h2 class=my-3>{{ page_title }}</h2>
|
||||
<article>
|
||||
{%- assign base_url = "admin/" | append: model.entity | append: "/" -%}
|
||||
<form action="{{ base_url | append: "permalinks" | relative_link }}" method=post>
|
||||
<input type=hidden name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
||||
<input type=hidden name=Id value="{{ model.id }}">
|
||||
<div class=container>
|
||||
<div class=row>
|
||||
<div class=col>
|
||||
<p style="line-height:1.2rem;">
|
||||
<strong>{{ model.current_title }}</strong><br>
|
||||
<small class=text-muted>
|
||||
<span class=fst-italic>{{ model.current_permalink }}</span><br>
|
||||
<a href="{{ base_url | append: model.id | append: "/edit" | relative_link }}">
|
||||
« Back to Edit {{ model.entity | capitalize }}
|
||||
</a>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class=col>
|
||||
<button type=button class="btn btn-sm btn-secondary" onclick="Admin.addPermalink()">Add a Permalink</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class=col>
|
||||
<div id=permalinks class=container>
|
||||
{%- assign link_count = 0 -%}
|
||||
{%- for link in model.prior %}
|
||||
<div id="link_{{ link_count }}" class="row mb-3">
|
||||
<div class="col-1 text-center align-self-center">
|
||||
<button type=button class="btn btn-sm btn-danger" onclick="Admin.removePermalink({{ link_count }})">
|
||||
−
|
||||
</button>
|
||||
</div>
|
||||
<div class=col-11>
|
||||
<div class=form-floating>
|
||||
<input type=text name=Prior id="prior_{{ link_count }}" class=form-control placeholder=Link
|
||||
value="{{ link }}">
|
||||
<label for="prior_{{ link_count }}">Link</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{%- assign link_count = link_count | plus: 1 -%}
|
||||
{% endfor -%}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => Admin.setPermalinkIndex({{ link_count }}))
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row pb-3">
|
||||
<div class=col>
|
||||
<button type=submit class="btn btn-primary">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
|
@ -1,65 +0,0 @@
|
|||
<h2 class=my-3>{{ page_title }}</h2>
|
||||
<article>
|
||||
<form method=post hx-target=body>
|
||||
<input type=hidden name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
||||
<input type=hidden name=Id value="{{ model.id }}">
|
||||
<div class="container mb-3">
|
||||
<div class=row>
|
||||
<div class=col>
|
||||
<p style="line-height:1.2rem;">
|
||||
<strong>{{ model.current_title }}</strong><br>
|
||||
<small class=text-muted>
|
||||
<a href="{{ "admin/" | append: model.entity | append: "/" | append: model.id | append: "/edit" | relative_link }}">
|
||||
« Back to Edit {{ model.entity | capitalize }}
|
||||
</a>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
{%- assign revision_count = model.revisions | size -%}
|
||||
{%- assign rev_url_base = "admin/" | append: model.entity | append: "/" | append: model.id | append: "/revision" -%}
|
||||
{%- if revision_count > 1 %}
|
||||
<div class="row mb-3">
|
||||
<div class=col>
|
||||
<button type=button class="btn btn-sm btn-danger"
|
||||
hx-post="{{ rev_url_base | append: "s/purge" | relative_link }}"
|
||||
hx-confirm="This will remove all revisions but the current one; are you sure this is what you wish to do?">
|
||||
Delete All Prior Revisions
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{%- endif %}
|
||||
<div class="row mwl-table-heading"><div class=col>Revision</div></div>
|
||||
{% for rev in model.revisions %}
|
||||
{%- assign as_of_string = rev.as_of | date: "o" -%}
|
||||
{%- assign as_of_id = "rev_" | append: as_of_string | replace: "\.", "_" | replace: ":", "-" -%}
|
||||
<div id="{{ as_of_id }}" class="row pb-3 mwl-table-detail">
|
||||
<div class="col-12 mb-1">
|
||||
{{ 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>
|
||||
{%- if forloop.first %}
|
||||
<span class="badge bg-primary text-uppercase ms-2">Current Revision</span>
|
||||
{%- endif %}<br>
|
||||
{% unless forloop.first %}
|
||||
{%- assign rev_url_prefix = rev_url_base | append: "/" | append: as_of_string -%}
|
||||
{%- assign rev_restore = rev_url_prefix | append: "/restore" | relative_link -%}
|
||||
{%- assign rev_delete = rev_url_prefix | append: "/delete" | relative_link -%}
|
||||
<small>
|
||||
<a href="{{ rev_url_prefix | append: "/preview" | relative_link }}" hx-target="#{{ as_of_id }}_preview">
|
||||
Preview
|
||||
</a>
|
||||
<span class=text-muted> • </span>
|
||||
<a href="{{ rev_restore }}" hx-post="{{ rev_restore }}">Restore as Current</a>
|
||||
<span class=text-muted> • </span>
|
||||
<a href="{{ rev_delete }}" hx-post="{{ rev_delete }}" hx-target="#{{ as_of_id }}" hx-swap=outerHTML
|
||||
class=text-danger>
|
||||
Delete
|
||||
</a>
|
||||
</small>
|
||||
{% endunless %}
|
||||
</div>
|
||||
{% unless forloop.first %}<div id="{{ as_of_id }}_preview" class=col-12></div>{% endunless %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
|
@ -109,19 +109,6 @@
|
|||
</fieldset>
|
||||
<fieldset id=users class="container mb-3 pb-0">
|
||||
<legend>Users</legend>
|
||||
{% include_template "_user-list-columns" %}
|
||||
<a href="{{ "admin/settings/user/new/edit" | relative_link }}" class="btn btn-primary btn-sm mb-3"
|
||||
hx-target=#user_new>
|
||||
Add a New User
|
||||
</a>
|
||||
<div class="container g-0">
|
||||
<div class="row mwl-table-heading">
|
||||
<div class="{{ user_col }}">User<span class=d-md-none>; Full Name / E-mail; Last Log On</span></div>
|
||||
<div class="{{ email_col }} d-none d-md-inline-block">Full Name / E-mail</div>
|
||||
<div class="{{ cre8_col }}">Created</div>
|
||||
<div class="{{ last_col }} d-none d-md-block">Last Log On</div>
|
||||
</div>
|
||||
</div>
|
||||
<span hx-get="{{ "admin/settings/users" | relative_link }}" hx-trigger="load" hx-swap="outerHTML"></span>
|
||||
</fieldset>
|
||||
<fieldset id=rss-settings class="container mb-3 pb-0">
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
<form method=post id=themeList class="container g-0" hx-target=this hx-swap="outerHTML show:window:top">
|
||||
<input type=hidden name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
||||
{% include_template "_theme-list-columns" %}
|
||||
{% for theme in themes -%}
|
||||
<div class="row mwl-table-detail" id="theme_{{ theme.id }}">
|
||||
<div class="{{ theme_col }} no-wrap">
|
||||
{{ theme.name }}
|
||||
{%- if theme.is_in_use %}
|
||||
<span class="badge bg-primary ms-2">IN USE</span>
|
||||
{%- endif %}
|
||||
{%- unless theme.is_on_disk %}
|
||||
<span class="badge bg-warning text-dark ms-2">NOT ON DISK</span>
|
||||
{%- endunless %}<br>
|
||||
<small>
|
||||
<span class=text-muted>v{{ theme.version }}</span>
|
||||
{% unless theme.is_in_use or theme.id == "default" %}
|
||||
<span class=text-muted> • </span>
|
||||
{%- assign theme_del_link = "admin/theme/" | append: theme.id | append: "/delete" | relative_link -%}
|
||||
<a href="{{ theme_del_link }}" hx-post="{{ theme_del_link }}" class=text-danger
|
||||
hx-confirm="Are you sure you want to delete the theme “{{ theme.name }}”? This action cannot be undone.">
|
||||
Delete
|
||||
</a>
|
||||
{% endunless %}
|
||||
<span class="d-md-none text-muted">
|
||||
<br>Slug: {{ theme.id }} • {{ theme.template_count }} Templates
|
||||
</span>
|
||||
</small>
|
||||
</div>
|
||||
<div class="{{ slug_col }}">{{ theme.id }}</div>
|
||||
<div class="{{ tmpl_col }}">{{ theme.template_count }}</div>
|
||||
</div>
|
||||
{%- endfor %}
|
||||
</form>
|
|
@ -1,30 +0,0 @@
|
|||
<div class=col>
|
||||
<h5 class=mt-2>{{ page_title }}</h5>
|
||||
<form action="{{ "admin/theme/new" | relative_link }}" method=post class=container enctype=multipart/form-data
|
||||
hx-boost=false>
|
||||
<input type=hidden name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
||||
<div class=row>
|
||||
<div class="col-12 col-sm-6 pb-3">
|
||||
<div class=form-floating>
|
||||
<input type=file id=file name=file class=form-control accept=.zip placeholder="Theme File" required>
|
||||
<label for=file>Theme File</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 pb-3 d-flex justify-content-center align-items-center">
|
||||
<div class="form-check form-switch pb-2">
|
||||
<input type=checkbox name=DoOverwrite id=doOverwrite class=form-check-input value=true>
|
||||
<label for=doOverwrite class=form-check-label>Overwrite</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row pb-3">
|
||||
<div class="col text-center">
|
||||
<button type=submit class="btn btn-sm btn-primary">Upload Theme</button>
|
||||
<button type=button class="btn btn-sm btn-secondary ms-3"
|
||||
onclick="document.getElementById('theme_new').innerHTML = ''">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
Loading…
Reference in New Issue
Block a user