WIP on chapter edit page (#6)
This commit is contained in:
parent
90bca34be3
commit
12b23eab46
src
MyWebLog.Domain
MyWebLog.Tests
MyWebLog
admin-theme
@ -190,7 +190,16 @@ type Chapter = {
|
|||||||
|
|
||||||
/// A location that applies to a chapter
|
/// A location that applies to a chapter
|
||||||
Location: Location option
|
Location: Location option
|
||||||
}
|
} with
|
||||||
|
|
||||||
|
/// An empty chapter
|
||||||
|
static member Empty =
|
||||||
|
{ StartTime = Duration.Zero
|
||||||
|
Title = None
|
||||||
|
ImageUrl = None
|
||||||
|
IsHidden = None
|
||||||
|
EndTime = None
|
||||||
|
Location = None }
|
||||||
|
|
||||||
|
|
||||||
open NodaTime.Text
|
open NodaTime.Text
|
||||||
|
@ -72,9 +72,10 @@ type DisplayCategory = {
|
|||||||
PostCount: int
|
PostCount: int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// A display version of an episode chapter
|
/// A display version of an episode chapter
|
||||||
type DisplayChapter = {
|
type DisplayChapter = {
|
||||||
/// The start time of the chapter (HH:MM:SS.FF format)
|
/// The start time of the chapter (H:mm:ss.FF format)
|
||||||
StartTime: string
|
StartTime: string
|
||||||
|
|
||||||
/// The title of the chapter
|
/// The title of the chapter
|
||||||
@ -86,7 +87,7 @@ type DisplayChapter = {
|
|||||||
/// Whether this chapter should be displayed in podcast players
|
/// Whether this chapter should be displayed in podcast players
|
||||||
IsHidden: bool
|
IsHidden: bool
|
||||||
|
|
||||||
/// The end time of the chapter (HH:MM:SS.FF format)
|
/// The end time of the chapter (H:mm:ss.FF format)
|
||||||
EndTime: string
|
EndTime: string
|
||||||
|
|
||||||
/// The name of a location
|
/// The name of a location
|
||||||
@ -101,15 +102,15 @@ type DisplayChapter = {
|
|||||||
|
|
||||||
/// Create a display chapter from a chapter
|
/// Create a display chapter from a chapter
|
||||||
static member FromChapter (chapter: Chapter) =
|
static member FromChapter (chapter: Chapter) =
|
||||||
let pattern = DurationPattern.CreateWithInvariantCulture("H:mm:ss.ff")
|
let pattern = DurationPattern.CreateWithInvariantCulture "H:mm:ss.FF"
|
||||||
{ StartTime = pattern.Format(chapter.StartTime)
|
{ StartTime = pattern.Format chapter.StartTime
|
||||||
Title = defaultArg chapter.Title ""
|
Title = defaultArg chapter.Title ""
|
||||||
ImageUrl = defaultArg chapter.ImageUrl ""
|
ImageUrl = defaultArg chapter.ImageUrl ""
|
||||||
IsHidden = defaultArg chapter.IsHidden false
|
IsHidden = defaultArg chapter.IsHidden false
|
||||||
EndTime = chapter.EndTime |> Option.map pattern.Format |> Option.defaultValue ""
|
EndTime = chapter.EndTime |> Option.map pattern.Format |> Option.defaultValue ""
|
||||||
LocationName = chapter.Location |> Option.map (fun l -> l.Name) |> Option.defaultValue ""
|
LocationName = chapter.Location |> Option.map _.Name |> Option.defaultValue ""
|
||||||
LocationGeo = chapter.Location |> Option.map (fun l -> l.Geo) |> Option.flatten |> Option.defaultValue ""
|
LocationGeo = chapter.Location |> Option.map _.Geo |> Option.flatten |> Option.defaultValue ""
|
||||||
LocationOsm = chapter.Location |> Option.map (fun l -> l.Osm) |> Option.flatten |> Option.defaultValue "" }
|
LocationOsm = chapter.Location |> Option.map _.Osm |> Option.flatten |> Option.defaultValue "" }
|
||||||
|
|
||||||
|
|
||||||
/// A display version of a custom feed definition
|
/// A display version of a custom feed definition
|
||||||
@ -360,6 +361,78 @@ type EditCategoryModel = {
|
|||||||
this.CategoryId = "new"
|
this.CategoryId = "new"
|
||||||
|
|
||||||
|
|
||||||
|
/// View model to add/edit an episode chapter
|
||||||
|
type EditChapterModel = {
|
||||||
|
/// The ID of the post to which the chapter belongs
|
||||||
|
PostId: string
|
||||||
|
|
||||||
|
/// The index in the chapter list (-1 means new)
|
||||||
|
Index: int
|
||||||
|
|
||||||
|
/// The start time of the chapter (H:mm:ss.FF format)
|
||||||
|
StartTime: string
|
||||||
|
|
||||||
|
/// The title of the chapter
|
||||||
|
Title: string
|
||||||
|
|
||||||
|
/// An image to display for this chapter
|
||||||
|
ImageUrl: string
|
||||||
|
|
||||||
|
/// Whether this chapter should be displayed in podcast players
|
||||||
|
IsHidden: bool
|
||||||
|
|
||||||
|
/// The end time of the chapter (HH:MM:SS.FF format)
|
||||||
|
EndTime: string
|
||||||
|
|
||||||
|
/// The name of a location
|
||||||
|
LocationName: string
|
||||||
|
|
||||||
|
/// The geographic coordinates of the location
|
||||||
|
LocationGeo: string
|
||||||
|
|
||||||
|
/// An OpenStreetMap query for this location
|
||||||
|
LocationOsm: string
|
||||||
|
} with
|
||||||
|
|
||||||
|
/// Create a display chapter from a chapter
|
||||||
|
static member FromChapter (postId: PostId) idx chapter =
|
||||||
|
let it = DisplayChapter.FromChapter chapter
|
||||||
|
{ PostId = string postId
|
||||||
|
Index = idx
|
||||||
|
StartTime = it.StartTime
|
||||||
|
Title = it.Title
|
||||||
|
ImageUrl = it.ImageUrl
|
||||||
|
IsHidden = it.IsHidden
|
||||||
|
EndTime = it.EndTime
|
||||||
|
LocationName = it.LocationName
|
||||||
|
LocationGeo = it.LocationGeo
|
||||||
|
LocationOsm = it.LocationOsm }
|
||||||
|
|
||||||
|
/// Create a chapter from the values in this model
|
||||||
|
member this.ToChapter () =
|
||||||
|
let parseDuration name value =
|
||||||
|
let pattern =
|
||||||
|
match value |> Seq.fold (fun count chr -> if chr = ':' then count + 1 else count) 0 with
|
||||||
|
| 0 -> "S"
|
||||||
|
| 1 -> "MM:ss"
|
||||||
|
| 2 -> "H:mm:ss"
|
||||||
|
| _ -> invalidArg name "Max time format is H:mm:ss"
|
||||||
|
|> function
|
||||||
|
| it -> DurationPattern.CreateWithInvariantCulture $"{it}.FFFFFFFFF"
|
||||||
|
let result = pattern.Parse value
|
||||||
|
if result.Success then result.Value else raise result.Exception
|
||||||
|
let location =
|
||||||
|
match noneIfBlank this.LocationName with
|
||||||
|
| None -> None
|
||||||
|
| Some name -> Some { Name = name; Geo = noneIfBlank this.LocationGeo; Osm = noneIfBlank this.LocationOsm }
|
||||||
|
{ StartTime = parseDuration (nameof this.StartTime) this.StartTime
|
||||||
|
Title = noneIfBlank this.Title
|
||||||
|
ImageUrl = noneIfBlank this.ImageUrl
|
||||||
|
IsHidden = if this.IsHidden then Some true else None
|
||||||
|
EndTime = noneIfBlank this.EndTime |> Option.map (parseDuration (nameof this.EndTime))
|
||||||
|
Location = location }
|
||||||
|
|
||||||
|
|
||||||
/// View model to edit a custom RSS feed
|
/// View model to edit a custom RSS feed
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
type EditCustomFeedModel = {
|
type EditCustomFeedModel = {
|
||||||
@ -1033,14 +1106,15 @@ type ManageChaptersModel = {
|
|||||||
/// The title of the post for which chapters are being edited
|
/// The title of the post for which chapters are being edited
|
||||||
Title: string
|
Title: string
|
||||||
|
|
||||||
Chapters: Chapter array
|
/// The chapters for the post
|
||||||
|
Chapters: DisplayChapter array
|
||||||
} with
|
} with
|
||||||
|
|
||||||
/// Create a model from a post and its episode's chapters
|
/// Create a model from a post and its episode's chapters
|
||||||
static member Create (post: Post) =
|
static member Create (post: Post) =
|
||||||
{ Id = string post.Id
|
{ Id = string post.Id
|
||||||
Title = post.Title
|
Title = post.Title
|
||||||
Chapters = Array.ofList post.Episode.Value.Chapters.Value }
|
Chapters = post.Episode.Value.Chapters.Value |> List.map DisplayChapter.FromChapter |> Array.ofList }
|
||||||
|
|
||||||
|
|
||||||
/// View model to manage permalinks
|
/// View model to manage permalinks
|
||||||
|
@ -33,6 +33,39 @@ let addBaseToRelativeUrlsTests = testList "PublicHelpers.addBaseToRelativeUrls"
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
/// Unit tests for the DisplayChapter type
|
||||||
|
let displayChapterTests = testList "DisplayChapter.FromChapter" [
|
||||||
|
test "succeeds for a minimally-filled chapter" {
|
||||||
|
let chapter = DisplayChapter.FromChapter { Chapter.Empty with StartTime = Duration.FromSeconds 322L }
|
||||||
|
Expect.equal chapter.StartTime "0:05:22" "Start time not filled/formatted properly"
|
||||||
|
Expect.equal chapter.Title "" "Title not filled properly"
|
||||||
|
Expect.equal chapter.ImageUrl "" "Image URL not filled properly"
|
||||||
|
Expect.isFalse chapter.IsHidden "Is hidden flag not filled properly"
|
||||||
|
Expect.equal chapter.EndTime "" "End time not filled properly"
|
||||||
|
Expect.equal chapter.LocationName "" "Location name not filled properly"
|
||||||
|
Expect.equal chapter.LocationGeo "" "Location geo URL not filled properly"
|
||||||
|
Expect.equal chapter.LocationOsm "" "Location OSM query not filled properly"
|
||||||
|
}
|
||||||
|
test "succeeds for a fully-filled chapter" {
|
||||||
|
let chapter =
|
||||||
|
DisplayChapter.FromChapter
|
||||||
|
{ StartTime = Duration.FromSeconds 7201.43242
|
||||||
|
Title = Some "My Test Chapter"
|
||||||
|
ImageUrl = Some "two-hours-in.jpg"
|
||||||
|
IsHidden = Some true
|
||||||
|
EndTime = Some (Duration.FromSeconds 7313.788)
|
||||||
|
Location = Some { Name = "Over Here"; Geo = Some "geo:23432"; Osm = Some "SF98fFSu-8" } }
|
||||||
|
Expect.equal chapter.StartTime "2:00:01.43" "Start time not filled/formatted properly"
|
||||||
|
Expect.equal chapter.Title "My Test Chapter" "Title not filled properly"
|
||||||
|
Expect.equal chapter.ImageUrl "two-hours-in.jpg" "Image URL not filled properly"
|
||||||
|
Expect.isTrue chapter.IsHidden "Is hidden flag not filled properly"
|
||||||
|
Expect.equal chapter.EndTime "2:01:53.78" "End time not filled/formatted properly"
|
||||||
|
Expect.equal chapter.LocationName "Over Here" "Location name not filled properly"
|
||||||
|
Expect.equal chapter.LocationGeo "geo:23432" "Location geo URL not filled properly"
|
||||||
|
Expect.equal chapter.LocationOsm "SF98fFSu-8" "Location OSM query not filled properly"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
/// Unit tests for the DisplayCustomFeed type
|
/// Unit tests for the DisplayCustomFeed type
|
||||||
let displayCustomFeedTests = testList "DisplayCustomFeed.FromFeed" [
|
let displayCustomFeedTests = testList "DisplayCustomFeed.FromFeed" [
|
||||||
test "succeeds for a feed for an existing category" {
|
test "succeeds for a feed for an existing category" {
|
||||||
@ -1071,6 +1104,29 @@ let editUserModelTests = testList "EditUserModel" [
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
/// Unit tests for the ManageChaptersModel type
|
||||||
|
let manageChaptersModelTests = testList "ManageChaptersModel.Create" [
|
||||||
|
test "succeeds" {
|
||||||
|
let model =
|
||||||
|
ManageChaptersModel.Create
|
||||||
|
{ Post.Empty with
|
||||||
|
Id = PostId "test-post"
|
||||||
|
Title = "Look at all these chapters"
|
||||||
|
Episode = Some
|
||||||
|
{ Episode.Empty with
|
||||||
|
Chapters = Some
|
||||||
|
[ { Chapter.Empty with StartTime = Duration.FromSeconds 18L }
|
||||||
|
{ Chapter.Empty with StartTime = Duration.FromSeconds 36L }
|
||||||
|
{ Chapter.Empty with StartTime = Duration.FromSeconds 180.7 } ] } }
|
||||||
|
Expect.equal model.Id "test-post" "ID not filled properly"
|
||||||
|
Expect.equal model.Title "Look at all these chapters" "Title not filled properly"
|
||||||
|
Expect.hasLength model.Chapters 3 "There should be three chapters"
|
||||||
|
Expect.equal model.Chapters[0].StartTime "0:00:18" "First chapter not filled properly"
|
||||||
|
Expect.equal model.Chapters[1].StartTime "0:00:36" "Second chapter not filled properly"
|
||||||
|
Expect.equal model.Chapters[2].StartTime "0:03:00.7" "Third chapter not filled properly"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
/// Unit tests for the ManagePermalinksModel type
|
/// Unit tests for the ManagePermalinksModel type
|
||||||
let managePermalinksModelTests = testList "ManagePermalinksModel" [
|
let managePermalinksModelTests = testList "ManagePermalinksModel" [
|
||||||
test "FromPage succeeds" {
|
test "FromPage succeeds" {
|
||||||
@ -1285,6 +1341,7 @@ let userMessageTests = testList "UserMessage" [
|
|||||||
/// All tests in the Domain.ViewModels file
|
/// All tests in the Domain.ViewModels file
|
||||||
let all = testList "ViewModels" [
|
let all = testList "ViewModels" [
|
||||||
addBaseToRelativeUrlsTests
|
addBaseToRelativeUrlsTests
|
||||||
|
displayChapterTests
|
||||||
displayCustomFeedTests
|
displayCustomFeedTests
|
||||||
displayPageTests
|
displayPageTests
|
||||||
displayRevisionTests
|
displayRevisionTests
|
||||||
@ -1300,6 +1357,7 @@ let all = testList "ViewModels" [
|
|||||||
editRssModelTests
|
editRssModelTests
|
||||||
editTagMapModelTests
|
editTagMapModelTests
|
||||||
editUserModelTests
|
editUserModelTests
|
||||||
|
manageChaptersModelTests
|
||||||
managePermalinksModelTests
|
managePermalinksModelTests
|
||||||
manageRevisionsModelTests
|
manageRevisionsModelTests
|
||||||
postListItemTests
|
postListItemTests
|
||||||
|
@ -12,15 +12,19 @@ let postgresOnly = (RethinkDbDataTests.env "PG_ONLY" "0") = "1"
|
|||||||
/// Whether any of the data tests are being isolated
|
/// Whether any of the data tests are being isolated
|
||||||
let dbOnly = rethinkOnly || sqliteOnly || postgresOnly
|
let dbOnly = rethinkOnly || sqliteOnly || postgresOnly
|
||||||
|
|
||||||
|
/// Whether to only run the unit tests (skip database/integration tests)
|
||||||
|
let unitOnly = (RethinkDbDataTests.env "UNIT_ONLY" "0") = "1"
|
||||||
|
|
||||||
let allTests = testList "MyWebLog" [
|
let allTests = testList "MyWebLog" [
|
||||||
if not dbOnly then testList "Domain" [ SupportTypesTests.all; DataTypesTests.all; ViewModelsTests.all ]
|
if not dbOnly then testList "Domain" [ SupportTypesTests.all; DataTypesTests.all; ViewModelsTests.all ]
|
||||||
testList "Data" [
|
if not unitOnly then
|
||||||
if not dbOnly then ConvertersTests.all
|
testList "Data" [
|
||||||
if not dbOnly then UtilsTests.all
|
if not dbOnly then ConvertersTests.all
|
||||||
if not dbOnly || (dbOnly && rethinkOnly) then RethinkDbDataTests.all
|
if not dbOnly then UtilsTests.all
|
||||||
if not dbOnly || (dbOnly && sqliteOnly) then SQLiteDataTests.all
|
if not dbOnly || (dbOnly && rethinkOnly) then RethinkDbDataTests.all
|
||||||
if not dbOnly || (dbOnly && postgresOnly) then PostgresDataTests.all
|
if not dbOnly || (dbOnly && sqliteOnly) then SQLiteDataTests.all
|
||||||
]
|
if not dbOnly || (dbOnly && postgresOnly) then PostgresDataTests.all
|
||||||
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
[<EntryPoint>]
|
[<EntryPoint>]
|
||||||
|
@ -227,15 +227,15 @@ let register () =
|
|||||||
typeof<CustomFeed>; typeof<Episode>; typeof<Episode option>; typeof<MetaItem>; typeof<Page>
|
typeof<CustomFeed>; typeof<Episode>; typeof<Episode option>; typeof<MetaItem>; typeof<Page>
|
||||||
typeof<RedirectRule>; typeof<RssOptions>; typeof<TagMap>; typeof<UploadDestination>; typeof<WebLog>
|
typeof<RedirectRule>; typeof<RssOptions>; typeof<TagMap>; typeof<UploadDestination>; typeof<WebLog>
|
||||||
// View models
|
// View models
|
||||||
typeof<DashboardModel>; typeof<DisplayCategory>; typeof<DisplayChapter>
|
typeof<DashboardModel>; typeof<DisplayCategory>; typeof<DisplayChapter>
|
||||||
typeof<DisplayCustomFeed>; typeof<DisplayPage>; typeof<DisplayRevision>
|
typeof<DisplayCustomFeed>; typeof<DisplayPage>; typeof<DisplayRevision>
|
||||||
typeof<DisplayTheme>; typeof<DisplayUpload>; typeof<DisplayUser>
|
typeof<DisplayTheme>; typeof<DisplayUpload>; typeof<DisplayUser>
|
||||||
typeof<EditCategoryModel>; typeof<EditCustomFeedModel>; typeof<EditMyInfoModel>
|
typeof<EditCategoryModel>; typeof<EditChapterModel>; typeof<EditCustomFeedModel>
|
||||||
typeof<EditPageModel>; typeof<EditPostModel>; typeof<EditRedirectRuleModel>
|
typeof<EditMyInfoModel>; typeof<EditPageModel>; typeof<EditPostModel>
|
||||||
typeof<EditRssModel>; typeof<EditTagMapModel>; typeof<EditUserModel>
|
typeof<EditRedirectRuleModel>; typeof<EditRssModel>; typeof<EditTagMapModel>
|
||||||
typeof<LogOnModel>; typeof<ManageChaptersModel>; typeof<ManagePermalinksModel>
|
typeof<EditUserModel>; typeof<LogOnModel>; typeof<ManageChaptersModel>
|
||||||
typeof<ManageRevisionsModel>; typeof<PostDisplay>; typeof<PostListItem>
|
typeof<ManagePermalinksModel>; typeof<ManageRevisionsModel>; typeof<PostDisplay>
|
||||||
typeof<SettingsModel>; typeof<UserMessage>
|
typeof<PostListItem>; typeof<SettingsModel>; typeof<UserMessage>
|
||||||
// Framework types
|
// Framework types
|
||||||
typeof<AntiforgeryTokenSet>; typeof<DateTime option>; typeof<int option>; typeof<KeyValuePair>
|
typeof<AntiforgeryTokenSet>; typeof<DateTime option>; typeof<int option>; typeof<KeyValuePair>
|
||||||
typeof<MetaItem list>; typeof<string list>; typeof<string option>; typeof<TagMap list>
|
typeof<MetaItem list>; typeof<string list>; typeof<string option>; typeof<TagMap list>
|
||||||
|
@ -386,6 +386,29 @@ let chapters postId : HttpHandler = requireAccess Author >=> fun next ctx -> tas
|
|||||||
| Some _ | None -> return! Error.notFound next ctx
|
| Some _ | None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET /admin/post/{id}/chapter/{idx}
|
||||||
|
let editChapter (postId, index) : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||||
|
match! ctx.Data.Post.FindById (PostId postId) ctx.WebLog.Id with
|
||||||
|
| Some post
|
||||||
|
when Option.isSome post.Episode
|
||||||
|
&& Option.isSome post.Episode.Value.Chapters
|
||||||
|
&& canEdit post.AuthorId ctx ->
|
||||||
|
let chapter =
|
||||||
|
if index = -1 then Some Chapter.Empty
|
||||||
|
else
|
||||||
|
let chapters = post.Episode.Value.Chapters.Value
|
||||||
|
if index < List.length chapters then Some chapters[index] else None
|
||||||
|
match chapter with
|
||||||
|
| Some chap ->
|
||||||
|
return!
|
||||||
|
hashForPage (if index = -1 then "Add a Chapter" else "Edit Chapter")
|
||||||
|
|> withAntiCsrf ctx
|
||||||
|
|> addToHash ViewContext.Model (EditChapterModel.FromChapter post.Id index chap)
|
||||||
|
|> adminBareView "chapter-edit" next ctx
|
||||||
|
| None -> return! Error.notFound next ctx
|
||||||
|
| Some _ | None -> return! Error.notFound next ctx
|
||||||
|
}
|
||||||
|
|
||||||
// POST /admin/post/save
|
// POST /admin/post/save
|
||||||
let save : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
let save : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||||
let! model = ctx.BindFormAsync<EditPostModel>()
|
let! model = ctx.BindFormAsync<EditPostModel>()
|
||||||
|
@ -129,6 +129,7 @@ let router : HttpHandler = choose [
|
|||||||
routef "/%s/permalinks" Post.editPermalinks
|
routef "/%s/permalinks" Post.editPermalinks
|
||||||
routef "/%s/revision/%s/preview" Post.previewRevision
|
routef "/%s/revision/%s/preview" Post.previewRevision
|
||||||
routef "/%s/revisions" Post.editRevisions
|
routef "/%s/revisions" Post.editRevisions
|
||||||
|
routef "/%s/chapter/%i" Post.editChapter
|
||||||
routef "/%s/chapters" Post.chapters
|
routef "/%s/chapters" Post.chapters
|
||||||
])
|
])
|
||||||
subRoute "/settings" (requireAccess WebLogAdmin >=> choose [
|
subRoute "/settings" (requireAccess WebLogAdmin >=> choose [
|
||||||
|
84
src/admin-theme/chapter-edit.liquid
Normal file
84
src/admin-theme/chapter-edit.liquid
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<h2 class=my-3>{% if model.index < 0 %}Add{% else %}Edit{% endif %} Chapter</h2>
|
||||||
|
<form method=post hx-target=#chapter_list class=container>
|
||||||
|
<input type=hidden name=PostId value="{{ model.post_id }}">
|
||||||
|
<input type=hidden name=Index value={{ model.index }}>
|
||||||
|
<div class=row>
|
||||||
|
<div class="col">
|
||||||
|
<div class=form-floating>
|
||||||
|
<input type=text id=start_time name=StartTime class=form-control value="{{ model.start_time }}">
|
||||||
|
<label for=start_time>Start Time</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class=form-floating>
|
||||||
|
<input type=text id=title name=Title class=form-control value="{{ model.title }}" placeholder=Title>
|
||||||
|
<label for=title>Chapter Title</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class=row>
|
||||||
|
<div class="col">
|
||||||
|
<div class=form-floating>
|
||||||
|
<input type=text id=image_url name=ImageUrl class=form-control value="{{ model.image_url }}"
|
||||||
|
placeholder="Image URL">
|
||||||
|
<label for=image_url>Image URL</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input type=checkbox id=is_hidden name=IsHidden class=form-check-input value=true
|
||||||
|
{%- if model.is_hidden %} checked{% endif %}>
|
||||||
|
<label for=is_hidden>Hidden Chapter</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class=form-floating>
|
||||||
|
<input type=text id=end_time name=EndTime class=form-control value="{{ model.end_time }}"
|
||||||
|
placeholder="End Time">
|
||||||
|
<label for=end_time>End Time</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class=row>
|
||||||
|
<div class="col">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input type=checkbox id=has_location class=form-check-input value=true
|
||||||
|
{%- if model.location_name != "" %} checked{% endif %}>
|
||||||
|
<label for=has_location>Associate Location</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class=form-floating>
|
||||||
|
<input type=text id=location_name name=LocationName class=form-control value="{{ model.location_name }}"
|
||||||
|
placeholder="Location Name">
|
||||||
|
<label for=location_name>Name</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class=form-floating>
|
||||||
|
<input type=text id=location_geo name=LocationGeo class=form-control value="{{ model.location_geo }}"
|
||||||
|
placeholder="Location Geo URL">
|
||||||
|
<label for=location_geo>Geo URL</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class=form-floating>
|
||||||
|
<input type=text id=location_osm name=LocationOsm class=form-control value="{{ model.location_osm }}"
|
||||||
|
placeholder="Location OSM Query">
|
||||||
|
<label for=location_osm>OSM Query</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class=row>
|
||||||
|
<div class=col>
|
||||||
|
{% if model.index < 0 -%}
|
||||||
|
<button type=submit class="btn btn-primary">Add + New Chapter</button>
|
||||||
|
<button type=button class="btn btn-secondary">Save</button>
|
||||||
|
{% else -%}
|
||||||
|
<button type=submit class="btn btn-primary">Save</button>
|
||||||
|
{% endif %}
|
||||||
|
{% assign cancel_link = "admin/post/" | append: model.post_id | append: "/chapters" | relative_link %}
|
||||||
|
<a href="{{ cancel_link }}" hx-get="{{ cancel_link }}" class="btn btn-secondary" hx-target=body>Cancel</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
@ -1,6 +1,6 @@
|
|||||||
<h2 class=my-3>{{ page_title }}</h2>
|
<h2 class=my-3>{{ page_title }}</h2>
|
||||||
<article>
|
<article>
|
||||||
<form method=post hx-target=body>
|
<form method=post id=chapter_list hx-target=body>
|
||||||
<input type=hidden name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
<input type=hidden name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
||||||
<input type=hidden name=Id value="{{ model.id }}">
|
<input type=hidden name=Id value="{{ model.id }}">
|
||||||
<div class="container mb-3">
|
<div class="container mb-3">
|
||||||
@ -29,9 +29,9 @@
|
|||||||
<div class=col>{% if chapter.location %}Y{% else %}N{% endif %}</div>
|
<div class=col>{% if chapter.location %}Y{% else %}N{% endif %}</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<div class="row pb-3 mwl-table-detail" id=chapter_new>
|
<div class="row pb-3" id=chapter_new>
|
||||||
{% assign new_link = "admin/post/" | append: model.id | append: "/chapter/new" | relative_link %}
|
{% assign new_link = "admin/post/" | append: model.id | append: "/chapter/-1" | relative_link %}
|
||||||
<a href="{{ new_link }}" hx-get="{{ new_link }}" hx-target=#chapter_new>Add a New Chapter</a>
|
<a class="btn btn-primary" href="{{ new_link }}" hx-get="{{ new_link }}" hx-target=#chapter_new>Add a New Chapter</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
Loading…
Reference in New Issue
Block a user