Migrate custom feed page to GVE

- Allow IDs to be overridden
This commit is contained in:
Daniel J. Summers 2024-03-14 22:50:37 -04:00
parent b85cae2241
commit 91046c643e
5 changed files with 240 additions and 280 deletions

View File

@ -431,20 +431,23 @@ let editCustomFeed feedId : HttpHandler = requireAccess WebLogAdmin >=> fun next
| _ -> ctx.WebLog.Rss.CustomFeeds |> List.tryFind (fun f -> f.Id = CustomFeedId feedId)
match customFeed with
| Some f ->
hashForPage $"""{if feedId = "new" then "Add" else "Edit"} Custom RSS Feed"""
|> withAntiCsrf ctx
|> addToHash ViewContext.Model (EditCustomFeedModel.FromFeed f)
|> addToHash "medium_values" [|
KeyValuePair.Create("", "– Unspecified –")
KeyValuePair.Create(string Podcast, "Podcast")
KeyValuePair.Create(string Music, "Music")
KeyValuePair.Create(string Video, "Video")
KeyValuePair.Create(string Film, "Film")
KeyValuePair.Create(string Audiobook, "Audiobook")
KeyValuePair.Create(string Newsletter, "Newsletter")
KeyValuePair.Create(string Blog, "Blog")
|]
|> adminView "custom-feed-edit" next ctx
let ratings = [
{ Name = string Yes; Value = "Yes" }
{ Name = string No; Value = "No" }
{ Name = string Clean; Value = "Clean" }
]
let mediums = [
{ Name = ""; Value = "– Unspecified –" }
{ Name = string Podcast; Value = "Podcast" }
{ Name = string Music; Value = "Music" }
{ Name = string Video; Value = "Video" }
{ Name = string Film; Value = "Film" }
{ Name = string Audiobook; Value = "Audiobook" }
{ Name = string Newsletter; Value = "Newsletter" }
{ Name = string Blog; Value = "Blog" }
]
Views.Admin.feedEdit (EditCustomFeedModel.FromFeed f) ratings mediums
|> adminPage $"""{if feedId = "new" then "Add" else "Edit"} Custom RSS Feed""" true next ctx
| None -> Error.notFound next ctx
// POST /admin/settings/rss/save

View File

@ -81,6 +81,216 @@ let dashboard (model: DashboardModel) app = [
]
/// Custom RSS feed edit form
let feedEdit (model: EditCustomFeedModel) (ratings: MetaItem list) (mediums: MetaItem list) app = [
h2 [ _class "my-3" ] [ raw app.PageTitle ]
article [] [
form [ _action (relUrl app "admin/settings/rss/save"); _method "post"; _class "container" ] [
antiCsrf app
input [ _type "hidden"; _name "Id"; _value model.Id ]
div [ _class "row pb-3" ] [
div [ _class "col" ] [
a [ _href (relUrl app "admin/settings#rss-settings") ] [ raw "« Back to Settings" ]
]
]
div [ _class "row pb-3" ] [
div [ _class "col-12 col-lg-6" ] [
fieldset [ _class "container pb-0" ] [
legend [] [ raw "Identification" ]
div [ _class "row" ] [
div [ _class "col" ] [
textField [ _required ] (nameof model.Path) "Relative Feed Path" model.Path [
span [ _class "form-text fst-italic" ] [ raw "Appended to "; txt app.WebLog.UrlBase ]
]
]
]
div [ _class "row" ] [
div [ _class "col py-3 d-flex align-self-center justify-content-center" ] [
checkboxSwitch [ _onclick "Admin.checkPodcast()"; if model.IsPodcast then _checked ]
(nameof model.IsPodcast) "This Is a Podcast Feed" model.IsPodcast []
]
]
]
]
div [ _class "col-12 col-lg-6" ] [
fieldset [ _class "container pb-0" ] [
legend [] [ raw "Feed Source" ]
div [ _class "row d-flex align-items-center" ] [
div [ _class "col-1 d-flex justify-content-end pb-3" ] [
div [ _class "form-check form-check-inline me-0" ] [
input [ _type "radio"; _name (nameof model.SourceType); _id "SourceTypeCat"
_class "form-check-input"; _value "category"
if model.SourceType <> "tag" then _checked
_onclick "Admin.customFeedBy('category')" ]
label [ _for "SourceTypeCat"; _class "form-check-label d-none" ] [ raw "Category" ]
]
]
div [ _class "col-11 pb-3" ] [
let cats =
app.Categories
|> Seq.ofArray
|> Seq.map (fun c ->
let parents =
if c.ParentNames.Length = 0 then ""
else
c.ParentNames
|> Array.map (fun it -> $"{it} &rang; ")
|> String.concat ""
{ Name = c.Id; Value = $"{parents} {c.Name}".Trim() })
|> Seq.append [ { Name = ""; Value = "&ndash; Select Category &ndash;" } ]
|> List.ofSeq
selectField [ _id "SourceValueCat"; _required
if model.SourceType = "tag" then _disabled ]
(nameof model.SourceValue) "Category" model.SourceValue cats (_.Name)
(_.Value) []
]
div [ _class "col-1 d-flex justify-content-end pb-3" ] [
div [ _class "form-check form-check-inline me-0" ] [
input [ _type "radio"; _name (nameof model.SourceType); _id "SourceTypeTag"
_class "form-check-input"; _value "tag"
if model.SourceType= "tag" then _checked
_onclick "Admin.customFeedBy('tag')" ]
label [ _for "sourceTypeTag"; _class "form-check-label d-none" ] [ raw "Tag" ]
]
]
div [ _class "col-11 pb-3" ] [
textField [ _id "SourceValueTag"; _required
if model.SourceType <> "tag" then _disabled ]
(nameof model.SourceValue) "Tag"
(if model.SourceType = "tag" then model.SourceValue else "") []
]
]
]
]
]
div [ _class "row pb-3" ] [
div [ _class "col" ] [
fieldset [ _class "container"; _id "podcastFields"; if not model.IsPodcast then _disabled ] [
legend [] [ raw "Podcast Settings" ]
div [ _class "row" ] [
div [ _class "col-12 col-md-5 col-lg-4 offset-lg-1 pb-3" ] [
textField [ _required ] (nameof model.Title) "Title" model.Title []
]
div [ _class "col-12 col-md-4 col-lg-4 pb-3" ] [
textField [] (nameof model.Subtitle) "Podcast Subtitle" model.Subtitle []
]
div [ _class "col-12 col-md-3 col-lg-2 pb-3" ] [
numberField [ _required ] (nameof model.ItemsInFeed) "# Episodes" model.ItemsInFeed []
]
]
div [ _class "row" ] [
div [ _class "col-12 col-md-5 col-lg-4 offset-lg-1 pb-3" ] [
textField [ _required ] (nameof model.AppleCategory) "iTunes Category"
model.AppleCategory [
span [ _class "form-text fst-italic" ] [
a [ _href "https://www.thepodcasthost.com/planning/itunes-podcast-categories/"
_target "_blank"; _rel "noopener" ] [
raw "iTunes Category / Subcategory List"
]
]
]
]
div [ _class "col-12 col-md-4 pb-3" ] [
textField [] (nameof model.AppleSubcategory) "iTunes Subcategory" model.AppleSubcategory
[]
]
div [ _class "col-12 col-md-3 col-lg-2 pb-3" ] [
selectField [ _required ] (nameof model.Explicit) "Explicit Rating" model.Explicit
ratings (_.Name) (_.Value) []
]
]
div [ _class "row" ] [
div [ _class "col-12 col-md-6 col-lg-4 offset-xxl-1 pb-3" ] [
textField [ _required ] (nameof model.DisplayedAuthor) "Displayed Author"
model.DisplayedAuthor []
]
div [ _class "col-12 col-md-6 col-lg-4 pb-3" ] [
emailField [ _required ] (nameof model.Email) "Author E-mail" model.Email [
span [ _class "form-text fst-italic" ] [
raw "For iTunes, must match registered e-mail"
]
]
]
div [ _class "col-12 col-sm-5 col-md-4 col-lg-4 col-xl-3 offset-xl-1 col-xxl-2 offset-xxl-0 pb-3" ] [
textField [] (nameof model.DefaultMediaType) "Default Media Type"
model.DefaultMediaType [
span [ _class "form-text fst-italic" ] [ raw "Optional; blank for no default" ]
]
]
div [ _class "col-12 col-sm-7 col-md-8 col-lg-10 offset-lg-1 pb-3" ] [
textField [ _required ] (nameof model.ImageUrl) "Image URL" model.ImageUrl [
span [ _class "form-text fst-italic"] [
raw "Relative URL will be appended to "; txt app.WebLog.UrlBase; raw "/"
]
]
]
]
div [ _class "row pb-3" ] [
div [ _class "col-12 col-lg-10 offset-lg-1" ] [
textField [ _required ] (nameof model.Summary) "Summary" model.Summary [
span [ _class "form-text fst-italic" ] [ raw "Displayed in podcast directories" ]
]
]
]
div [ _class "row pb-3" ] [
div [ _class "col-12 col-lg-10 offset-lg-1" ] [
textField [] (nameof model.MediaBaseUrl) "Media Base URL" model.MediaBaseUrl [
span [ _class "form-text fst-italic" ] [
raw "Optional; prepended to episode media file if present"
]
]
]
]
div [ _class "row" ] [
div [ _class "col-12 col-lg-5 offset-lg-1 pb-3" ] [
textField [] (nameof model.FundingUrl) "Funding URL" model.FundingUrl [
span [ _class "form-text fst-italic" ] [
raw "Optional; URL describing donation options for this podcast, "
raw "relative URL supported"
]
]
]
div [ _class "col-12 col-lg-5 pb-3" ] [
textField [ _maxlength "128" ] (nameof model.FundingText) "Funding Text"
model.FundingText [
span [ _class "form-text fst-italic" ] [ raw "Optional; text for the funding link" ]
]
]
]
div [ _class "row pb-3" ] [
div [ _class "col-8 col-lg-5 offset-lg-1 pb-3" ] [
textField [] (nameof model.PodcastGuid) "Podcast GUID" model.PodcastGuid [
span [ _class "form-text fst-italic" ] [
raw "Optional; v5 UUID uniquely identifying this podcast; "
raw "once entered, do not change this value ("
a [ _href "https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#guid"
_target "_blank"; _rel "noopener" ] [
raw "documentation"
]; raw ")"
]
]
]
div [ _class "col-4 col-lg-3 offset-lg-2 pb-3" ] [
selectField [] (nameof model.Medium) "Medium" model.Medium mediums (_.Name) (_.Value) [
span [ _class "form-text fst-italic" ] [
raw "Optional; medium of the podcast content ("
a [ _href "https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#medium"
_target "_blank"; _rel "noopener" ] [
raw "documentation"
]; raw ")"
]
]
]
]
]
]
]
div [ _class "row pb-3" ] [ div [ _class "col text-center" ] [ saveButton ] ]
]
]
]
/// Redirect Rule edit form
let redirectEdit (model: EditRedirectRuleModel) app = [
let url = relUrl app $"admin/settings/redirect-rules/{model.RuleId}"

View File

@ -104,11 +104,17 @@ let yesOrNo value =
/// 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 name; _class "form-control"; _placeholder labelText; _value value ]
|> List.append attrs
[ _type fieldType; _name name; _id fieldId; _class "form-control"; _placeholder labelText; _value value ]
|> List.append newAttrs
|> input
label [ _for name ] [ raw labelText ]
label [ _for fieldId ] [ raw labelText ]
yield! extra
]
@ -135,7 +141,7 @@ let selectField<'T, 'a>
select ([ _name name; _id name; _class "form-control" ] |> List.append attrs) [
for item in values do
let itemId = string (idFunc item)
option [ _value itemId; if value = itemId then _selected ] [ txt (displayFunc item) ]
option [ _value itemId; if value = itemId then _selected ] [ raw (displayFunc item) ]
]
label [ _for name ] [ raw labelText ]
yield! extra

View File

@ -1,259 +0,0 @@
<h2 class=my-3>{{ page_title }}</h2>
<article>
<form action="{{ "admin/settings/rss/save" | relative_link }}" method=post>
<input type=hidden name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
<input type=hidden name=Id value="{{ model.id }}">
{%- assign typ = model.source_type -%}
<div class=container>
<div class="row pb-3">
<div class=col>
<a href="{{ "admin/settings#rss-settings" | relative_link }}">&laquo; Back to Settings</a>
</div>
</div>
<div class="row pb-3">
<div class="col-12 col-lg-6">
<fieldset class="container pb-0">
<legend>Identification</legend>
<div class=row>
<div class=col>
<div class=form-floating>
<input type=text name=Path id=path class=form-control placeholder="Relative Feed Path"
value="{{ model.path }}">
<label for=path>Relative Feed Path</label>
<span class="form-text fst-italic">Appended to {{ web_log.url_base }}/</span>
</div>
</div>
</div>
<div class=row>
<div class="col py-3 d-flex align-self-center justify-content-center">
<div class="form-check form-switch">
<input type=checkbox name=IsPodcast id=isPodcast class=form-check-input value=true
{%- if model.is_podcast %} checked{% endif %} onclick="Admin.checkPodcast()">
<label for=isPodcast class=form-check-label>This Is a Podcast Feed</label>
</div>
</div>
</div>
</fieldset>
</div>
<div class="col-12 col-lg-6">
<fieldset class="container pb-0">
<legend>Feed Source</legend>
<div class="row d-flex align-items-center">
<div class="col-1 d-flex justify-content-end pb-3">
<div class="form-check form-check-inline me-0">
<input type=radio name=SourceType id=sourceTypeCat class=form-check-input value=category
{%- unless typ == "tag" %} checked{% endunless %} onclick="Admin.customFeedBy('category')">
<label for=sourceTypeCat class="form-check-label d-none">Category</label>
</div>
</div>
<div class="col-11 pb-3">
<div class=form-floating>
<select name=SourceValue id=sourceValueCat class=form-control required
{%- if typ == "tag" %} disabled{% endif %}>
<option value="">&ndash; Select Category &ndash;</option>
{% for cat in categories -%}
<option value="{{ cat.id }}"
{%- if typ != "tag" and model.source_value == cat.id %}selected{% endif -%}>
{% for it in cat.parent_names %}
{{ it }} &rang;
{% endfor %}
{{ cat.name }}
</option>
{%- endfor %}
</select>
<label for=sourceValueCat>Category</label>
</div>
</div>
<div class="col-1 d-flex justify-content-end pb-3">
<div class="form-check form-check-inline me-0">
<input type=radio name=SourceType id=sourceTypeTag class=form-check-input value=tag
{%- if typ == "tag" %} checked{% endif %} onclick="Admin.customFeedBy('tag')">
<label for="sourceTypeTag" class="form-check-label d-none">Tag</label>
</div>
</div>
<div class="col-11 pb-3">
<div class=form-floating>
<input type=text name=SourceValue id=sourceValueTag class=form-control placeholder=Tag
{%- unless typ == "tag" %} disabled{% endunless %} required
{%- if typ == "tag" %} value="{{ model.source_value }}"{% endif %}>
<label for=sourceValueTag>Tag</label>
</div>
</div>
</div>
</fieldset>
</div>
</div>
<div class="row pb-3">
<div class=col>
<fieldset class=container id=podcastFields{% unless model.is_podcast %} disabled=disabled{% endunless %}>
<legend>Podcast Settings</legend>
<div class=row>
<div class="col-12 col-md-5 col-lg-4 offset-lg-1 pb-3">
<div class=form-floating>
<input type=text name=Title id=title class=form-control placeholder=Title required
value="{{ model.title }}">
<label for=title>Title</label>
</div>
</div>
<div class="col-12 col-md-4 col-lg-4 pb-3">
<div class=form-floating>
<input type=text name=Subtitle id=subtitle class=form-control placeholder=Subtitle
value="{{ model.subtitle }}">
<label for=subtitle>Podcast Subtitle</label>
</div>
</div>
<div class="col-12 col-md-3 col-lg-2 pb-3">
<div class=form-floating>
<input type=number name=ItemsInFeed id=itemsInFeed class=form-control placeholder=Items required
value="{{ model.items_in_feed }}">
<label for=itemsInFeed># Episodes</label>
</div>
</div>
</div>
<div class=row>
<div class="col-12 col-md-5 col-lg-4 offset-lg-1 pb-3">
<div class=form-floating>
<input type=text name=AppleCategory id=appleCategory class=form-control placeholder="iTunes Category"
required value="{{ model.apple_category }}">
<label for=appleCategory>iTunes Category</label>
<span class="form-text fst-italic">
<a href="https://www.thepodcasthost.com/planning/itunes-podcast-categories/" target=_blank
rel=noopener>
iTunes Category / Subcategory List
</a>
</span>
</div>
</div>
<div class="col-12 col-md-4 pb-3">
<div class=form-floating>
<input type=text name=AppleSubcategory id=appleSubcategory class=form-control
placeholder="iTunes Subcategory" value="{{ model.apple_subcategory }}">
<label for=appleSubcategory>iTunes Subcategory</label>
</div>
</div>
<div class="col-12 col-md-3 col-lg-2 pb-3">
<div class=form-floating>
<select name=Explicit id=explicit class=form-control required>
<option value="yes"{% if model.explicit == "yes" %} selected{% endif %}>Yes</option>
<option value="no"{% if model.explicit == "no" %} selected{% endif %}>No</option>
<option value="clean"{% if model.explicit == "clean" %} selected{% endif %}>Clean</option>
</select>
<label for=explicit>Explicit Rating</label>
</div>
</div>
</div>
<div class=row>
<div class="col-12 col-md-6 col-lg-4 offset-xxl-1 pb-3">
<div class=form-floating>
<input type=text name=DisplayedAuthor id=displayedAuthor class=form-control placeholder=Author
required value="{{ model.displayed_author }}">
<label for=displayedAuthor>Displayed Author</label>
</div>
</div>
<div class="col-12 col-md-6 col-lg-4 pb-3">
<div class=form-floating>
<input type=email name=Email id=email class=form-control placeholder=Email required
value="{{ model.email }}">
<label for=email>Author E-mail</label>
<span class="form-text fst-italic">For iTunes, must match registered e-mail</span>
</div>
</div>
<div class="col-12 col-sm-5 col-md-4 col-lg-4 col-xl-3 offset-xl-1 col-xxl-2 offset-xxl-0">
<div class=form-floating>
<input type=text name=DefaultMediaType id=defaultMediaType class=form-control
placeholder="Media Type" value="{{ model.default_media_type }}">
<label for=defaultMediaType>Default Media Type</label>
<span class="form-text fst-italic">Optional; blank for no default</span>
</div>
</div>
<div class="col-12 col-sm-7 col-md-8 col-lg-10 offset-lg-1">
<div class=form-floating>
<input type=text name=ImageUrl id=imageUrl class=form-control placeholder="Image URL" required
value="{{ model.image_url }}">
<label for=imageUrl>Image URL</label>
<span class="form-text fst-italic">Relative URL will be appended to {{ web_log.url_base }}/</span>
</div>
</div>
</div>
<div class="row pb-3">
<div class="col-12 col-lg-10 offset-lg-1">
<div class=form-floating>
<input type=text name=Summary id=summary class=form-control placeholder=Summary required
value="{{ model.summary }}">
<label for=summary>Summary</label>
<span class="form-text fst-italic">Displayed in podcast directories</span>
</div>
</div>
</div>
<div class="row pb-3">
<div class="col-12 col-lg-10 offset-lg-1">
<div class=form-floating>
<input type=text name=MediaBaseUrl id=mediaBaseUrl class=form-control placeholder="Media Base URL"
value="{{ model.media_base_url }}">
<label for=mediaBaseUrl>Media Base URL</label>
<span class="form-text fst-italic">Optional; prepended to episode media file if present</span>
</div>
</div>
</div>
<div class=row>
<div class="col-12 col-lg-5 offset-lg-1 pb-3">
<div class=form-floating>
<input type=text name=FundingUrl id=fundingUrl class=form-control placeholder="Funding URL"
value="{{ model.funding_url }}">
<label for=fundingUrl>Funding URL</label>
<span class="form-text fst-italic">
Optional; URL describing donation options for this podcast, relative URL supported
</span>
</div>
</div>
<div class="col-12 col-lg-5 pb-3">
<div class=form-floating>
<input type=text name=FundingText id=fundingText class=form-control maxlength=128
placeholder="Funding Text" value="{{ model.funding_text }}">
<label for=fundingText>Funding Text</label>
<span class="form-text fst-italic">Optional; text for the funding link</span>
</div>
</div>
</div>
<div class="row pb-3">
<div class="col-8 col-lg-5 offset-lg-1 pb-3">
<div class=form-floating>
<input type=text name=PodcastGuid id=guid class=form-control placeholder=GUID
value="{{ model.podcast_guid }}">
<label for=guid>Podcast GUID</label>
<span class="form-text fst-italic">
Optional; v5 UUID uniquely identifying this podcast; once entered, do not change this value
(<a href=https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#guid
target=_blank rel=noopener>documentation</a>)
</span>
</div>
</div>
<div class="col-4 col-lg-3 offset-lg-2 pb-3">
<div class=form-floating>
<select name=Medium id=medium class=form-control>
{% for med in medium_values -%}
<option value="{{ med[0] }}"{% if model.medium == med[0] %} selected{% endif %}>
{{ med[1] }}
</option>
{%- endfor %}
</select>
<label for=medium>Medium</label>
<span class="form-text fst-italic">
Optional; medium of the podcast content
(<a href=https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#medium
target=_blank rel=noopener>documentation</a>)
</span>
</div>
</div>
</div>
</fieldset>
</div>
</div>
<div class="row pb-3">
<div class="col text-center">
<button type=submit class="btn btn-primary">Save Changes</button>
</div>
</div>
</div>
</form>
</article>

View File

@ -249,7 +249,7 @@ this.Admin = {
* Check to enable or disable podcast fields
*/
checkPodcast() {
document.getElementById("podcastFields").disabled = !document.getElementById("isPodcast").checked
document.getElementById("podcastFields").disabled = !document.getElementById("IsPodcast").checked
},
/**
@ -269,8 +269,8 @@ this.Admin = {
* @param {string} source The source that was selected
*/
customFeedBy(source) {
const categoryInput = document.getElementById("sourceValueCat")
const tagInput = document.getElementById("sourceValueTag")
const categoryInput = document.getElementById("SourceValueCat")
const tagInput = document.getElementById("SourceValueTag")
if (source === "category") {
tagInput.value = ""
tagInput.disabled = true