Migrate page edit to GVE
This commit is contained in:
parent
ddab491dfc
commit
ec04fea86c
@ -626,88 +626,65 @@ type EditCommonModel() =
|
|||||||
/// Whether to provide a link to manage chapters
|
/// Whether to provide a link to manage chapters
|
||||||
member val IncludeChapterLink = false with get, set
|
member val IncludeChapterLink = false with get, set
|
||||||
|
|
||||||
|
/// The template to use to display the page
|
||||||
|
member val Template = "" with get, set
|
||||||
|
|
||||||
/// The source type ("HTML" or "Markdown")
|
/// The source type ("HTML" or "Markdown")
|
||||||
member val Source = "" with get, set
|
member val Source = "" with get, set
|
||||||
|
|
||||||
/// The text of the page or post
|
/// The text of the page or post
|
||||||
member val Text = "" with get, set
|
member val Text = "" with get, set
|
||||||
|
|
||||||
|
/// Names of metadata items
|
||||||
|
member val MetaNames: string array = [||] with get, set
|
||||||
|
|
||||||
|
/// Values of metadata items
|
||||||
|
member val MetaValues: string array = [||] with get, set
|
||||||
|
|
||||||
/// Whether this is a new page or post
|
/// Whether this is a new page or post
|
||||||
member this.IsNew with get () = this.Id = "new"
|
member this.IsNew with get () = this.Id = "new"
|
||||||
|
|
||||||
/// Fill the properties of this object from a page
|
/// Fill the properties of this object from a page
|
||||||
member this.FromPage (page: Page) =
|
member this.PopulateFromPage (page: Page) =
|
||||||
let latest = findLatestRevision page.Revisions
|
let latest = findLatestRevision page.Revisions
|
||||||
this.Id <- string page.Id
|
this.Id <- string page.Id
|
||||||
this.Title <- page.Title
|
this.Title <- page.Title
|
||||||
this.Permalink <- string page.Permalink
|
this.Permalink <- string page.Permalink
|
||||||
this.Entity <- "page"
|
this.Entity <- "page"
|
||||||
this.Source <- latest.Text.SourceType
|
this.Template <- defaultArg page.Template ""
|
||||||
this.Text <- latest.Text.Text
|
this.Source <- latest.Text.SourceType
|
||||||
|
this.Text <- latest.Text.Text
|
||||||
|
this.MetaNames <- page.Metadata |> List.map _.Name |> Array.ofList
|
||||||
|
this.MetaValues <- page.Metadata |> List.map _.Value |> Array.ofList
|
||||||
|
|
||||||
/// Fill the properties of this object from a post
|
/// Fill the properties of this object from a post
|
||||||
member this.FromPost (post: Post) =
|
member this.PopulateFromPost (post: Post) =
|
||||||
let latest = findLatestRevision post.Revisions
|
let latest = findLatestRevision post.Revisions
|
||||||
this.Id <- string post.Id
|
this.Id <- string post.Id
|
||||||
this.Title <- post.Title
|
this.Title <- post.Title
|
||||||
this.Permalink <- string post.Permalink
|
this.Permalink <- string post.Permalink
|
||||||
this.Entity <- "post"
|
this.Entity <- "post"
|
||||||
this.IncludeChapterLink <- Option.isSome post.Episode && Option.isSome post.Episode.Value.Chapters
|
this.IncludeChapterLink <- Option.isSome post.Episode && Option.isSome post.Episode.Value.Chapters
|
||||||
|
this.Template <- defaultArg post.Template ""
|
||||||
this.Source <- latest.Text.SourceType
|
this.Source <- latest.Text.SourceType
|
||||||
this.Text <- latest.Text.Text
|
this.Text <- latest.Text.Text
|
||||||
|
this.MetaNames <- post.Metadata |> List.map _.Name |> Array.ofList
|
||||||
|
this.MetaValues <- post.Metadata |> List.map _.Value |> Array.ofList
|
||||||
|
|
||||||
|
|
||||||
/// View model to edit a page
|
/// View model to edit a page
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
type EditPageModel() =
|
||||||
type EditPageModel = {
|
inherit EditCommonModel()
|
||||||
/// The ID of the page being edited
|
|
||||||
PageId: string
|
|
||||||
|
|
||||||
/// The title of the page
|
|
||||||
Title: string
|
|
||||||
|
|
||||||
/// The permalink for the page
|
|
||||||
Permalink: string
|
|
||||||
|
|
||||||
/// The template to use to display the page
|
|
||||||
Template: string
|
|
||||||
|
|
||||||
/// Whether this page is shown in the page list
|
/// Whether this page is shown in the page list
|
||||||
IsShownInPageList: bool
|
member val IsShownInPageList = false with get, set
|
||||||
|
|
||||||
/// The source format for the text
|
|
||||||
Source: string
|
|
||||||
|
|
||||||
/// The text of the page
|
|
||||||
Text: string
|
|
||||||
|
|
||||||
/// Names of metadata items
|
|
||||||
MetaNames: string array
|
|
||||||
|
|
||||||
/// Values of metadata items
|
|
||||||
MetaValues: string array
|
|
||||||
} with
|
|
||||||
|
|
||||||
/// Create an edit model from an existing page
|
/// Create an edit model from an existing page
|
||||||
static member FromPage (page: Page) =
|
static member FromPage(page: Page) =
|
||||||
let latest =
|
let model = EditPageModel()
|
||||||
match page.Revisions |> List.sortByDescending _.AsOf |> List.tryHead with
|
model.PopulateFromPage page
|
||||||
| Some rev -> rev
|
model.IsShownInPageList <- page.IsInPageList
|
||||||
| None -> Revision.Empty
|
model
|
||||||
let page = if page.Metadata |> List.isEmpty then { page with Metadata = [ MetaItem.Empty ] } else page
|
|
||||||
{ PageId = string page.Id
|
|
||||||
Title = page.Title
|
|
||||||
Permalink = string page.Permalink
|
|
||||||
Template = defaultArg page.Template ""
|
|
||||||
IsShownInPageList = page.IsInPageList
|
|
||||||
Source = latest.Text.SourceType
|
|
||||||
Text = latest.Text.Text
|
|
||||||
MetaNames = page.Metadata |> List.map _.Name |> Array.ofList
|
|
||||||
MetaValues = page.Metadata |> List.map _.Value |> Array.ofList }
|
|
||||||
|
|
||||||
/// Whether this is a new page
|
|
||||||
member this.IsNew =
|
|
||||||
this.PageId = "new"
|
|
||||||
|
|
||||||
/// Update a page with values from this model
|
/// Update a page with values from this model
|
||||||
member this.UpdatePage (page: Page) now =
|
member this.UpdatePage (page: Page) now =
|
||||||
@ -737,163 +714,123 @@ type EditPageModel = {
|
|||||||
|
|
||||||
|
|
||||||
/// View model to edit a post
|
/// View model to edit a post
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
type EditPostModel() =
|
||||||
type EditPostModel = {
|
inherit EditCommonModel()
|
||||||
/// The ID of the post being edited
|
|
||||||
PostId: string
|
|
||||||
|
|
||||||
/// The title of the post
|
|
||||||
Title: string
|
|
||||||
|
|
||||||
/// The permalink for the post
|
|
||||||
Permalink: string
|
|
||||||
|
|
||||||
/// The source format for the text
|
|
||||||
Source: string
|
|
||||||
|
|
||||||
/// The text of the post
|
|
||||||
Text: string
|
|
||||||
|
|
||||||
/// The tags for the post
|
/// The tags for the post
|
||||||
Tags: string
|
member val Tags = "" with get, set
|
||||||
|
|
||||||
/// The template used to display the post
|
|
||||||
Template: string
|
|
||||||
|
|
||||||
/// The category IDs for the post
|
/// The category IDs for the post
|
||||||
CategoryIds: string array
|
member val CategoryIds: string array = [||] with get, set
|
||||||
|
|
||||||
/// The post status
|
/// The post status
|
||||||
Status: string
|
member val Status = "" with get, set
|
||||||
|
|
||||||
/// Whether this post should be published
|
/// Whether this post should be published
|
||||||
DoPublish: bool
|
member val DoPublish = false with get, set
|
||||||
|
|
||||||
/// Names of metadata items
|
|
||||||
MetaNames: string array
|
|
||||||
|
|
||||||
/// Values of metadata items
|
|
||||||
MetaValues: string array
|
|
||||||
|
|
||||||
/// Whether to override the published date/time
|
/// Whether to override the published date/time
|
||||||
SetPublished: bool
|
member val SetPublished = false with get, set
|
||||||
|
|
||||||
/// The published date/time to override
|
/// The published date/time to override
|
||||||
PubOverride: Nullable<DateTime>
|
member val PubOverride = Nullable<DateTime>() with get, set
|
||||||
|
|
||||||
/// Whether all revisions should be purged and the override date set as the updated date as well
|
/// Whether all revisions should be purged and the override date set as the updated date as well
|
||||||
SetUpdated: bool
|
member val SetUpdated = false with get, set
|
||||||
|
|
||||||
/// Whether this post has a podcast episode
|
/// Whether this post has a podcast episode
|
||||||
IsEpisode: bool
|
member val IsEpisode = false with get, set
|
||||||
|
|
||||||
/// The URL for the media for this episode (may be permalink)
|
/// The URL for the media for this episode (may be permalink)
|
||||||
Media: string
|
member val Media = "" with get, set
|
||||||
|
|
||||||
/// The size (in bytes) of the media for this episode
|
/// The size (in bytes) of the media for this episode
|
||||||
Length: int64
|
member val Length = 0L with get, set
|
||||||
|
|
||||||
/// The duration of the media for this episode
|
/// The duration of the media for this episode
|
||||||
Duration: string
|
member val Duration = "" with get, set
|
||||||
|
|
||||||
/// The media type (optional, defaults to podcast-defined media type)
|
/// The media type (optional, defaults to podcast-defined media type)
|
||||||
MediaType: string
|
member val MediaType = "" with get, set
|
||||||
|
|
||||||
/// The URL for the image for this episode (may be permalink; optional, defaults to podcast image)
|
/// The URL for the image for this episode (may be permalink; optional, defaults to podcast image)
|
||||||
ImageUrl: string
|
member val ImageUrl = "" with get, set
|
||||||
|
|
||||||
/// A subtitle for the episode (optional)
|
/// A subtitle for the episode (optional)
|
||||||
Subtitle: string
|
member val Subtitle = "" with get, set
|
||||||
|
|
||||||
/// The explicit rating for this episode (optional, defaults to podcast setting)
|
/// The explicit rating for this episode (optional, defaults to podcast setting)
|
||||||
Explicit: string
|
member val Explicit = "" with get, set
|
||||||
|
|
||||||
/// The chapter source ("internal" for chapters defined here, "external" for a file link, "none" if none defined)
|
/// The chapter source ("internal" for chapters defined here, "external" for a file link, "none" if none defined)
|
||||||
ChapterSource: string
|
member val ChapterSource = "" with get, set
|
||||||
|
|
||||||
/// The URL for the chapter file for the episode (may be permalink; optional)
|
/// The URL for the chapter file for the episode (may be permalink; optional)
|
||||||
ChapterFile: string
|
member val ChapterFile = "" with get, set
|
||||||
|
|
||||||
/// The type of the chapter file (optional; defaults to application/json+chapters if chapterFile is provided)
|
/// The type of the chapter file (optional; defaults to application/json+chapters if chapterFile is provided)
|
||||||
ChapterType: string
|
member val ChapterType = "" with get, set
|
||||||
|
|
||||||
/// Whether the chapter file (or chapters) contains/contain waypoints
|
/// Whether the chapter file (or chapters) contains/contain waypoints
|
||||||
ContainsWaypoints: bool
|
member val ContainsWaypoints = false with get, set
|
||||||
|
|
||||||
/// The URL for the transcript (may be permalink; optional)
|
/// The URL for the transcript (may be permalink; optional)
|
||||||
TranscriptUrl: string
|
member val TranscriptUrl = "" with get, set
|
||||||
|
|
||||||
/// The MIME type for the transcript (optional, recommended if transcriptUrl is provided)
|
/// The MIME type for the transcript (optional, recommended if transcriptUrl is provided)
|
||||||
TranscriptType: string
|
member val TranscriptType = "" with get, set
|
||||||
|
|
||||||
/// The language of the transcript (optional)
|
/// The language of the transcript (optional)
|
||||||
TranscriptLang: string
|
member val TranscriptLang = "" with get, set
|
||||||
|
|
||||||
/// Whether the provided transcript should be presented as captions
|
/// Whether the provided transcript should be presented as captions
|
||||||
TranscriptCaptions: bool
|
member val TranscriptCaptions = false with get, set
|
||||||
|
|
||||||
/// The season number (optional)
|
/// The season number (optional)
|
||||||
SeasonNumber: int
|
member val SeasonNumber = 0 with get, set
|
||||||
|
|
||||||
/// A description of this season (optional, ignored if season number is not provided)
|
/// A description of this season (optional, ignored if season number is not provided)
|
||||||
SeasonDescription: string
|
member val SeasonDescription = "" with get, set
|
||||||
|
|
||||||
/// The episode number (decimal; optional)
|
/// The episode number (decimal; optional)
|
||||||
EpisodeNumber: string
|
member val EpisodeNumber = "" with get, set
|
||||||
|
|
||||||
/// A description of this episode (optional, ignored if episode number is not provided)
|
/// A description of this episode (optional, ignored if episode number is not provided)
|
||||||
EpisodeDescription: string
|
member val EpisodeDescription = "" with get, set
|
||||||
} with
|
|
||||||
|
|
||||||
/// Create an edit model from an existing past
|
/// Create an edit model from an existing past
|
||||||
static member FromPost (webLog: WebLog) (post: Post) =
|
static member FromPost (webLog: WebLog) (post: Post) =
|
||||||
let latest =
|
let model = EditPostModel()
|
||||||
match post.Revisions |> List.sortByDescending _.AsOf |> List.tryHead with
|
let post = if post.Metadata |> List.isEmpty then { post with Metadata = [ MetaItem.Empty ] } else post
|
||||||
| Some rev -> rev
|
model.PopulateFromPost post
|
||||||
| None -> Revision.Empty
|
|
||||||
let post = if post.Metadata |> List.isEmpty then { post with Metadata = [ MetaItem.Empty ] } else post
|
|
||||||
let episode = defaultArg post.Episode Episode.Empty
|
let episode = defaultArg post.Episode Episode.Empty
|
||||||
{ PostId = string post.Id
|
model.Tags <- post.Tags |> String.concat ", "
|
||||||
Title = post.Title
|
model.CategoryIds <- post.CategoryIds |> List.map string |> Array.ofList
|
||||||
Permalink = string post.Permalink
|
model.Status <- string post.Status
|
||||||
Source = latest.Text.SourceType
|
model.PubOverride <- post.PublishedOn |> Option.map webLog.LocalTime |> Option.toNullable
|
||||||
Text = latest.Text.Text
|
model.IsEpisode <- Option.isSome post.Episode
|
||||||
Tags = String.Join(", ", post.Tags)
|
model.Media <- episode.Media
|
||||||
Template = defaultArg post.Template ""
|
model.Length <- episode.Length
|
||||||
CategoryIds = post.CategoryIds |> List.map string |> Array.ofList
|
model.Duration <- defaultArg (episode.FormatDuration()) ""
|
||||||
Status = string post.Status
|
model.MediaType <- defaultArg episode.MediaType ""
|
||||||
DoPublish = false
|
model.ImageUrl <- defaultArg episode.ImageUrl ""
|
||||||
MetaNames = post.Metadata |> List.map _.Name |> Array.ofList
|
model.Subtitle <- defaultArg episode.Subtitle ""
|
||||||
MetaValues = post.Metadata |> List.map _.Value |> Array.ofList
|
model.Explicit <- defaultArg (episode.Explicit |> Option.map string) ""
|
||||||
SetPublished = false
|
model.ChapterSource <- if Option.isSome episode.Chapters then "internal"
|
||||||
PubOverride = post.PublishedOn |> Option.map webLog.LocalTime |> Option.toNullable
|
elif Option.isSome episode.ChapterFile then "external"
|
||||||
SetUpdated = false
|
else "none"
|
||||||
IsEpisode = Option.isSome post.Episode
|
model.ChapterFile <- defaultArg episode.ChapterFile ""
|
||||||
Media = episode.Media
|
model.ChapterType <- defaultArg episode.ChapterType ""
|
||||||
Length = episode.Length
|
model.ContainsWaypoints <- defaultArg episode.ChapterWaypoints false
|
||||||
Duration = defaultArg (episode.FormatDuration()) ""
|
model.TranscriptUrl <- defaultArg episode.TranscriptUrl ""
|
||||||
MediaType = defaultArg episode.MediaType ""
|
model.TranscriptType <- defaultArg episode.TranscriptType ""
|
||||||
ImageUrl = defaultArg episode.ImageUrl ""
|
model.TranscriptLang <- defaultArg episode.TranscriptLang ""
|
||||||
Subtitle = defaultArg episode.Subtitle ""
|
model.TranscriptCaptions <- defaultArg episode.TranscriptCaptions false
|
||||||
Explicit = defaultArg (episode.Explicit |> Option.map string) ""
|
model.SeasonNumber <- defaultArg episode.SeasonNumber 0
|
||||||
ChapterSource = if Option.isSome episode.Chapters then "internal"
|
model.SeasonDescription <- defaultArg episode.SeasonDescription ""
|
||||||
elif Option.isSome episode.ChapterFile then "external"
|
model.EpisodeNumber <- defaultArg (episode.EpisodeNumber |> Option.map string) ""
|
||||||
else "none"
|
model.EpisodeDescription <- defaultArg episode.EpisodeDescription ""
|
||||||
ChapterFile = defaultArg episode.ChapterFile ""
|
model
|
||||||
ChapterType = defaultArg episode.ChapterType ""
|
|
||||||
ContainsWaypoints = defaultArg episode.ChapterWaypoints false
|
|
||||||
TranscriptUrl = defaultArg episode.TranscriptUrl ""
|
|
||||||
TranscriptType = defaultArg episode.TranscriptType ""
|
|
||||||
TranscriptLang = defaultArg episode.TranscriptLang ""
|
|
||||||
TranscriptCaptions = defaultArg episode.TranscriptCaptions false
|
|
||||||
SeasonNumber = defaultArg episode.SeasonNumber 0
|
|
||||||
SeasonDescription = defaultArg episode.SeasonDescription ""
|
|
||||||
EpisodeNumber = defaultArg (episode.EpisodeNumber |> Option.map string) ""
|
|
||||||
EpisodeDescription = defaultArg episode.EpisodeDescription "" }
|
|
||||||
|
|
||||||
/// Whether this is a new post
|
|
||||||
member this.IsNew =
|
|
||||||
this.PostId = "new"
|
|
||||||
|
|
||||||
/// Update a post with values from the submitted form
|
/// Update a post with values from the submitted form
|
||||||
member this.UpdatePost (post: Post) now =
|
member this.UpdatePost (post: Post) now =
|
||||||
|
@ -427,23 +427,21 @@ let absoluteUrl (url: string) (ctx: HttpContext) =
|
|||||||
if url.StartsWith "http" then url else ctx.WebLog.AbsoluteUrl (Permalink url)
|
if url.StartsWith "http" then url else ctx.WebLog.AbsoluteUrl (Permalink url)
|
||||||
|
|
||||||
|
|
||||||
open System.Collections.Generic
|
|
||||||
open MyWebLog.Data
|
open MyWebLog.Data
|
||||||
|
|
||||||
/// Get the templates available for the current web log's theme (in a key/value pair list)
|
/// Get the templates available for the current web log's theme (in a meta item list)
|
||||||
let templatesForTheme (ctx: HttpContext) (typ: string) = backgroundTask {
|
let templatesForTheme (ctx: HttpContext) (typ: string) = backgroundTask {
|
||||||
match! ctx.Data.Theme.FindByIdWithoutText ctx.WebLog.ThemeId with
|
match! ctx.Data.Theme.FindByIdWithoutText ctx.WebLog.ThemeId with
|
||||||
| Some theme ->
|
| Some theme ->
|
||||||
return seq {
|
return seq {
|
||||||
KeyValuePair.Create("", $"- Default (single-{typ}) -")
|
{ Name = ""; Value = $"- Default (single-{typ}) -" }
|
||||||
yield!
|
yield!
|
||||||
theme.Templates
|
theme.Templates
|
||||||
|> Seq.ofList
|
|> Seq.ofList
|
||||||
|> Seq.filter (fun it -> it.Name.EndsWith $"-{typ}" && it.Name <> $"single-{typ}")
|
|> Seq.filter (fun it -> it.Name.EndsWith $"-{typ}" && it.Name <> $"single-{typ}")
|
||||||
|> Seq.map (fun it -> KeyValuePair.Create(it.Name, it.Name))
|
|> Seq.map (fun it -> { Name = it.Name; Value = it.Name })
|
||||||
}
|
}
|
||||||
|> Array.ofSeq
|
| None -> return seq { { Name = ""; Value = $"- Default (single-{typ}) -" } }
|
||||||
| None -> return [| KeyValuePair.Create("", $"- Default (single-{typ}) -") |]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all authors for a list of posts as metadata items
|
/// Get all authors for a list of posts as metadata items
|
||||||
|
@ -34,15 +34,7 @@ let edit pgId : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
|||||||
| Some (title, page) when canEdit page.AuthorId ctx ->
|
| Some (title, page) when canEdit page.AuthorId ctx ->
|
||||||
let model = EditPageModel.FromPage page
|
let model = EditPageModel.FromPage page
|
||||||
let! templates = templatesForTheme ctx "page"
|
let! templates = templatesForTheme ctx "page"
|
||||||
return!
|
return! adminPage title true next ctx (Views.Page.pageEdit model templates)
|
||||||
hashForPage title
|
|
||||||
|> withAntiCsrf ctx
|
|
||||||
|> addToHash ViewContext.Model model
|
|
||||||
|> addToHash "metadata" (
|
|
||||||
Array.zip model.MetaNames model.MetaValues
|
|
||||||
|> Array.mapi (fun idx (name, value) -> [| string idx; name; value |]))
|
|
||||||
|> addToHash "templates" templates
|
|
||||||
|> adminView "page-edit" next ctx
|
|
||||||
| Some _ -> return! Error.notAuthorized next ctx
|
| Some _ -> return! Error.notAuthorized next ctx
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
@ -177,7 +169,7 @@ let save : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
|||||||
AuthorId = ctx.UserId
|
AuthorId = ctx.UserId
|
||||||
PublishedOn = now
|
PublishedOn = now
|
||||||
} |> someTask
|
} |> someTask
|
||||||
else data.Page.FindFullById (PageId model.PageId) ctx.WebLog.Id
|
else data.Page.FindFullById (PageId model.Id) ctx.WebLog.Id
|
||||||
match! tryPage with
|
match! tryPage with
|
||||||
| Some page when canEdit page.AuthorId ctx ->
|
| Some page when canEdit page.AuthorId ctx ->
|
||||||
let updateList = page.IsInPageList <> model.IsShownInPageList
|
let updateList = page.IsInPageList <> model.IsShownInPageList
|
||||||
|
@ -505,7 +505,7 @@ let save : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
|||||||
WebLogId = ctx.WebLog.Id
|
WebLogId = ctx.WebLog.Id
|
||||||
AuthorId = ctx.UserId }
|
AuthorId = ctx.UserId }
|
||||||
|> someTask
|
|> someTask
|
||||||
else data.Post.FindFullById (PostId model.PostId) ctx.WebLog.Id
|
else data.Post.FindFullById (PostId model.Id) ctx.WebLog.Id
|
||||||
match! tryPost with
|
match! tryPost with
|
||||||
| Some post when canEdit post.AuthorId ctx ->
|
| Some post when canEdit post.AuthorId ctx ->
|
||||||
let priorCats = post.CategoryIds
|
let priorCats = post.CategoryIds
|
||||||
@ -522,7 +522,7 @@ let save : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
|||||||
Revisions = [ { (List.head post.Revisions) with AsOf = dt } ] }
|
Revisions = [ { (List.head post.Revisions) with AsOf = dt } ] }
|
||||||
else { post with PublishedOn = Some dt }
|
else { post with PublishedOn = Some dt }
|
||||||
else post
|
else post
|
||||||
do! (if model.PostId = "new" then data.Post.Add else data.Post.Update) updatedPost
|
do! (if model.IsNew then data.Post.Add else data.Post.Update) updatedPost
|
||||||
// If the post was published or its categories changed, refresh the category cache
|
// If the post was published or its categories changed, refresh the category cache
|
||||||
if model.DoPublish
|
if model.DoPublish
|
||||||
|| not (priorCats
|
|| not (priorCats
|
||||||
|
@ -105,19 +105,25 @@ let shortTime app (instant: Instant) =
|
|||||||
let yesOrNo value =
|
let yesOrNo value =
|
||||||
raw (if value then "Yes" else "No")
|
raw (if value then "Yes" else "No")
|
||||||
|
|
||||||
|
/// Extract an attribute value from a list of attributes, remove that attribute if it is found
|
||||||
|
let extractAttrValue name attrs =
|
||||||
|
let valueAttr = attrs |> List.tryFind (fun x -> match x with KeyValue (key, _) when key = name -> true | _ -> false)
|
||||||
|
match valueAttr with
|
||||||
|
| Some (KeyValue (_, value)) ->
|
||||||
|
Some value,
|
||||||
|
attrs |> List.filter (fun x -> match x with KeyValue (key, _) when key = name -> false | _ -> true)
|
||||||
|
| Some _ | None -> None, attrs
|
||||||
|
|
||||||
/// Create a text input field
|
/// Create a text input field
|
||||||
let inputField fieldType attrs name labelText value extra =
|
let inputField fieldType attrs name labelText value extra =
|
||||||
let fieldId, newAttrs =
|
let fieldId, attrs = extractAttrValue "id" attrs
|
||||||
let passedId = attrs |> List.tryFind (fun x -> match x with KeyValue ("id", _) -> true | _ -> false)
|
let cssClass, attrs = extractAttrValue "class" attrs
|
||||||
match passedId with
|
div [ _class $"""form-floating {defaultArg cssClass ""}""" ] [
|
||||||
| Some (KeyValue (_, idValue)) ->
|
[ _type fieldType; _name name; _id (defaultArg fieldId name); _class "form-control"; _placeholder labelText
|
||||||
idValue, attrs |> List.filter (fun x -> match x with KeyValue ("id", _) -> false | _ -> true)
|
_value value ]
|
||||||
| Some _ | None -> name, attrs
|
|> List.append attrs
|
||||||
div [ _class "form-floating" ] [
|
|
||||||
[ _type fieldType; _name name; _id fieldId; _class "form-control"; _placeholder labelText; _value value ]
|
|
||||||
|> List.append newAttrs
|
|
||||||
|> input
|
|> input
|
||||||
label [ _for fieldId ] [ raw labelText ]
|
label [ _for (defaultArg fieldId name) ] [ raw labelText ]
|
||||||
yield! extra
|
yield! extra
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -140,7 +146,8 @@ let passwordField attrs name labelText value extra =
|
|||||||
/// Create a select (dropdown) field
|
/// Create a select (dropdown) field
|
||||||
let selectField<'T, 'a>
|
let selectField<'T, 'a>
|
||||||
attrs name labelText value (values: 'T seq) (idFunc: 'T -> 'a) (displayFunc: 'T -> string) extra =
|
attrs name labelText value (values: 'T seq) (idFunc: 'T -> 'a) (displayFunc: 'T -> string) extra =
|
||||||
div [ _class "form-floating" ] [
|
let cssClass, attrs = extractAttrValue "class" attrs
|
||||||
|
div [ _class $"""form-floating {defaultArg cssClass ""}""" ] [
|
||||||
select ([ _name name; _id name; _class "form-control" ] |> List.append attrs) [
|
select ([ _name name; _id name; _class "form-control" ] |> List.append attrs) [
|
||||||
for item in values do
|
for item in values do
|
||||||
let itemId = string (idFunc item)
|
let itemId = string (idFunc item)
|
||||||
@ -152,7 +159,8 @@ let selectField<'T, 'a>
|
|||||||
|
|
||||||
/// Create a checkbox input styled as a switch
|
/// Create a checkbox input styled as a switch
|
||||||
let checkboxSwitch attrs name labelText (value: bool) extra =
|
let checkboxSwitch attrs name labelText (value: bool) extra =
|
||||||
div [ _class "form-check form-switch" ] [
|
let cssClass, attrs = extractAttrValue "class" attrs
|
||||||
|
div [ _class $"""form-check form-switch {defaultArg cssClass ""}""" ] [
|
||||||
[ _type "checkbox"; _name name; _id name; _class "form-check-input"; _value "true"; if value then _checked ]
|
[ _type "checkbox"; _name name; _id name; _class "form-check-input"; _value "true"; if value then _checked ]
|
||||||
|> List.append attrs
|
|> List.append attrs
|
||||||
|> input
|
|> input
|
||||||
@ -312,8 +320,8 @@ let private capitalize (it: string) =
|
|||||||
|
|
||||||
/// The common edit form shared by pages and posts
|
/// The common edit form shared by pages and posts
|
||||||
let commonEdit (model: EditCommonModel) app = [
|
let commonEdit (model: EditCommonModel) app = [
|
||||||
textField [ _required; _autofocus ] (nameof model.Title) "Title" model.Title []
|
textField [ _class "mb-3"; _required; _autofocus ] (nameof model.Title) "Title" model.Title []
|
||||||
textField [ _required ] (nameof model.Permalink) "Permalink" model.Permalink [
|
textField [ _class "mb-3"; _required ] (nameof model.Permalink) "Permalink" model.Permalink [
|
||||||
if not model.IsNew then
|
if not model.IsNew then
|
||||||
let urlBase = relUrl app $"admin/{model.Entity}/{model.Id}"
|
let urlBase = relUrl app $"admin/{model.Entity}/{model.Id}"
|
||||||
span [ _class "form-text" ] [
|
span [ _class "form-text" ] [
|
||||||
@ -329,14 +337,14 @@ let commonEdit (model: EditCommonModel) app = [
|
|||||||
label [ _for "text" ] [ raw "Text" ]; raw " "
|
label [ _for "text" ] [ raw "Text" ]; raw " "
|
||||||
div [ _class "btn-group btn-group-sm"; _roleGroup; _ariaLabel "Text format button group" ] [
|
div [ _class "btn-group btn-group-sm"; _roleGroup; _ariaLabel "Text format button group" ] [
|
||||||
input [ _type "radio"; _name (nameof model.Source); _id "source_html"; _class "btn-check"
|
input [ _type "radio"; _name (nameof model.Source); _id "source_html"; _class "btn-check"
|
||||||
_value (string Html); if model.Source = string Html then _checked ]
|
_value "HTML"; if model.Source = "HTML" then _checked ]
|
||||||
label [ _class "btn btn-sm btn-outline-secondary"; _for "source_html" ] [ raw "HTML" ]
|
label [ _class "btn btn-sm btn-outline-secondary"; _for "source_html" ] [ raw "HTML" ]
|
||||||
input [ _type "radio"; _name (nameof model.Source); _id "source_md"; _class "btn-check"
|
input [ _type "radio"; _name (nameof model.Source); _id "source_md"; _class "btn-check"
|
||||||
_value (string Markdown); if model.Source = string Markdown then _checked ]
|
_value "Markdown"; if model.Source = "Markdown" then _checked ]
|
||||||
label [ _class "btn btn-sm btn-outline-secondary"; _for "source_md" ] [ raw "Markdown" ]
|
label [ _class "btn btn-sm btn-outline-secondary"; _for "source_md" ] [ raw "Markdown" ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
div [ _class "pb-3" ] [
|
div [ _class "mb-3" ] [
|
||||||
textarea [ _name (nameof model.Text); _id (nameof model.Text); _class "form-control"; _rows "20" ] [
|
textarea [ _name (nameof model.Text); _id (nameof model.Text); _class "form-control"; _rows "20" ] [
|
||||||
raw model.Text
|
raw model.Text
|
||||||
]
|
]
|
||||||
@ -344,6 +352,47 @@ let commonEdit (model: EditCommonModel) app = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
/// Display a common template list
|
||||||
|
let commonTemplates (model: EditCommonModel) (templates: MetaItem seq) =
|
||||||
|
selectField [ _class "mb-3" ] (nameof model.Template) $"{capitalize model.Entity} Template" model.Template templates
|
||||||
|
(_.Name) (_.Value) []
|
||||||
|
|
||||||
|
|
||||||
|
/// Display the metadata item edit form
|
||||||
|
let commonMetaItems (model: EditCommonModel) =
|
||||||
|
let items = Array.zip model.MetaNames model.MetaValues
|
||||||
|
let metaDetail idx (name, value) =
|
||||||
|
div [ _id $"meta_%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.removeMetaItem({idx})" ] [
|
||||||
|
raw "−"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
div [ _class "col-3" ] [ textField [ _id $"MetaNames_{idx}" ] (nameof model.MetaNames) "Name" name [] ]
|
||||||
|
div [ _class "col-8" ] [ textField [ _id $"MetaValues_{idx}" ] (nameof model.MetaValues) "Value" value [] ]
|
||||||
|
]
|
||||||
|
|
||||||
|
fieldset [] [
|
||||||
|
legend [] [
|
||||||
|
raw "Metadata "
|
||||||
|
button [ _type "button"; _class "btn btn-sm btn-secondary"; _data "bs-toggle" "collapse"
|
||||||
|
_data "bs-target" "#meta_item_container" ] [
|
||||||
|
raw "show"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
div [ _id "meta_item_container"; _class "collapse" ] [
|
||||||
|
div [ _id "meta_items"; _class "container" ] (items |> Array.mapi metaDetail |> List.ofArray)
|
||||||
|
button [ _type "button"; _class "btn btn-sm btn-secondary"; _onclick "Admin.addMetaItem()" ] [
|
||||||
|
raw "Add an Item"
|
||||||
|
]
|
||||||
|
script [] [
|
||||||
|
raw """document.addEventListener("DOMContentLoaded", """
|
||||||
|
raw $"() => Admin.setNextMetaIndex({items.Length}))"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
/// Form to manage permalinks for pages or posts
|
/// Form to manage permalinks for pages or posts
|
||||||
let managePermalinks (model: ManagePermalinksModel) app = [
|
let managePermalinks (model: ManagePermalinksModel) app = [
|
||||||
let baseUrl = relUrl app $"admin/{model.Entity}/"
|
let baseUrl = relUrl app $"admin/{model.Entity}/"
|
||||||
|
@ -5,6 +5,27 @@ open Giraffe.ViewEngine.Htmx
|
|||||||
open MyWebLog
|
open MyWebLog
|
||||||
open MyWebLog.ViewModels
|
open MyWebLog.ViewModels
|
||||||
|
|
||||||
|
/// The form to edit pages
|
||||||
|
let pageEdit (model: EditPageModel) templates app = [
|
||||||
|
h2 [ _class "my-3" ] [ raw app.PageTitle ]
|
||||||
|
article [] [
|
||||||
|
form [ _action (relUrl app "admin/page/save"); _method "post"; _hxPushUrl "true"; _class "container" ] [
|
||||||
|
antiCsrf app
|
||||||
|
input [ _type "hidden"; _name (nameof model.Id); _value model.Id ]
|
||||||
|
div [ _class "row mb-3" ] [
|
||||||
|
div [ _class "col-9" ] (commonEdit model app)
|
||||||
|
div [ _class "col-3" ] [
|
||||||
|
commonTemplates model templates
|
||||||
|
checkboxSwitch [] (nameof model.IsShownInPageList) "Show in Page List" model.IsShownInPageList []
|
||||||
|
]
|
||||||
|
]
|
||||||
|
div [ _class "row mb-3" ] [ div [ _class "col" ] [ saveButton ] ]
|
||||||
|
div [ _class "row mb-3" ] [ div [ _class "col" ] [ commonMetaItems model ] ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
/// Display a list of pages for this web log
|
/// Display a list of pages for this web log
|
||||||
let pageList (pages: DisplayPage list) pageNbr hasNext app = [
|
let pageList (pages: DisplayPage list) pageNbr hasNext app = [
|
||||||
h2 [ _class "my-3" ] [ raw app.PageTitle ]
|
h2 [ _class "my-3" ] [ raw app.PageTitle ]
|
||||||
|
@ -146,7 +146,7 @@ this.Admin = {
|
|||||||
newRow.appendChild(nameCol)
|
newRow.appendChild(nameCol)
|
||||||
newRow.appendChild(valueCol)
|
newRow.appendChild(valueCol)
|
||||||
|
|
||||||
document.getElementById("metaItems").appendChild(newRow)
|
document.getElementById("meta_items").appendChild(newRow)
|
||||||
this.nextMetaIndex++
|
this.nextMetaIndex++
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user