Version 2.1 #41
@ -178,10 +178,6 @@ let addToHash key (value: obj) (hash: Hash) =
|
||||
if hash.ContainsKey key then hash[key] <- value else hash.Add(key, value)
|
||||
hash
|
||||
|
||||
/// Add anti-CSRF tokens to the given hash
|
||||
let withAntiCsrf (ctx: HttpContext) =
|
||||
addToHash ViewContext.AntiCsrfTokens ctx.CsrfTokenSet
|
||||
|
||||
open System.Security.Claims
|
||||
open Giraffe
|
||||
open Giraffe.Htmx
|
||||
@ -362,10 +358,6 @@ let themedView template next ctx hash = task {
|
||||
/// The ID for the admin theme
|
||||
let adminTheme = ThemeId "admin"
|
||||
|
||||
/// Display a view for the admin theme
|
||||
let adminView template =
|
||||
viewForTheme adminTheme template
|
||||
|
||||
/// Display a bare view for the admin theme
|
||||
let adminBareView template =
|
||||
bareForTheme adminTheme template
|
||||
|
@ -272,21 +272,12 @@ let edit postId : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||
| Some (title, post) when canEdit post.AuthorId ctx ->
|
||||
let! templates = templatesForTheme ctx "post"
|
||||
let model = EditPostModel.FromPost ctx.WebLog post
|
||||
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
|
||||
|> addToHash "explicit_values" [|
|
||||
KeyValuePair.Create("", "– Default –")
|
||||
KeyValuePair.Create(string Yes, "Yes")
|
||||
KeyValuePair.Create(string No, "No")
|
||||
KeyValuePair.Create(string Clean, "Clean")
|
||||
|]
|
||||
|> adminView "post-edit" next ctx
|
||||
let ratings = [
|
||||
{ Name = string Yes; Value = "Yes" }
|
||||
{ Name = string No; Value = "No" }
|
||||
{ Name = string Clean; Value = "Clean" }
|
||||
]
|
||||
return! adminPage title true next ctx (Views.Post.postEdit model templates ratings)
|
||||
| Some _ -> return! Error.notAuthorized next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
@ -276,3 +276,246 @@ let list (model: PostDisplay) app = [
|
||||
p [ _class "text-muted fst-italic text-center" ] [ raw "This web log has no posts" ]
|
||||
]
|
||||
]
|
||||
|
||||
let postEdit (model: EditPostModel) templates (ratings: MetaItem list) app = [
|
||||
h2 [ _class "my-3" ] [ raw app.PageTitle ]
|
||||
article [] [
|
||||
form [ _action (relUrl app "admin/post/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-12 col-lg-9" ] [
|
||||
yield! commonEdit model app
|
||||
textField [ _class "mb-3" ] (nameof model.Tags) "Tags" model.Tags [
|
||||
div [ _class "form-text" ] [ raw "comma-delimited" ]
|
||||
]
|
||||
if model.Status = string Draft then
|
||||
checkboxSwitch [ _class "mb-2" ] (nameof model.DoPublish) "Publish This Post" model.DoPublish []
|
||||
saveButton
|
||||
hr [ _class "mb-3" ]
|
||||
fieldset [ _class "mb-3" ] [
|
||||
legend [] [
|
||||
span [ _class "form-check form-switch" ] [
|
||||
small [] [
|
||||
input [ _type "checkbox"; _name (nameof model.IsEpisode)
|
||||
_id (nameof model.IsEpisode); _class "form-check-input"; _value "true"
|
||||
_data "bs-toggle" "collapse"; _data "bs-target" "#episode_items"
|
||||
_onclick "Admin.toggleEpisodeFields()"; if model.IsEpisode then _checked ]
|
||||
]
|
||||
label [ _for (nameof model.IsEpisode) ] [ raw "Podcast Episode" ]
|
||||
]
|
||||
]
|
||||
div [ _id "episode_items"
|
||||
_class $"""container p-0 collapse{if model.IsEpisode then " show" else ""}""" ] [
|
||||
div [ _class "row" ] [
|
||||
div [ _class "col-12 col-md-8 pb-3" ] [
|
||||
textField [ _required ] (nameof model.Media) "Media File" model.Media [
|
||||
div [ _class "form-text" ] [
|
||||
raw "Relative URL will be appended to base media path (if set) "
|
||||
raw "or served from this web log"
|
||||
]
|
||||
]
|
||||
]
|
||||
div [ _class "col-12 col-md-4 pb-3" ] [
|
||||
textField [] (nameof model.MediaType) "Media MIME Type" model.MediaType [
|
||||
div [ _class "form-text" ] [ raw "Optional; overrides podcast default" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
div [ _class "row pb-3" ] [
|
||||
div [ _class "col" ] [
|
||||
numberField [ _required ] (nameof model.Length) "Media Length (bytes)"
|
||||
0 (* TODO: string model.Length *) [
|
||||
div [ _class "form-text" ] [ raw "TODO: derive from above file name" ]
|
||||
]
|
||||
]
|
||||
div [ _class "col" ] [
|
||||
textField [] (nameof model.Duration) "Duration" model.Duration [
|
||||
div [ _class "form-text" ] [
|
||||
raw "Recommended; enter in "; code [] [ raw "HH:MM:SS"]; raw " format"
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
div [ _class "row pb-3" ] [
|
||||
div [ _class "col" ] [
|
||||
textField [] (nameof model.Subtitle) "Subtitle" model.Subtitle [
|
||||
div [ _class "form-text" ] [ raw "Optional; a subtitle for this episode" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
div [ _class "row" ] [
|
||||
div [ _class "col-12 col-md-8 pb-3" ] [
|
||||
textField [] (nameof model.ImageUrl) "Image URL" model.ImageUrl [
|
||||
div [ _class "form-text" ] [
|
||||
raw "Optional; overrides podcast default; "
|
||||
raw "relative URL served from this web log"
|
||||
]
|
||||
]
|
||||
]
|
||||
div [ _class "col-12 col-md-4 pb-3" ] [
|
||||
selectField [] (nameof model.Explicit) "Explicit Rating" model.Explicit ratings
|
||||
(_.Name) (_.Value) [
|
||||
div [ _class "form-text" ] [ raw "Optional; overrides podcast default" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
div [ _class "row" ] [
|
||||
div [ _class "col-12 col-md-8 pb-3" ] [
|
||||
div [ _class "form-text" ] [ raw "Chapters" ]
|
||||
div [ _class "form-check form-check-inline" ] [
|
||||
input [ _type "radio"; _name (nameof model.ChapterSource)
|
||||
_id "chapter_source_none"; _value "none"; _class "form-check-input"
|
||||
if model.ChapterSource = "none" then _checked
|
||||
_onclick "Admin.setChapterSource('none')" ]
|
||||
label [ _for "chapter_source_none" ] [ raw "None" ]
|
||||
]
|
||||
div [ _class "form-check form-check-inline" ] [
|
||||
input [ _type "radio"; _name (nameof model.ChapterSource)
|
||||
_id "chapter_source_internal"; _value "internal"
|
||||
_class "form-check-input"
|
||||
if model.ChapterSource= "internal" then _checked
|
||||
_onclick "Admin.setChapterSource('internal')" ]
|
||||
label [ _for "chapter_source_internal" ] [ raw "Defined Here" ]
|
||||
]
|
||||
div [ _class "form-check form-check-inline" ] [
|
||||
input [ _type "radio"; _name (nameof model.ChapterSource)
|
||||
_id "chapter_source_external"; _value "external"
|
||||
_class "form-check-input"
|
||||
if model.ChapterSource = "external" then _checked
|
||||
_onclick "Admin.setChapterSource('external')" ]
|
||||
label [ _for "chapter_source_external" ] [ raw "Separate File" ]
|
||||
]
|
||||
]
|
||||
div [ _class "col-md-4 d-flex justify-content-center" ] [
|
||||
checkboxSwitch [ _class "align-self-center pb-3" ] (nameof model.ContainsWaypoints)
|
||||
"Chapters contain waypoints" model.ContainsWaypoints []
|
||||
]
|
||||
]
|
||||
div [ _class "row" ] [
|
||||
div [ _class "col-12 col-md-8 pb-3" ] [
|
||||
textField [] (nameof model.ChapterFile) "Chapter File" model.ChapterFile [
|
||||
div [ _class "form-text" ] [ raw "Relative URL served from this web log" ]
|
||||
]
|
||||
]
|
||||
div [ _class "col-12 col-md-4 pb-3" ] [
|
||||
textField [] (nameof model.ChapterType) "Chapter MIME Type" model.ChapterType [
|
||||
div [ _class "form-text" ] [
|
||||
raw "Optional; "; code [] [ raw "application/json+chapters" ]
|
||||
raw " assumed if chapter file ends with "; code [] [ raw ".json" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
div [ _class "row" ] [
|
||||
div [ _class "col-12 col-md-8 pb-3" ] [
|
||||
textField [] (nameof model.TranscriptUrl) "Transcript URL" model.TranscriptUrl [
|
||||
div [ _class "form-text" ] [
|
||||
raw "Optional; relative URL served from this web log"
|
||||
]
|
||||
]
|
||||
]
|
||||
div [ _class "col-12 col-md-4 pb-3" ] [
|
||||
textField [] (nameof model.TranscriptType) "Transcript MIME Type"
|
||||
model.TranscriptType [
|
||||
div [ _class "form-text" ] [ raw "Required if transcript URL provided" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
div [ _class "row pb-3" ] [
|
||||
div [ _class "col" ] [
|
||||
textField [] (nameof model.TranscriptLang) "Transcript Language"
|
||||
model.TranscriptLang [
|
||||
div [ _class "form-text" ] [ raw "Optional; overrides podcast default" ]
|
||||
]
|
||||
]
|
||||
div [ _class "col d-flex justify-content-center" ] [
|
||||
checkboxSwitch [ _class "align-self-center pb-3" ] (nameof model.TranscriptCaptions)
|
||||
"This is a captions file" model.TranscriptCaptions []
|
||||
]
|
||||
]
|
||||
div [ _class "row pb-3" ] [
|
||||
div [ _class "col col-md-4" ] [
|
||||
numberField [] (nameof model.SeasonNumber) "Season Number" model.SeasonNumber [
|
||||
div [ _class "form-text" ] [ raw "Optional" ]
|
||||
]
|
||||
]
|
||||
div [ _class "col col-md-8" ] [
|
||||
textField [ _maxlength "128" ] (nameof model.SeasonDescription) "Season Description"
|
||||
model.SeasonDescription [
|
||||
div [ _class "form-text" ] [ raw "Optional" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
div [ _class "row pb-3" ] [
|
||||
div [ _class "col col-md-4" ] [
|
||||
numberField [ _step "0.01" ] (nameof model.EpisodeNumber) "Episode Number"
|
||||
0 (* TODO: model.EpisodeNumber *) [
|
||||
div [ _class "form-text" ] [ raw "Optional; up to 2 decimal points" ]
|
||||
]
|
||||
]
|
||||
div [ _class "col col-md-8" ] [
|
||||
textField [ _maxlength "128" ] (nameof model.EpisodeDescription)
|
||||
"Episode Description" model.EpisodeDescription [
|
||||
div [ _class "form-text" ] [ raw "Optional" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
script [] [
|
||||
raw """document.addEventListener("DOMContentLoaded", () => Admin.toggleEpisodeFields())"""
|
||||
]
|
||||
]
|
||||
commonMetaItems model
|
||||
if model.Status = string Published then
|
||||
fieldset [ _class "pb-3" ] [
|
||||
legend [] [ raw "Maintenance" ]
|
||||
div [ _class "container" ] [
|
||||
div [ _class "row" ] [
|
||||
div [ _class "col align-self-center" ] [
|
||||
checkboxSwitch [ _class "pb-2" ] (nameof model.SetPublished)
|
||||
"Set Published Date" model.SetPublished []
|
||||
]
|
||||
div [ _class "col-4" ] [
|
||||
div [ _class "form-floating" ] [
|
||||
input [ _type "datetime-local"; _name (nameof model.PubOverride)
|
||||
_id (nameof model.PubOverride); _class "form-control"
|
||||
_placeholder "Override Date"
|
||||
if model.PubOverride.HasValue then
|
||||
_value (model.PubOverride.Value.ToString "yyyy-MM-dd\THH:mm") ]
|
||||
label [ _for (nameof model.PubOverride); _class "form-label" ] [
|
||||
raw "Published On"
|
||||
]
|
||||
]
|
||||
]
|
||||
div [ _class "col-5 align-self-center" ] [
|
||||
checkboxSwitch [ _class "pb-2" ] (nameof model.SetUpdated)
|
||||
"Purge revisions and<br>set as updated date as well"
|
||||
model.SetUpdated []
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
div [ _class "col-12 col-lg-3" ] [
|
||||
commonTemplates model templates
|
||||
fieldset [] [
|
||||
legend [] [ raw "Categories" ]
|
||||
for cat in app.Categories do
|
||||
div [ _class "form-check" ] [
|
||||
input [ _type "checkbox"; _name (nameof model.CategoryIds); _id $"category_{cat.Id}"
|
||||
_class "form-check-input"; _value cat.Id
|
||||
if model.CategoryIds |> Array.contains cat.Id then _checked ]
|
||||
label [ _for $"category_{cat.Id}"; _class "form-check-label"
|
||||
match cat.Description with Some it -> _title it | None -> () ] [
|
||||
yield! cat.ParentNames |> Array.map (fun _ -> raw " ⟩ ")
|
||||
txt cat.Name
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
script [] [ raw "window.setTimeout(() => Admin.toggleEpisodeFields(), 500)" ]
|
||||
]
|
||||
|
Loading…
Reference in New Issue
Block a user