Update deps; WIP on comments
This commit is contained in:
@@ -6,7 +6,10 @@ open Giraffe.ViewEngine.Htmx
|
||||
open MyWebLog
|
||||
open MyWebLog.ViewModels
|
||||
|
||||
/// The administrator dashboard
|
||||
/// <summary>The administrator dashboard</summary>
|
||||
/// <param name="themes">The themes to display</param>
|
||||
/// <param name="app">The view context</param>
|
||||
/// <returns>The admin dashboard view</returns>
|
||||
let dashboard (themes: Theme list) app = [
|
||||
let templates = Template.Cache.allNames ()
|
||||
let cacheBaseUrl = relUrl app "admin/cache/"
|
||||
@@ -55,7 +58,7 @@ let dashboard (themes: Theme list) app = [
|
||||
]
|
||||
div [ _class "row" ] [
|
||||
div [ _class "col-12 col-lg-6 pb-3" ] [
|
||||
div [ _class "card" ] [
|
||||
div [ _class "card" ] [
|
||||
header [ _class "card-header text-white bg-secondary" ] [ raw "Web Logs" ]
|
||||
div [ _class "card-body pb-0" ] [
|
||||
h6 [ _class "card-subtitle text-muted pb-3" ] [
|
||||
|
||||
+131
-38
@@ -1,3 +1,4 @@
|
||||
/// <summary>Helpers available for all myWebLog views</summary>
|
||||
[<AutoOpen>]
|
||||
module MyWebLog.Views.Helpers
|
||||
|
||||
@@ -9,28 +10,35 @@ open MyWebLog.ViewModels
|
||||
open NodaTime
|
||||
open NodaTime.Text
|
||||
|
||||
/// Create a relative URL for the current web log
|
||||
/// <summary>Create a relative URL for the current web log</summary>
|
||||
/// <param name="app">The app view context for the current view</param>
|
||||
/// <returns>A function that, given a string, will construct a relative URL</returns>
|
||||
let relUrl app =
|
||||
Permalink >> app.WebLog.RelativeUrl
|
||||
|
||||
/// Add a hidden input with the anti-Cross Site Request Forgery (CSRF) token
|
||||
/// <summary>Create a hidden input with the anti-Cross Site Request Forgery (CSRF) token</summary>
|
||||
/// <param name="app">The app view context for the current view</param>
|
||||
/// <returns>A hidden input with the CSRF token value</returns>
|
||||
let antiCsrf app =
|
||||
input [ _type "hidden"; _name app.Csrf.Value.FormFieldName; _value app.Csrf.Value.RequestToken ]
|
||||
|
||||
/// Shorthand for encoded text in a template
|
||||
/// <summary>Shorthand for encoded text in a template</summary>
|
||||
let txt = encodedText
|
||||
|
||||
/// Shorthand for raw text in a template
|
||||
/// <summary>Shorthand for raw text in a template</summary>
|
||||
let raw = rawText
|
||||
|
||||
/// Rel attribute to prevent opener information from being provided to the new window
|
||||
/// <summary><c>rel</c> attribute to prevent opener information from being provided to the new window</summary>
|
||||
let _relNoOpener = _rel "noopener"
|
||||
|
||||
/// The pattern for a long date
|
||||
/// <summary>The pattern for a long date</summary>
|
||||
let longDatePattern =
|
||||
ZonedDateTimePattern.CreateWithInvariantCulture("MMMM d, yyyy", DateTimeZoneProviders.Tzdb)
|
||||
|
||||
/// Create a long date
|
||||
/// <summary>Create a long date</summary>
|
||||
/// <param name="app">The app view context for the current view</param>
|
||||
/// <param name="instant">The instant from which a localized long date should be produced</param>
|
||||
/// <returns>A text node with the long date</returns>
|
||||
let longDate app (instant: Instant) =
|
||||
DateTimeZoneProviders.Tzdb[app.WebLog.TimeZone]
|
||||
|> Option.ofObj
|
||||
@@ -38,11 +46,14 @@ let longDate app (instant: Instant) =
|
||||
|> Option.defaultValue "--"
|
||||
|> txt
|
||||
|
||||
/// The pattern for a short time
|
||||
/// <summary>The pattern for a short time</summary>
|
||||
let shortTimePattern =
|
||||
ZonedDateTimePattern.CreateWithInvariantCulture("h:mmtt", DateTimeZoneProviders.Tzdb)
|
||||
|
||||
/// Create a short time
|
||||
/// <summary>Create a short time</summary>
|
||||
/// <param name="app">The app view context for the current view</param>
|
||||
/// <param name="instant">The instant from which a localized short date should be produced</param>
|
||||
/// <returns>A text node with the short date</returns>
|
||||
let shortTime app (instant: Instant) =
|
||||
DateTimeZoneProviders.Tzdb[app.WebLog.TimeZone]
|
||||
|> Option.ofObj
|
||||
@@ -50,11 +61,19 @@ let shortTime app (instant: Instant) =
|
||||
|> Option.defaultValue "--"
|
||||
|> txt
|
||||
|
||||
/// Display "Yes" or "No" based on the state of a boolean value
|
||||
/// <summary>Display "Yes" or "No" based on the state of a boolean value</summary>
|
||||
/// <param name="value">The true/false value</param>
|
||||
/// <returns>A text node with <c>Yes</c> if true, <c>No</c> if false</returns>
|
||||
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
|
||||
/// <summary>Extract an attribute value from a list of attributes, remove that attribute if it is found</summary>
|
||||
/// <param name="name">The name of the attribute to be extracted and removed</param>
|
||||
/// <param name="attrs">The list of attributes to be searched</param>
|
||||
/// <returns>
|
||||
/// A tuple with <c>fst</c> being <c>Some</c> with the attribute if found, <c>None</c> if not; and <c>snd</c>
|
||||
/// being the list of attributes with the extracted one removed
|
||||
/// </returns>
|
||||
let extractAttrValue name attrs =
|
||||
let valueAttr = attrs |> List.tryFind (fun x -> match x with KeyValue (key, _) when key = name -> true | _ -> false)
|
||||
match valueAttr with
|
||||
@@ -62,8 +81,15 @@ let extractAttrValue name attrs =
|
||||
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
|
||||
|
||||
/// <summary>Create a text input field</summary>
|
||||
/// <param name="fieldType">The <c>input</c> field type</param>
|
||||
/// <param name="attrs">Attributes for the field</param>
|
||||
/// <param name="name">The name of the input field</param>
|
||||
/// <param name="labelText">The text of the <c>label</c> element associated with this <c>input</c></param>
|
||||
/// <param name="value">The value of the <c>input</c> field</param>
|
||||
/// <param name="extra">Any extra elements to include after the <c>input</c> and <c>label</c></param>
|
||||
/// <returns>A <c>div</c> element with the <c>input</c> field constructed</returns>
|
||||
let inputField fieldType attrs name labelText value extra =
|
||||
let fieldId, attrs = extractAttrValue "id" attrs
|
||||
let cssClass, attrs = extractAttrValue "class" attrs
|
||||
@@ -76,23 +102,58 @@ let inputField fieldType attrs name labelText value extra =
|
||||
yield! extra
|
||||
]
|
||||
|
||||
/// Create a text input field
|
||||
/// <summary>Create a text input field</summary>
|
||||
/// <param name="attrs">Attributes for the field</param>
|
||||
/// <param name="name">The name of the input field</param>
|
||||
/// <param name="labelText">The text of the <c>label</c> element associated with this <c>input</c></param>
|
||||
/// <param name="value">The value of the <c>input</c> field</param>
|
||||
/// <param name="extra">Any extra elements to include after the <c>input</c> and <c>label</c></param>
|
||||
/// <returns>A <c>div</c> element with the <input type=text> field constructed</returns>
|
||||
let textField attrs name labelText value extra =
|
||||
inputField "text" attrs name labelText value extra
|
||||
|
||||
/// Create a number input field
|
||||
/// <summary>Create a number input field</summary>
|
||||
/// <param name="attrs">Attributes for the field</param>
|
||||
/// <param name="name">The name of the input field</param>
|
||||
/// <param name="labelText">The text of the <c>label</c> element associated with this <c>input</c></param>
|
||||
/// <param name="value">The value of the <c>input</c> field</param>
|
||||
/// <param name="extra">Any extra elements to include after the <c>input</c> and <c>label</c></param>
|
||||
/// <returns>A <c>div</c> element with the <input type=number> field constructed</returns>
|
||||
let numberField attrs name labelText value extra =
|
||||
inputField "number" attrs name labelText value extra
|
||||
|
||||
/// Create an e-mail input field
|
||||
/// <summary>Create an e-mail input field</summary>
|
||||
/// <param name="attrs">Attributes for the field</param>
|
||||
/// <param name="name">The name of the input field</param>
|
||||
/// <param name="labelText">The text of the <c>label</c> element associated with this <c>input</c></param>
|
||||
/// <param name="value">The value of the <c>input</c> field</param>
|
||||
/// <param name="extra">Any extra elements to include after the <c>input</c> and <c>label</c></param>
|
||||
/// <returns>A <c>div</c> element with the <input type=email> field constructed</returns>
|
||||
let emailField attrs name labelText value extra =
|
||||
inputField "email" attrs name labelText value extra
|
||||
|
||||
/// Create a password input field
|
||||
/// <summary>Create a password input field</summary>
|
||||
/// <param name="attrs">Attributes for the field</param>
|
||||
/// <param name="name">The name of the input field</param>
|
||||
/// <param name="labelText">The text of the <c>label</c> element associated with this <c>input</c></param>
|
||||
/// <param name="value">The value of the <c>input</c> field</param>
|
||||
/// <param name="extra">Any extra elements to include after the <c>input</c> and <c>label</c></param>
|
||||
/// <returns>A <c>div</c> element with the <input type=password> field constructed</returns>
|
||||
let passwordField attrs name labelText value extra =
|
||||
inputField "password" attrs name labelText value extra
|
||||
|
||||
/// Create a select (dropdown) field
|
||||
/// <summary>Create a select (dropdown) field</summary>
|
||||
/// <typeparam name="T">The type of value in the backing list</typeparam>
|
||||
/// <typeparam name="a">The type of the <c>value</c> attribute</typeparam>
|
||||
/// <param name="attrs">Attributes for the field</param>
|
||||
/// <param name="name">The name of the input field</param>
|
||||
/// <param name="labelText">The text of the <c>label</c> element associated with this <c>input</c></param>
|
||||
/// <param name="value">The value of the <c>input</c> field</param>
|
||||
/// <param name="values">The backing list for this dropdown</param>
|
||||
/// <param name="idFunc">The function to extract the ID (<c>value</c> attribute)</param>
|
||||
/// <param name="displayFunc">The function to extract the displayed version of the item</param>
|
||||
/// <param name="extra">Any extra elements to include after the <c>input</c> and <c>label</c></param>
|
||||
/// <returns>A <c>div</c> element with the <select> field constructed</returns>
|
||||
let selectField<'T, 'a>
|
||||
attrs name labelText value (values: 'T seq) (idFunc: 'T -> 'a) (displayFunc: 'T -> string) extra =
|
||||
let cssClass, attrs = extractAttrValue "class" attrs
|
||||
@@ -106,7 +167,13 @@ let selectField<'T, 'a>
|
||||
yield! extra
|
||||
]
|
||||
|
||||
/// Create a checkbox input styled as a switch
|
||||
/// <summary>Create a checkbox input styled as a switch</summary>
|
||||
/// <param name="attrs">Attributes for the field</param>
|
||||
/// <param name="name">The name of the input field</param>
|
||||
/// <param name="labelText">The text of the <c>label</c> element associated with this <c>input</c></param>
|
||||
/// <param name="value">Whether the checkbox should be checked or not</param>
|
||||
/// <param name="extra">Any extra elements to include after the <c>input</c> and <c>label</c></param>
|
||||
/// <returns>A <c>div</c> element with the switch-style <input type=checkbox> field constructed</returns>
|
||||
let checkboxSwitch attrs name labelText (value: bool) extra =
|
||||
let cssClass, attrs = extractAttrValue "class" attrs
|
||||
div [ _class $"""form-check form-switch {defaultArg cssClass ""}""" ] [
|
||||
@@ -117,17 +184,17 @@ let checkboxSwitch attrs name labelText (value: bool) extra =
|
||||
yield! extra
|
||||
]
|
||||
|
||||
/// A standard save button
|
||||
/// <summary>A standard save button</summary>
|
||||
let saveButton =
|
||||
button [ _type "submit"; _class "btn btn-sm btn-primary" ] [ raw "Save Changes" ]
|
||||
|
||||
/// A spacer bullet to use between action links
|
||||
/// <summary>A spacer bullet to use between action links</summary>
|
||||
let actionSpacer =
|
||||
span [ _class "text-muted" ] [ raw " • " ]
|
||||
|
||||
/// Functions for generating content in varying layouts
|
||||
/// <summary>Functions for generating content in varying layouts</summary>
|
||||
module Layout =
|
||||
|
||||
|
||||
/// Generate the title tag for a page
|
||||
let private titleTag (app: AppViewContext) =
|
||||
title [] [ txt app.PageTitle; raw " « Admin « "; txt app.WebLog.Name ]
|
||||
@@ -221,15 +288,21 @@ module Layout =
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
/// Render a page with a partial layout (htmx request)
|
||||
|
||||
/// <summary>Render a page with a partial layout (htmx request)</summary>
|
||||
/// <param name="content">A function that, when given a view context, will return a view</param>
|
||||
/// <param name="app">The app view context to use when rendering the view</param>
|
||||
/// <returns>A constructed Giraffe View Engine view</returns>
|
||||
let partial content app =
|
||||
html [ _lang "en" ] [
|
||||
titleTag app
|
||||
yield! pageView content app
|
||||
]
|
||||
|
||||
/// Render a page with a full layout
|
||||
|
||||
/// <summary>Render a page with a full layout</summary>
|
||||
/// <param name="content">A function that, when given a view context, will return a view</param>
|
||||
/// <param name="app">The app view context to use when rendering the view</param>
|
||||
/// <returns>A constructed Giraffe View Engine view</returns>
|
||||
let full content app =
|
||||
html [ _lang "en" ] [
|
||||
meta [ _name "viewport"; _content "width=device-width, initial-scale=1" ]
|
||||
@@ -248,8 +321,11 @@ module Layout =
|
||||
script [ _src (relUrl app "themes/admin/admin.js") ] []
|
||||
]
|
||||
]
|
||||
|
||||
/// Render a bare layout
|
||||
|
||||
/// <summary>Render a bare layout</summary>
|
||||
/// <param name="content">A function that, when given a view context, will return a view</param>
|
||||
/// <param name="app">The app view context to use when rendering the view</param>
|
||||
/// <returns>A constructed Giraffe View Engine view</returns>
|
||||
let bare (content: AppViewContext -> XmlNode list) app =
|
||||
html [ _lang "en" ] [
|
||||
title [] []
|
||||
@@ -260,14 +336,17 @@ module Layout =
|
||||
// ~~ SHARED TEMPLATES BETWEEN POSTS AND PAGES
|
||||
open Giraffe.Htmx.Common
|
||||
|
||||
/// The round-trip instant pattern
|
||||
/// <summary>The round-trip instant pattern</summary>
|
||||
let roundTrip = InstantPattern.CreateWithInvariantCulture "uuuu'-'MM'-'dd'T'HH':'mm':'ss'.'fffffff"
|
||||
|
||||
/// Capitalize the first letter in the given string
|
||||
let private capitalize (it: string) =
|
||||
$"{(string it[0]).ToUpper()}{it[1..]}"
|
||||
|
||||
/// The common edit form shared by pages and posts
|
||||
/// <summary>The common edit form shared by pages and posts</summary>
|
||||
/// <param name="model">The model to use to render this view</param>
|
||||
/// <param name="app">The app view context to use to render this view</param>
|
||||
/// <returns>A common edit view</returns>
|
||||
let commonEdit (model: EditCommonModel) app = [
|
||||
textField [ _class "mb-3"; _required; _autofocus ] (nameof model.Title) "Title" model.Title []
|
||||
textField [ _class "mb-3"; _required ] (nameof model.Permalink) "Permalink" model.Permalink [
|
||||
@@ -301,13 +380,18 @@ let commonEdit (model: EditCommonModel) app = [
|
||||
]
|
||||
|
||||
|
||||
/// Display a common template list
|
||||
/// <summary>Display a common template list</summary>
|
||||
/// <param name="model">The edit model</param>
|
||||
/// <param name="templates">A list of available templates for this page or post</param>
|
||||
/// <returns>A <select> element to allow a template to be selected</returns>
|
||||
let commonTemplates (model: EditCommonModel) (templates: MetaItem seq) =
|
||||
selectField [ _class "mb-3" ] (nameof model.Template) $"{capitalize model.Entity} Template" model.Template templates
|
||||
(_.Name) (_.Value) []
|
||||
_.Name _.Value []
|
||||
|
||||
|
||||
/// Display the metadata item edit form
|
||||
/// <summary>Display the metadata item edit form</summary>
|
||||
/// <param name="model">The edit model</param>
|
||||
/// <returns>A form for editing metadata</returns>
|
||||
let commonMetaItems (model: EditCommonModel) =
|
||||
let items = Array.zip model.MetaNames model.MetaValues
|
||||
let metaDetail idx (name, value) =
|
||||
@@ -320,7 +404,7 @@ let commonMetaItems (model: EditCommonModel) =
|
||||
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 "
|
||||
@@ -342,7 +426,10 @@ let commonMetaItems (model: EditCommonModel) =
|
||||
]
|
||||
|
||||
|
||||
/// Revision preview template
|
||||
/// <summary>Revision preview template</summary>
|
||||
/// <param name="rev">The revision to preview</param>
|
||||
/// <param name="app">The app view context to use when rendering the preview</param>
|
||||
/// <returns>A view with a revision preview</returns>
|
||||
let commonPreview (rev: Revision) app =
|
||||
div [ _class "mwl-revision-preview mb-3" ] [
|
||||
rev.Text.AsHtml() |> addBaseToRelativeUrls app.WebLog.ExtraPath |> raw
|
||||
@@ -350,7 +437,10 @@ let commonPreview (rev: Revision) app =
|
||||
|> List.singleton
|
||||
|
||||
|
||||
/// Form to manage permalinks for pages or posts
|
||||
/// <summary>Form to manage permalinks for pages or posts</summary>
|
||||
/// <param name="model">The manage permalinks model to be rendered</param>
|
||||
/// <param name="app">The app view context to use when rendering this view</param>
|
||||
/// <returns>A view for managing permalinks for a page or post</returns>
|
||||
let managePermalinks (model: ManagePermalinksModel) app = [
|
||||
let baseUrl = relUrl app $"admin/{model.Entity}/"
|
||||
let linkDetail idx link =
|
||||
@@ -414,7 +504,10 @@ let managePermalinks (model: ManagePermalinksModel) app = [
|
||||
]
|
||||
]
|
||||
|
||||
/// Form to manage revisions for pages or posts
|
||||
/// <summary>Form to manage revisions for pages or posts</summary>
|
||||
/// <param name="model">The manage revisions model to be rendered</param>
|
||||
/// <param name="app">The app view context to use when rendering this view</param>
|
||||
/// <returns>A view for managing revisions for a page or post</returns>
|
||||
let manageRevisions (model: ManageRevisionsModel) app = [
|
||||
let revUrlBase = relUrl app $"admin/{model.Entity}/{model.Id}/revision"
|
||||
let revDetail idx (rev: Revision) =
|
||||
|
||||
Reference in New Issue
Block a user