Version 2.1 #41

Merged
danieljsummers merged 123 commits from version-2.1 into main 2024-03-27 00:13:28 +00:00
7 changed files with 187 additions and 190 deletions
Showing only changes of commit ec04fea86c - Show all commits

View File

@ -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.Template <- defaultArg page.Template ""
this.Source <- latest.Text.SourceType this.Source <- latest.Text.SourceType
this.Text <- latest.Text.Text 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
| Some rev -> rev
| None -> Revision.Empty
let post = if post.Metadata |> List.isEmpty then { post with Metadata = [ MetaItem.Empty ] } else post 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 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
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" elif Option.isSome episode.ChapterFile then "external"
else "none" else "none"
ChapterFile = defaultArg episode.ChapterFile "" model.ChapterFile <- defaultArg episode.ChapterFile ""
ChapterType = defaultArg episode.ChapterType "" model.ChapterType <- defaultArg episode.ChapterType ""
ContainsWaypoints = defaultArg episode.ChapterWaypoints false model.ContainsWaypoints <- defaultArg episode.ChapterWaypoints false
TranscriptUrl = defaultArg episode.TranscriptUrl "" model.TranscriptUrl <- defaultArg episode.TranscriptUrl ""
TranscriptType = defaultArg episode.TranscriptType "" model.TranscriptType <- defaultArg episode.TranscriptType ""
TranscriptLang = defaultArg episode.TranscriptLang "" model.TranscriptLang <- defaultArg episode.TranscriptLang ""
TranscriptCaptions = defaultArg episode.TranscriptCaptions false model.TranscriptCaptions <- defaultArg episode.TranscriptCaptions false
SeasonNumber = defaultArg episode.SeasonNumber 0 model.SeasonNumber <- defaultArg episode.SeasonNumber 0
SeasonDescription = defaultArg episode.SeasonDescription "" model.SeasonDescription <- defaultArg episode.SeasonDescription ""
EpisodeNumber = defaultArg (episode.EpisodeNumber |> Option.map string) "" model.EpisodeNumber <- defaultArg (episode.EpisodeNumber |> Option.map string) ""
EpisodeDescription = defaultArg episode.EpisodeDescription "" } model.EpisodeDescription <- defaultArg episode.EpisodeDescription ""
model
/// 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 =

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 " &nbsp; &nbsp; " label [ _for "text" ] [ raw "Text" ]; raw " &nbsp; &nbsp; "
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 "&minus;"
]
]
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}/"

View File

@ -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 ]

View File

@ -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++
}, },