741 lines
24 KiB
Forth
741 lines
24 KiB
Forth
|
namespace MyWebLog.ViewModels
|
||
|
|
||
|
open System
|
||
|
open MyWebLog
|
||
|
|
||
|
/// Helper functions for view models
|
||
|
[<AutoOpen>]
|
||
|
module private Helpers =
|
||
|
|
||
|
/// Create a string option if a string is blank
|
||
|
let noneIfBlank (it : string) =
|
||
|
match it.Trim () with "" -> None | trimmed -> Some trimmed
|
||
|
|
||
|
|
||
|
/// Details about a category, used to display category lists
|
||
|
[<NoComparison; NoEquality>]
|
||
|
type DisplayCategory =
|
||
|
{ /// The ID of the category
|
||
|
id : string
|
||
|
|
||
|
/// The slug for the category
|
||
|
slug : string
|
||
|
|
||
|
/// The name of the category
|
||
|
name : string
|
||
|
|
||
|
/// A description of the category
|
||
|
description : string option
|
||
|
|
||
|
/// The parent category names for this (sub)category
|
||
|
parentNames : string[]
|
||
|
|
||
|
/// The number of posts in this category
|
||
|
postCount : int
|
||
|
}
|
||
|
|
||
|
|
||
|
/// A display version of a custom feed definition
|
||
|
type DisplayCustomFeed =
|
||
|
{ /// The ID of the custom feed
|
||
|
id : string
|
||
|
|
||
|
/// The source of the custom feed
|
||
|
source : string
|
||
|
|
||
|
/// The relative path at which the custom feed is served
|
||
|
path : string
|
||
|
|
||
|
/// Whether this custom feed is for a podcast
|
||
|
isPodcast : bool
|
||
|
}
|
||
|
|
||
|
/// Create a display version from a custom feed
|
||
|
static member fromFeed (cats : DisplayCategory[]) (feed : CustomFeed) : DisplayCustomFeed =
|
||
|
let source =
|
||
|
match feed.source with
|
||
|
| Category (CategoryId catId) -> $"Category: {(cats |> Array.find (fun cat -> cat.id = catId)).name}"
|
||
|
| Tag tag -> $"Tag: {tag}"
|
||
|
{ id = CustomFeedId.toString feed.id
|
||
|
source = source
|
||
|
path = Permalink.toString feed.path
|
||
|
isPodcast = Option.isSome feed.podcast
|
||
|
}
|
||
|
|
||
|
|
||
|
/// Details about a page used to display page lists
|
||
|
[<NoComparison; NoEquality>]
|
||
|
type DisplayPage =
|
||
|
{ /// The ID of this page
|
||
|
id : string
|
||
|
|
||
|
/// The title of the page
|
||
|
title : string
|
||
|
|
||
|
/// The link at which this page is displayed
|
||
|
permalink : string
|
||
|
|
||
|
/// When this page was published
|
||
|
publishedOn : DateTime
|
||
|
|
||
|
/// When this page was last updated
|
||
|
updatedOn : DateTime
|
||
|
|
||
|
/// Whether this page shows as part of the web log's navigation
|
||
|
showInPageList : bool
|
||
|
|
||
|
/// Is this the default page?
|
||
|
isDefault : bool
|
||
|
|
||
|
/// The text of the page
|
||
|
text : string
|
||
|
|
||
|
/// The metadata for the page
|
||
|
metadata : MetaItem list
|
||
|
}
|
||
|
|
||
|
/// Create a minimal display page (no text or metadata) from a database page
|
||
|
static member fromPageMinimal webLog (page : Page) =
|
||
|
let pageId = PageId.toString page.id
|
||
|
{ id = pageId
|
||
|
title = page.title
|
||
|
permalink = Permalink.toString page.permalink
|
||
|
publishedOn = page.publishedOn
|
||
|
updatedOn = page.updatedOn
|
||
|
showInPageList = page.showInPageList
|
||
|
isDefault = pageId = webLog.defaultPage
|
||
|
text = ""
|
||
|
metadata = []
|
||
|
}
|
||
|
|
||
|
/// Create a display page from a database page
|
||
|
static member fromPage webLog (page : Page) =
|
||
|
let _, extra = WebLog.hostAndPath webLog
|
||
|
let pageId = PageId.toString page.id
|
||
|
{ id = pageId
|
||
|
title = page.title
|
||
|
permalink = Permalink.toString page.permalink
|
||
|
publishedOn = page.publishedOn
|
||
|
updatedOn = page.updatedOn
|
||
|
showInPageList = page.showInPageList
|
||
|
isDefault = pageId = webLog.defaultPage
|
||
|
text = if extra = "" then page.text else page.text.Replace ("href=\"/", $"href=\"{extra}/")
|
||
|
metadata = page.metadata
|
||
|
}
|
||
|
|
||
|
|
||
|
/// The model used to display the admin dashboard
|
||
|
[<NoComparison; NoEquality>]
|
||
|
type DashboardModel =
|
||
|
{ /// The number of published posts
|
||
|
posts : int
|
||
|
|
||
|
/// The number of post drafts
|
||
|
drafts : int
|
||
|
|
||
|
/// The number of pages
|
||
|
pages : int
|
||
|
|
||
|
/// The number of pages in the page list
|
||
|
listedPages : int
|
||
|
|
||
|
/// The number of categories
|
||
|
categories : int
|
||
|
|
||
|
/// The top-level categories
|
||
|
topLevelCategories : int
|
||
|
}
|
||
|
|
||
|
|
||
|
/// View model for editing categories
|
||
|
[<CLIMutable; NoComparison; NoEquality>]
|
||
|
type EditCategoryModel =
|
||
|
{ /// The ID of the category being edited
|
||
|
categoryId : string
|
||
|
|
||
|
/// The name of the category
|
||
|
name : string
|
||
|
|
||
|
/// The category's URL slug
|
||
|
slug : string
|
||
|
|
||
|
/// A description of the category (optional)
|
||
|
description : string
|
||
|
|
||
|
/// The ID of the category for which this is a subcategory (optional)
|
||
|
parentId : string
|
||
|
}
|
||
|
|
||
|
/// Create an edit model from an existing category
|
||
|
static member fromCategory (cat : Category) =
|
||
|
{ categoryId = CategoryId.toString cat.id
|
||
|
name = cat.name
|
||
|
slug = cat.slug
|
||
|
description = defaultArg cat.description ""
|
||
|
parentId = cat.parentId |> Option.map CategoryId.toString |> Option.defaultValue ""
|
||
|
}
|
||
|
|
||
|
|
||
|
/// View model to edit a custom RSS feed
|
||
|
[<CLIMutable; NoComparison; NoEquality>]
|
||
|
type EditCustomFeedModel =
|
||
|
{ /// The ID of the feed being editing
|
||
|
id : string
|
||
|
|
||
|
/// The type of source for this feed ("category" or "tag")
|
||
|
sourceType : string
|
||
|
|
||
|
/// The category ID or tag on which this feed is based
|
||
|
sourceValue : string
|
||
|
|
||
|
/// The relative path at which this feed is served
|
||
|
path : string
|
||
|
|
||
|
/// Whether this feed defines a podcast
|
||
|
isPodcast : bool
|
||
|
|
||
|
/// The title of the podcast
|
||
|
title : string
|
||
|
|
||
|
/// A subtitle for the podcast
|
||
|
subtitle : string
|
||
|
|
||
|
/// The number of items in the podcast feed
|
||
|
itemsInFeed : int
|
||
|
|
||
|
/// A summary of the podcast (iTunes field)
|
||
|
summary : string
|
||
|
|
||
|
/// The display name of the podcast author (iTunes field)
|
||
|
displayedAuthor : string
|
||
|
|
||
|
/// The e-mail address of the user who registered the podcast at iTunes
|
||
|
email : string
|
||
|
|
||
|
/// The link to the image for the podcast
|
||
|
imageUrl : string
|
||
|
|
||
|
/// The category from iTunes under which this podcast is categorized
|
||
|
itunesCategory : string
|
||
|
|
||
|
/// A further refinement of the categorization of this podcast (iTunes field / values)
|
||
|
itunesSubcategory : string
|
||
|
|
||
|
/// The explictness rating (iTunes field)
|
||
|
explicit : string
|
||
|
|
||
|
/// The default media type for files in this podcast
|
||
|
defaultMediaType : string
|
||
|
|
||
|
/// The base URL for relative URL media files for this podcast (optional; defaults to web log base)
|
||
|
mediaBaseUrl : string
|
||
|
}
|
||
|
|
||
|
/// An empty custom feed model
|
||
|
static member empty =
|
||
|
{ id = ""
|
||
|
sourceType = "category"
|
||
|
sourceValue = ""
|
||
|
path = ""
|
||
|
isPodcast = false
|
||
|
title = ""
|
||
|
subtitle = ""
|
||
|
itemsInFeed = 25
|
||
|
summary = ""
|
||
|
displayedAuthor = ""
|
||
|
email = ""
|
||
|
imageUrl = ""
|
||
|
itunesCategory = ""
|
||
|
itunesSubcategory = ""
|
||
|
explicit = "no"
|
||
|
defaultMediaType = "audio/mpeg"
|
||
|
mediaBaseUrl = ""
|
||
|
}
|
||
|
|
||
|
/// Create a model from a custom feed
|
||
|
static member fromFeed (feed : CustomFeed) =
|
||
|
let rss =
|
||
|
{ EditCustomFeedModel.empty with
|
||
|
id = CustomFeedId.toString feed.id
|
||
|
sourceType = match feed.source with Category _ -> "category" | Tag _ -> "tag"
|
||
|
sourceValue = match feed.source with Category (CategoryId catId) -> catId | Tag tag -> tag
|
||
|
path = Permalink.toString feed.path
|
||
|
}
|
||
|
match feed.podcast with
|
||
|
| Some p ->
|
||
|
{ rss with
|
||
|
isPodcast = true
|
||
|
title = p.title
|
||
|
subtitle = defaultArg p.subtitle ""
|
||
|
itemsInFeed = p.itemsInFeed
|
||
|
summary = p.summary
|
||
|
displayedAuthor = p.displayedAuthor
|
||
|
email = p.email
|
||
|
imageUrl = Permalink.toString p.imageUrl
|
||
|
itunesCategory = p.iTunesCategory
|
||
|
itunesSubcategory = defaultArg p.iTunesSubcategory ""
|
||
|
explicit = ExplicitRating.toString p.explicit
|
||
|
defaultMediaType = defaultArg p.defaultMediaType ""
|
||
|
mediaBaseUrl = defaultArg p.mediaBaseUrl ""
|
||
|
}
|
||
|
| None -> rss
|
||
|
|
||
|
/// Update a feed with values from this model
|
||
|
member this.updateFeed (feed : CustomFeed) =
|
||
|
{ feed with
|
||
|
source = if this.sourceType = "tag" then Tag this.sourceValue else Category (CategoryId this.sourceValue)
|
||
|
path = Permalink this.path
|
||
|
podcast =
|
||
|
if this.isPodcast then
|
||
|
Some {
|
||
|
title = this.title
|
||
|
subtitle = noneIfBlank this.subtitle
|
||
|
itemsInFeed = this.itemsInFeed
|
||
|
summary = this.summary
|
||
|
displayedAuthor = this.displayedAuthor
|
||
|
email = this.email
|
||
|
imageUrl = Permalink this.imageUrl
|
||
|
iTunesCategory = this.itunesCategory
|
||
|
iTunesSubcategory = noneIfBlank this.itunesSubcategory
|
||
|
explicit = ExplicitRating.parse this.explicit
|
||
|
defaultMediaType = noneIfBlank this.defaultMediaType
|
||
|
mediaBaseUrl = noneIfBlank this.mediaBaseUrl
|
||
|
}
|
||
|
else
|
||
|
None
|
||
|
}
|
||
|
|
||
|
/// 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
|
||
|
|
||
|
/// Whether this page is shown in the page list
|
||
|
isShownInPageList : bool
|
||
|
|
||
|
/// The source format for the text
|
||
|
source : string
|
||
|
|
||
|
/// The text of the page
|
||
|
text : string
|
||
|
|
||
|
/// Names of metadata items
|
||
|
metaNames : string[]
|
||
|
|
||
|
/// Values of metadata items
|
||
|
metaValues : string[]
|
||
|
}
|
||
|
|
||
|
/// Create an edit model from an existing page
|
||
|
static member fromPage (page : Page) =
|
||
|
let latest =
|
||
|
match page.revisions |> List.sortByDescending (fun r -> r.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 = PageId.toString page.id
|
||
|
title = page.title
|
||
|
permalink = Permalink.toString page.permalink
|
||
|
template = defaultArg page.template ""
|
||
|
isShownInPageList = page.showInPageList
|
||
|
source = MarkupText.sourceType latest.text
|
||
|
text = MarkupText.text latest.text
|
||
|
metaNames = page.metadata |> List.map (fun m -> m.name) |> Array.ofList
|
||
|
metaValues = page.metadata |> List.map (fun m -> m.value) |> Array.ofList
|
||
|
}
|
||
|
|
||
|
|
||
|
/// 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
|
||
|
|
||
|
/// The tags for the post
|
||
|
tags : string
|
||
|
|
||
|
/// The template used to display the post
|
||
|
template : string
|
||
|
|
||
|
/// The category IDs for the post
|
||
|
categoryIds : string[]
|
||
|
|
||
|
/// The post status
|
||
|
status : string
|
||
|
|
||
|
/// Whether this post should be published
|
||
|
doPublish : bool
|
||
|
|
||
|
/// Names of metadata items
|
||
|
metaNames : string[]
|
||
|
|
||
|
/// Values of metadata items
|
||
|
metaValues : string[]
|
||
|
|
||
|
/// Whether to override the published date/time
|
||
|
setPublished : bool
|
||
|
|
||
|
/// The published date/time to override
|
||
|
pubOverride : Nullable<DateTime>
|
||
|
|
||
|
/// Whether all revisions should be purged and the override date set as the updated date as well
|
||
|
setUpdated : bool
|
||
|
}
|
||
|
/// Create an edit model from an existing past
|
||
|
static member fromPost webLog (post : Post) =
|
||
|
let latest =
|
||
|
match post.revisions |> List.sortByDescending (fun r -> r.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
|
||
|
{ postId = PostId.toString post.id
|
||
|
title = post.title
|
||
|
permalink = Permalink.toString post.permalink
|
||
|
source = MarkupText.sourceType latest.text
|
||
|
text = MarkupText.text latest.text
|
||
|
tags = String.Join (", ", post.tags)
|
||
|
template = defaultArg post.template ""
|
||
|
categoryIds = post.categoryIds |> List.map CategoryId.toString |> Array.ofList
|
||
|
status = PostStatus.toString post.status
|
||
|
doPublish = false
|
||
|
metaNames = post.metadata |> List.map (fun m -> m.name) |> Array.ofList
|
||
|
metaValues = post.metadata |> List.map (fun m -> m.value) |> Array.ofList
|
||
|
setPublished = false
|
||
|
pubOverride = post.publishedOn |> Option.map (WebLog.localTime webLog) |> Option.toNullable
|
||
|
setUpdated = false
|
||
|
}
|
||
|
|
||
|
|
||
|
/// View model to edit RSS settings
|
||
|
[<CLIMutable; NoComparison; NoEquality>]
|
||
|
type EditRssModel =
|
||
|
{ /// Whether the site feed of posts is enabled
|
||
|
feedEnabled : bool
|
||
|
|
||
|
/// The name of the file generated for the site feed
|
||
|
feedName : string
|
||
|
|
||
|
/// Override the "posts per page" setting for the site feed
|
||
|
itemsInFeed : int
|
||
|
|
||
|
/// Whether feeds are enabled for all categories
|
||
|
categoryEnabled : bool
|
||
|
|
||
|
/// Whether feeds are enabled for all tags
|
||
|
tagEnabled : bool
|
||
|
|
||
|
/// A copyright string to be placed in all feeds
|
||
|
copyright : string
|
||
|
}
|
||
|
|
||
|
/// Create an edit model from a set of RSS options
|
||
|
static member fromRssOptions (rss : RssOptions) =
|
||
|
{ feedEnabled = rss.feedEnabled
|
||
|
feedName = rss.feedName
|
||
|
itemsInFeed = defaultArg rss.itemsInFeed 0
|
||
|
categoryEnabled = rss.categoryEnabled
|
||
|
tagEnabled = rss.tagEnabled
|
||
|
copyright = defaultArg rss.copyright ""
|
||
|
}
|
||
|
|
||
|
/// Update RSS options from values in this mode
|
||
|
member this.updateOptions (rss : RssOptions) =
|
||
|
{ rss with
|
||
|
feedEnabled = this.feedEnabled
|
||
|
feedName = this.feedName
|
||
|
itemsInFeed = if this.itemsInFeed = 0 then None else Some this.itemsInFeed
|
||
|
categoryEnabled = this.categoryEnabled
|
||
|
tagEnabled = this.tagEnabled
|
||
|
copyright = noneIfBlank this.copyright
|
||
|
}
|
||
|
|
||
|
|
||
|
/// View model to edit a tag mapping
|
||
|
[<CLIMutable; NoComparison; NoEquality>]
|
||
|
type EditTagMapModel =
|
||
|
{ /// The ID of the tag mapping being edited
|
||
|
id : string
|
||
|
|
||
|
/// The tag being mapped to a different link value
|
||
|
tag : string
|
||
|
|
||
|
/// The link value for the tag
|
||
|
urlValue : string
|
||
|
}
|
||
|
|
||
|
/// Whether this is a new tag mapping
|
||
|
member this.isNew = this.id = "new"
|
||
|
|
||
|
/// Create an edit model from the tag mapping
|
||
|
static member fromMapping (tagMap : TagMap) : EditTagMapModel =
|
||
|
{ id = TagMapId.toString tagMap.id
|
||
|
tag = tagMap.tag
|
||
|
urlValue = tagMap.urlValue
|
||
|
}
|
||
|
|
||
|
|
||
|
/// View model to edit a user
|
||
|
[<CLIMutable; NoComparison; NoEquality>]
|
||
|
type EditUserModel =
|
||
|
{ /// The user's first name
|
||
|
firstName : string
|
||
|
|
||
|
/// The user's last name
|
||
|
lastName : string
|
||
|
|
||
|
/// The user's preferred name
|
||
|
preferredName : string
|
||
|
|
||
|
/// A new password for the user
|
||
|
newPassword : string
|
||
|
|
||
|
/// A new password for the user, confirmed
|
||
|
newPasswordConfirm : string
|
||
|
}
|
||
|
/// Create an edit model from a user
|
||
|
static member fromUser (user : WebLogUser) =
|
||
|
{ firstName = user.firstName
|
||
|
lastName = user.lastName
|
||
|
preferredName = user.preferredName
|
||
|
newPassword = ""
|
||
|
newPasswordConfirm = ""
|
||
|
}
|
||
|
|
||
|
|
||
|
/// The model to use to allow a user to log on
|
||
|
[<CLIMutable; NoComparison; NoEquality>]
|
||
|
type LogOnModel =
|
||
|
{ /// The user's e-mail address
|
||
|
emailAddress : string
|
||
|
|
||
|
/// The user's password
|
||
|
password : string
|
||
|
|
||
|
/// Where the user should be redirected once they have logged on
|
||
|
returnTo : string option
|
||
|
}
|
||
|
|
||
|
/// An empty log on model
|
||
|
static member empty =
|
||
|
{ emailAddress = ""; password = ""; returnTo = None }
|
||
|
|
||
|
|
||
|
/// View model to manage permalinks
|
||
|
[<CLIMutable; NoComparison; NoEquality>]
|
||
|
type ManagePermalinksModel =
|
||
|
{ /// The ID for the entity being edited
|
||
|
id : string
|
||
|
|
||
|
/// The type of entity being edited ("page" or "post")
|
||
|
entity : string
|
||
|
|
||
|
/// The current title of the page or post
|
||
|
currentTitle : string
|
||
|
|
||
|
/// The current permalink of the page or post
|
||
|
currentPermalink : string
|
||
|
|
||
|
/// The prior permalinks for the page or post
|
||
|
prior : string[]
|
||
|
}
|
||
|
|
||
|
/// Create a permalink model from a page
|
||
|
static member fromPage (pg : Page) =
|
||
|
{ id = PageId.toString pg.id
|
||
|
entity = "page"
|
||
|
currentTitle = pg.title
|
||
|
currentPermalink = Permalink.toString pg.permalink
|
||
|
prior = pg.priorPermalinks |> List.map Permalink.toString |> Array.ofList
|
||
|
}
|
||
|
|
||
|
/// Create a permalink model from a post
|
||
|
static member fromPost (post : Post) =
|
||
|
{ id = PostId.toString post.id
|
||
|
entity = "post"
|
||
|
currentTitle = post.title
|
||
|
currentPermalink = Permalink.toString post.permalink
|
||
|
prior = post.priorPermalinks |> List.map Permalink.toString |> Array.ofList
|
||
|
}
|
||
|
|
||
|
|
||
|
/// View model for posts in a list
|
||
|
[<NoComparison; NoEquality>]
|
||
|
type PostListItem =
|
||
|
{ /// The ID of the post
|
||
|
id : string
|
||
|
|
||
|
/// The ID of the user who authored the post
|
||
|
authorId : string
|
||
|
|
||
|
/// The status of the post
|
||
|
status : string
|
||
|
|
||
|
/// The title of the post
|
||
|
title : string
|
||
|
|
||
|
/// The permalink for the post
|
||
|
permalink : string
|
||
|
|
||
|
/// When this post was published
|
||
|
publishedOn : Nullable<DateTime>
|
||
|
|
||
|
/// When this post was last updated
|
||
|
updatedOn : DateTime
|
||
|
|
||
|
/// The text of the post
|
||
|
text : string
|
||
|
|
||
|
/// The IDs of the categories for this post
|
||
|
categoryIds : string list
|
||
|
|
||
|
/// Tags for the post
|
||
|
tags : string list
|
||
|
|
||
|
/// Metadata for the post
|
||
|
metadata : MetaItem list
|
||
|
}
|
||
|
|
||
|
/// Create a post list item from a post
|
||
|
static member fromPost (webLog : WebLog) (post : Post) =
|
||
|
let _, extra = WebLog.hostAndPath webLog
|
||
|
let inTZ = WebLog.localTime webLog
|
||
|
{ id = PostId.toString post.id
|
||
|
authorId = WebLogUserId.toString post.authorId
|
||
|
status = PostStatus.toString post.status
|
||
|
title = post.title
|
||
|
permalink = Permalink.toString post.permalink
|
||
|
publishedOn = post.publishedOn |> Option.map inTZ |> Option.toNullable
|
||
|
updatedOn = inTZ post.updatedOn
|
||
|
text = if extra = "" then post.text else post.text.Replace ("href=\"/", $"href=\"{extra}/")
|
||
|
categoryIds = post.categoryIds |> List.map CategoryId.toString
|
||
|
tags = post.tags
|
||
|
metadata = post.metadata
|
||
|
}
|
||
|
|
||
|
|
||
|
/// View model for displaying posts
|
||
|
type PostDisplay =
|
||
|
{ /// The posts to be displayed
|
||
|
posts : PostListItem[]
|
||
|
|
||
|
/// Author ID -> name lookup
|
||
|
authors : MetaItem list
|
||
|
|
||
|
/// A subtitle for the page
|
||
|
subtitle : string option
|
||
|
|
||
|
/// The link to view newer (more recent) posts
|
||
|
newerLink : string option
|
||
|
|
||
|
/// The name of the next newer post (single-post only)
|
||
|
newerName : string option
|
||
|
|
||
|
/// The link to view older (less recent) posts
|
||
|
olderLink : string option
|
||
|
|
||
|
/// The name of the next older post (single-post only)
|
||
|
olderName : string option
|
||
|
}
|
||
|
|
||
|
|
||
|
/// View model for editing web log settings
|
||
|
[<CLIMutable; NoComparison; NoEquality>]
|
||
|
type SettingsModel =
|
||
|
{ /// The name of the web log
|
||
|
name : string
|
||
|
|
||
|
/// The subtitle of the web log
|
||
|
subtitle : string
|
||
|
|
||
|
/// The default page
|
||
|
defaultPage : string
|
||
|
|
||
|
/// How many posts should appear on index pages
|
||
|
postsPerPage : int
|
||
|
|
||
|
/// The time zone in which dates/times should be displayed
|
||
|
timeZone : string
|
||
|
|
||
|
/// The theme to use to display the web log
|
||
|
themePath : string
|
||
|
|
||
|
/// Whether to automatically load htmx
|
||
|
autoHtmx : bool
|
||
|
}
|
||
|
|
||
|
/// Create a settings model from a web log
|
||
|
static member fromWebLog (webLog : WebLog) =
|
||
|
{ name = webLog.name
|
||
|
subtitle = defaultArg webLog.subtitle ""
|
||
|
defaultPage = webLog.defaultPage
|
||
|
postsPerPage = webLog.postsPerPage
|
||
|
timeZone = webLog.timeZone
|
||
|
themePath = webLog.themePath
|
||
|
autoHtmx = webLog.autoHtmx
|
||
|
}
|
||
|
|
||
|
/// Update a web log with settings from the form
|
||
|
member this.update (webLog : WebLog) =
|
||
|
{ webLog with
|
||
|
name = this.name
|
||
|
subtitle = if this.subtitle = "" then None else Some this.subtitle
|
||
|
defaultPage = this.defaultPage
|
||
|
postsPerPage = this.postsPerPage
|
||
|
timeZone = this.timeZone
|
||
|
themePath = this.themePath
|
||
|
autoHtmx = this.autoHtmx
|
||
|
}
|
||
|
|
||
|
|
||
|
[<CLIMutable; NoComparison; NoEquality>]
|
||
|
type UserMessage =
|
||
|
{ /// The level of the message
|
||
|
level : string
|
||
|
|
||
|
/// The message
|
||
|
message : string
|
||
|
|
||
|
/// Further details about the message
|
||
|
detail : string option
|
||
|
}
|
||
|
|
||
|
/// Functions to support user messages
|
||
|
module UserMessage =
|
||
|
|
||
|
/// An empty user message (use one of the others for pre-filled level)
|
||
|
let empty = { level = ""; message = ""; detail = None }
|
||
|
|
||
|
/// A blank success message
|
||
|
let success = { empty with level = "success" }
|
||
|
|
||
|
/// A blank informational message
|
||
|
let info = { empty with level = "primary" }
|
||
|
|
||
|
/// A blank warning message
|
||
|
let warning = { empty with level = "warning" }
|
||
|
|
||
|
/// A blank error message
|
||
|
let error = { empty with level = "danger" }
|