Version 2.1 #41
|
@ -626,88 +626,65 @@ type EditCommonModel() =
|
|||
/// Whether to provide a link to manage chapters
|
||||
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")
|
||||
member val Source = "" with get, set
|
||||
|
||||
/// The text of the page or post
|
||||
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
|
||||
member this.IsNew with get () = this.Id = "new"
|
||||
|
||||
/// Fill the properties of this object from a page
|
||||
member this.FromPage (page: Page) =
|
||||
member this.PopulateFromPage (page: Page) =
|
||||
let latest = findLatestRevision page.Revisions
|
||||
this.Id <- string page.Id
|
||||
this.Title <- page.Title
|
||||
this.Permalink <- string page.Permalink
|
||||
this.Entity <- "page"
|
||||
this.Source <- latest.Text.SourceType
|
||||
this.Text <- latest.Text.Text
|
||||
this.Id <- string page.Id
|
||||
this.Title <- page.Title
|
||||
this.Permalink <- string page.Permalink
|
||||
this.Entity <- "page"
|
||||
this.Template <- defaultArg page.Template ""
|
||||
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
|
||||
member this.FromPost (post: Post) =
|
||||
member this.PopulateFromPost (post: Post) =
|
||||
let latest = findLatestRevision post.Revisions
|
||||
this.Id <- string post.Id
|
||||
this.Title <- post.Title
|
||||
this.Permalink <- string post.Permalink
|
||||
this.Entity <- "post"
|
||||
this.IncludeChapterLink <- Option.isSome post.Episode && Option.isSome post.Episode.Value.Chapters
|
||||
this.Template <- defaultArg post.Template ""
|
||||
this.Source <- latest.Text.SourceType
|
||||
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
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type EditPageModel = {
|
||||
/// 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
|
||||
type EditPageModel() =
|
||||
inherit EditCommonModel()
|
||||
|
||||
/// 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
|
||||
static member FromPage (page: Page) =
|
||||
let latest =
|
||||
match page.Revisions |> List.sortByDescending _.AsOf |> List.tryHead with
|
||||
| Some rev -> rev
|
||||
| None -> Revision.Empty
|
||||
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"
|
||||
static member FromPage(page: Page) =
|
||||
let model = EditPageModel()
|
||||
model.PopulateFromPage page
|
||||
model.IsShownInPageList <- page.IsInPageList
|
||||
model
|
||||
|
||||
/// Update a page with values from this model
|
||||
member this.UpdatePage (page: Page) now =
|
||||
|
@ -737,163 +714,123 @@ type EditPageModel = {
|
|||
|
||||
|
||||
/// View model to edit a post
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type EditPostModel = {
|
||||
/// 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
|
||||
type EditPostModel() =
|
||||
inherit EditCommonModel()
|
||||
|
||||
/// The tags for the post
|
||||
Tags: string
|
||||
|
||||
/// The template used to display the post
|
||||
Template: string
|
||||
member val Tags = "" with get, set
|
||||
|
||||
/// The category IDs for the post
|
||||
CategoryIds: string array
|
||||
member val CategoryIds: string array = [||] with get, set
|
||||
|
||||
/// The post status
|
||||
Status: string
|
||||
member val Status = "" with get, set
|
||||
|
||||
/// Whether this post should be published
|
||||
DoPublish: bool
|
||||
|
||||
/// Names of metadata items
|
||||
MetaNames: string array
|
||||
|
||||
/// Values of metadata items
|
||||
MetaValues: string array
|
||||
member val DoPublish = false with get, set
|
||||
|
||||
/// Whether to override the published date/time
|
||||
SetPublished: bool
|
||||
member val SetPublished = false with get, set
|
||||
|
||||
/// 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
|
||||
SetUpdated: bool
|
||||
member val SetUpdated = false with get, set
|
||||
|
||||
/// 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)
|
||||
Media: string
|
||||
member val Media = "" with get, set
|
||||
|
||||
/// 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
|
||||
Duration: string
|
||||
member val Duration = "" with get, set
|
||||
|
||||
/// 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)
|
||||
ImageUrl: string
|
||||
member val ImageUrl = "" with get, set
|
||||
|
||||
/// 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)
|
||||
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)
|
||||
ChapterSource: string
|
||||
member val ChapterSource = "" with get, set
|
||||
|
||||
/// 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)
|
||||
ChapterType: string
|
||||
member val ChapterType = "" with get, set
|
||||
|
||||
/// 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)
|
||||
TranscriptUrl: string
|
||||
member val TranscriptUrl = "" with get, set
|
||||
|
||||
/// 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)
|
||||
TranscriptLang: string
|
||||
member val TranscriptLang = "" with get, set
|
||||
|
||||
/// Whether the provided transcript should be presented as captions
|
||||
TranscriptCaptions: bool
|
||||
member val TranscriptCaptions = false with get, set
|
||||
|
||||
/// 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)
|
||||
SeasonDescription: string
|
||||
member val SeasonDescription = "" with get, set
|
||||
|
||||
/// 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)
|
||||
EpisodeDescription: string
|
||||
} with
|
||||
member val EpisodeDescription = "" with get, set
|
||||
|
||||
/// Create an edit model from an existing past
|
||||
static member FromPost (webLog: WebLog) (post: Post) =
|
||||
let latest =
|
||||
match post.Revisions |> List.sortByDescending _.AsOf |> List.tryHead with
|
||||
| Some rev -> rev
|
||||
| None -> Revision.Empty
|
||||
let post = if post.Metadata |> List.isEmpty then { post with Metadata = [ MetaItem.Empty ] } else post
|
||||
let model = EditPostModel()
|
||||
let post = if post.Metadata |> List.isEmpty then { post with Metadata = [ MetaItem.Empty ] } else post
|
||||
model.PopulateFromPost post
|
||||
let episode = defaultArg post.Episode Episode.Empty
|
||||
{ PostId = string post.Id
|
||||
Title = post.Title
|
||||
Permalink = string post.Permalink
|
||||
Source = latest.Text.SourceType
|
||||
Text = latest.Text.Text
|
||||
Tags = String.Join(", ", post.Tags)
|
||||
Template = defaultArg post.Template ""
|
||||
CategoryIds = post.CategoryIds |> List.map string |> Array.ofList
|
||||
Status = string post.Status
|
||||
DoPublish = false
|
||||
MetaNames = post.Metadata |> List.map _.Name |> Array.ofList
|
||||
MetaValues = post.Metadata |> List.map _.Value |> Array.ofList
|
||||
SetPublished = false
|
||||
PubOverride = post.PublishedOn |> Option.map webLog.LocalTime |> Option.toNullable
|
||||
SetUpdated = false
|
||||
IsEpisode = Option.isSome post.Episode
|
||||
Media = episode.Media
|
||||
Length = episode.Length
|
||||
Duration = defaultArg (episode.FormatDuration()) ""
|
||||
MediaType = defaultArg episode.MediaType ""
|
||||
ImageUrl = defaultArg episode.ImageUrl ""
|
||||
Subtitle = defaultArg episode.Subtitle ""
|
||||
Explicit = defaultArg (episode.Explicit |> Option.map string) ""
|
||||
ChapterSource = if Option.isSome episode.Chapters then "internal"
|
||||
elif Option.isSome episode.ChapterFile then "external"
|
||||
else "none"
|
||||
ChapterFile = defaultArg episode.ChapterFile ""
|
||||
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"
|
||||
model.Tags <- post.Tags |> String.concat ", "
|
||||
model.CategoryIds <- post.CategoryIds |> List.map string |> Array.ofList
|
||||
model.Status <- string post.Status
|
||||
model.PubOverride <- post.PublishedOn |> Option.map webLog.LocalTime |> Option.toNullable
|
||||
model.IsEpisode <- Option.isSome post.Episode
|
||||
model.Media <- episode.Media
|
||||
model.Length <- episode.Length
|
||||
model.Duration <- defaultArg (episode.FormatDuration()) ""
|
||||
model.MediaType <- defaultArg episode.MediaType ""
|
||||
model.ImageUrl <- defaultArg episode.ImageUrl ""
|
||||
model.Subtitle <- defaultArg episode.Subtitle ""
|
||||
model.Explicit <- defaultArg (episode.Explicit |> Option.map string) ""
|
||||
model.ChapterSource <- if Option.isSome episode.Chapters then "internal"
|
||||
elif Option.isSome episode.ChapterFile then "external"
|
||||
else "none"
|
||||
model.ChapterFile <- defaultArg episode.ChapterFile ""
|
||||
model.ChapterType <- defaultArg episode.ChapterType ""
|
||||
model.ContainsWaypoints <- defaultArg episode.ChapterWaypoints false
|
||||
model.TranscriptUrl <- defaultArg episode.TranscriptUrl ""
|
||||
model.TranscriptType <- defaultArg episode.TranscriptType ""
|
||||
model.TranscriptLang <- defaultArg episode.TranscriptLang ""
|
||||
model.TranscriptCaptions <- defaultArg episode.TranscriptCaptions false
|
||||
model.SeasonNumber <- defaultArg episode.SeasonNumber 0
|
||||
model.SeasonDescription <- defaultArg episode.SeasonDescription ""
|
||||
model.EpisodeNumber <- defaultArg (episode.EpisodeNumber |> Option.map string) ""
|
||||
model.EpisodeDescription <- defaultArg episode.EpisodeDescription ""
|
||||
model
|
||||
|
||||
/// Update a post with values from the submitted form
|
||||
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)
|
||||
|
||||
|
||||
open System.Collections.Generic
|
||||
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 {
|
||||
match! ctx.Data.Theme.FindByIdWithoutText ctx.WebLog.ThemeId with
|
||||
| Some theme ->
|
||||
return seq {
|
||||
KeyValuePair.Create("", $"- Default (single-{typ}) -")
|
||||
{ Name = ""; Value = $"- Default (single-{typ}) -" }
|
||||
yield!
|
||||
theme.Templates
|
||||
|> Seq.ofList
|
||||
|> 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 [| KeyValuePair.Create("", $"- Default (single-{typ}) -") |]
|
||||
| None -> return seq { { Name = ""; Value = $"- Default (single-{typ}) -" } }
|
||||
}
|
||||
|
||||
/// 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 ->
|
||||
let model = EditPageModel.FromPage page
|
||||
let! templates = templatesForTheme ctx "page"
|
||||
return!
|
||||
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
|
||||
return! adminPage title true next ctx (Views.Page.pageEdit model templates)
|
||||
| Some _ -> return! Error.notAuthorized next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
@ -177,7 +169,7 @@ let save : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
|||
AuthorId = ctx.UserId
|
||||
PublishedOn = now
|
||||
} |> someTask
|
||||
else data.Page.FindFullById (PageId model.PageId) ctx.WebLog.Id
|
||||
else data.Page.FindFullById (PageId model.Id) ctx.WebLog.Id
|
||||
match! tryPage with
|
||||
| Some page when canEdit page.AuthorId ctx ->
|
||||
let updateList = page.IsInPageList <> model.IsShownInPageList
|
||||
|
|
|
@ -505,7 +505,7 @@ let save : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
|||
WebLogId = ctx.WebLog.Id
|
||||
AuthorId = ctx.UserId }
|
||||
|> someTask
|
||||
else data.Post.FindFullById (PostId model.PostId) ctx.WebLog.Id
|
||||
else data.Post.FindFullById (PostId model.Id) ctx.WebLog.Id
|
||||
match! tryPost with
|
||||
| Some post when canEdit post.AuthorId ctx ->
|
||||
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 } ] }
|
||||
else { post with PublishedOn = Some dt }
|
||||
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 model.DoPublish
|
||||
|| not (priorCats
|
||||
|
|
|
@ -105,19 +105,25 @@ let shortTime app (instant: Instant) =
|
|||
let yesOrNo value =
|
||||
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
|
||||
let inputField fieldType attrs name labelText value extra =
|
||||
let fieldId, newAttrs =
|
||||
let passedId = attrs |> List.tryFind (fun x -> match x with KeyValue ("id", _) -> true | _ -> false)
|
||||
match passedId with
|
||||
| Some (KeyValue (_, idValue)) ->
|
||||
idValue, attrs |> List.filter (fun x -> match x with KeyValue ("id", _) -> false | _ -> true)
|
||||
| Some _ | None -> name, attrs
|
||||
div [ _class "form-floating" ] [
|
||||
[ _type fieldType; _name name; _id fieldId; _class "form-control"; _placeholder labelText; _value value ]
|
||||
|> List.append newAttrs
|
||||
let fieldId, attrs = extractAttrValue "id" attrs
|
||||
let cssClass, attrs = extractAttrValue "class" attrs
|
||||
div [ _class $"""form-floating {defaultArg cssClass ""}""" ] [
|
||||
[ _type fieldType; _name name; _id (defaultArg fieldId name); _class "form-control"; _placeholder labelText
|
||||
_value value ]
|
||||
|> List.append attrs
|
||||
|> input
|
||||
label [ _for fieldId ] [ raw labelText ]
|
||||
label [ _for (defaultArg fieldId name) ] [ raw labelText ]
|
||||
yield! extra
|
||||
]
|
||||
|
||||
|
@ -140,7 +146,8 @@ let passwordField attrs name labelText value extra =
|
|||
/// Create a select (dropdown) field
|
||||
let selectField<'T, 'a>
|
||||
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) [
|
||||
for item in values do
|
||||
let itemId = string (idFunc item)
|
||||
|
@ -152,7 +159,8 @@ let selectField<'T, 'a>
|
|||
|
||||
/// Create a checkbox input styled as a switch
|
||||
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 ]
|
||||
|> List.append attrs
|
||||
|> input
|
||||
|
@ -312,8 +320,8 @@ let private capitalize (it: string) =
|
|||
|
||||
/// The common edit form shared by pages and posts
|
||||
let commonEdit (model: EditCommonModel) app = [
|
||||
textField [ _required; _autofocus ] (nameof model.Title) "Title" model.Title []
|
||||
textField [ _required ] (nameof model.Permalink) "Permalink" model.Permalink [
|
||||
textField [ _class "mb-3"; _required; _autofocus ] (nameof model.Title) "Title" model.Title []
|
||||
textField [ _class "mb-3"; _required ] (nameof model.Permalink) "Permalink" model.Permalink [
|
||||
if not model.IsNew then
|
||||
let urlBase = relUrl app $"admin/{model.Entity}/{model.Id}"
|
||||
span [ _class "form-text" ] [
|
||||
|
@ -329,14 +337,14 @@ let commonEdit (model: EditCommonModel) app = [
|
|||
label [ _for "text" ] [ raw "Text" ]; raw " "
|
||||
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"
|
||||
_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" ]
|
||||
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" ]
|
||||
]
|
||||
]
|
||||
div [ _class "pb-3" ] [
|
||||
div [ _class "mb-3" ] [
|
||||
textarea [ _name (nameof model.Text); _id (nameof model.Text); _class "form-control"; _rows "20" ] [
|
||||
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
|
||||
let managePermalinks (model: ManagePermalinksModel) app = [
|
||||
let baseUrl = relUrl app $"admin/{model.Entity}/"
|
||||
|
|
|
@ -5,6 +5,27 @@ open Giraffe.ViewEngine.Htmx
|
|||
open MyWebLog
|
||||
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
|
||||
let pageList (pages: DisplayPage list) pageNbr hasNext app = [
|
||||
h2 [ _class "my-3" ] [ raw app.PageTitle ]
|
||||
|
|
|
@ -146,7 +146,7 @@ this.Admin = {
|
|||
newRow.appendChild(nameCol)
|
||||
newRow.appendChild(valueCol)
|
||||
|
||||
document.getElementById("metaItems").appendChild(newRow)
|
||||
document.getElementById("meta_items").appendChild(newRow)
|
||||
this.nextMetaIndex++
|
||||
},
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user