diff --git a/src/MyWebLog.Domain/ViewModels.fs b/src/MyWebLog.Domain/ViewModels.fs index 7f4829a..5a2afcd 100644 --- a/src/MyWebLog.Domain/ViewModels.fs +++ b/src/MyWebLog.Domain/ViewModels.fs @@ -419,7 +419,7 @@ type EditChapterModel = { let pattern = match value |> Seq.fold (fun count chr -> if chr = ':' then count + 1 else count) 0 with | 0 -> "S" - | 1 -> "MM:ss" + | 1 -> "M:ss" | 2 -> "H:mm:ss" | _ -> invalidArg name "Max time format is H:mm:ss" |> function diff --git a/src/MyWebLog/DotLiquidBespoke.fs b/src/MyWebLog/DotLiquidBespoke.fs index ea60ec0..660c215 100644 --- a/src/MyWebLog/DotLiquidBespoke.fs +++ b/src/MyWebLog/DotLiquidBespoke.fs @@ -6,8 +6,8 @@ open System.IO open System.Web open DotLiquid open Giraffe.ViewEngine -open MyWebLog.AdminViews.Helpers open MyWebLog.ViewModels +open MyWebLog.Views /// Extensions on the DotLiquid Context object type Context with diff --git a/src/MyWebLog/Handlers/Admin.fs b/src/MyWebLog/Handlers/Admin.fs index cd92a9a..6b962e5 100644 --- a/src/MyWebLog/Handlers/Admin.fs +++ b/src/MyWebLog/Handlers/Admin.fs @@ -27,7 +27,7 @@ module Dashboard = ListedPages = listed Categories = cats TopLevelCategories = topCats } - return! adminPage "Dashboard" false (AdminViews.Admin.dashboard model) next ctx + return! adminPage "Dashboard" false (Views.Admin.dashboard model) next ctx } // GET /admin/administration diff --git a/src/MyWebLog/Handlers/Helpers.fs b/src/MyWebLog/Handlers/Helpers.fs index 70ad2c5..ad3b611 100644 --- a/src/MyWebLog/Handlers/Helpers.fs +++ b/src/MyWebLog/Handlers/Helpers.fs @@ -3,8 +3,7 @@ module private MyWebLog.Handlers.Helpers open System.Text.Json open Microsoft.AspNetCore.Http -open MyWebLog.AdminViews -open MyWebLog.AdminViews.Helpers +open MyWebLog.Views /// Session extensions to get and set objects type ISession with diff --git a/src/MyWebLog/Handlers/Post.fs b/src/MyWebLog/Handlers/Post.fs index c34fa05..d735bd7 100644 --- a/src/MyWebLog/Handlers/Post.fs +++ b/src/MyWebLog/Handlers/Post.fs @@ -379,7 +379,7 @@ let chapters postId : HttpHandler = requireAccess Author >=> fun next ctx -> tas && Option.isSome post.Episode.Value.Chapters && canEdit post.AuthorId ctx -> return! - adminPage "Manage Chapters" true (AdminViews.Post.chapters false (ManageChaptersModel.Create post)) next ctx + adminPage "Manage Chapters" true (Views.Post.chapters false (ManageChaptersModel.Create post)) next ctx | Some _ | None -> return! Error.notFound next ctx } @@ -398,9 +398,9 @@ let editChapter (postId, index) : HttpHandler = requireAccess Author >=> fun nex match chapter with | Some chap -> return! - adminPage + adminBarePage (if index = -1 then "Add a Chapter" else "Edit Chapter") true - (AdminViews.Post.chapterEdit (EditChapterModel.FromChapter post.Id index chap)) next ctx + (Views.Post.chapterEdit (EditChapterModel.FromChapter post.Id index chap)) next ctx | None -> return! Error.notFound next ctx | Some _ | None -> return! Error.notFound next ctx } @@ -418,7 +418,7 @@ let saveChapter (postId, index) : HttpHandler = requireAccess Author >=> fun nex if index >= -1 && index < List.length chapters then try let chapter = form.ToChapter() - let existing = if index = -1 then chapters else chapters |> List.removeAt index + let existing = if index = -1 then chapters else List.removeAt index chapters let updatedPost = { post with Episode = Some @@ -429,9 +429,32 @@ let saveChapter (postId, index) : HttpHandler = requireAccess Author >=> fun nex return! adminPage "Manage Chapters" true - (AdminViews.Post.chapterList form.AddAnother (ManageChaptersModel.Create updatedPost)) next ctx + (Views.Post.chapterList form.AddAnother (ManageChaptersModel.Create updatedPost)) next ctx with - | ex -> return! Error.notFound next ctx // TODO: return error + | ex -> return! Error.server ex.Message next ctx + else return! Error.notFound next ctx + | Some _ | None -> return! Error.notFound next ctx +} + +// DELETE /admin/post/{id}/chapter/{idx} +let deleteChapter (postId, index) : HttpHandler = requireAccess Author >=> fun next ctx -> task { + let data = ctx.Data + match! 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 chapters = post.Episode.Value.Chapters.Value + if index >= 0 && index < List.length chapters then + let updatedPost = + { post with + Episode = Some { post.Episode.Value with Chapters = Some (List.removeAt index chapters) } } + do! data.Post.Update updatedPost + do! addMessage ctx { UserMessage.Success with Message = "Chapter deleted successfully" } + return! + adminPage + "Manage Chapters" true (Views.Post.chapterList false (ManageChaptersModel.Create updatedPost)) next + ctx else return! Error.notFound next ctx | Some _ | None -> return! Error.notFound next ctx } diff --git a/src/MyWebLog/Handlers/Routes.fs b/src/MyWebLog/Handlers/Routes.fs index 8216f07..1729db9 100644 --- a/src/MyWebLog/Handlers/Routes.fs +++ b/src/MyWebLog/Handlers/Routes.fs @@ -216,6 +216,11 @@ let router : HttpHandler = choose [ routef "/%s/delete" Upload.deleteFromDb ]) ] + DELETE >=> validateCsrf >=> choose [ + subRoute "/post" (choose [ + routef "/%s/chapter/%i" Post.deleteChapter + ]) + ] ]) GET_HEAD >=> routexp "/category/(.*)" Post.pageOfCategorizedPosts GET_HEAD >=> routef "/page/%i" Post.pageOfPosts diff --git a/src/MyWebLog/Handlers/User.fs b/src/MyWebLog/Handlers/User.fs index 454bf9f..b360df5 100644 --- a/src/MyWebLog/Handlers/User.fs +++ b/src/MyWebLog/Handlers/User.fs @@ -36,7 +36,7 @@ let logOn returnUrl : HttpHandler = fun next ctx -> match returnUrl with | Some _ -> returnUrl | None -> if ctx.Request.Query.ContainsKey "returnUrl" then Some ctx.Request.Query["returnUrl"].[0] else None - adminPage "Log On" true (AdminViews.User.logOn { LogOnModel.Empty with ReturnTo = returnTo }) next ctx + adminPage "Log On" true (Views.User.logOn { LogOnModel.Empty with ReturnTo = returnTo }) next ctx open System.Security.Claims @@ -93,7 +93,7 @@ let private goAway : HttpHandler = RequestErrors.BAD_REQUEST "really?" // GET /admin/settings/users let all : HttpHandler = fun next ctx -> task { let! users = ctx.Data.WebLogUser.FindByWebLog ctx.WebLog.Id - return! adminBarePage "User Administration" true (AdminViews.User.userList users) next ctx + return! adminBarePage "User Administration" true (Views.User.userList users) next ctx } /// Show the edit user page diff --git a/src/MyWebLog/MyWebLog.fsproj b/src/MyWebLog/MyWebLog.fsproj index 57bc9e5..c795441 100644 --- a/src/MyWebLog/MyWebLog.fsproj +++ b/src/MyWebLog/MyWebLog.fsproj @@ -9,10 +9,10 @@ - - - - + + + + diff --git a/src/MyWebLog/AdminViews/Admin.fs b/src/MyWebLog/Views/Admin.fs similarity index 99% rename from src/MyWebLog/AdminViews/Admin.fs rename to src/MyWebLog/Views/Admin.fs index e42d68f..335d4ce 100644 --- a/src/MyWebLog/AdminViews/Admin.fs +++ b/src/MyWebLog/Views/Admin.fs @@ -1,4 +1,4 @@ -module MyWebLog.AdminViews.Admin +module MyWebLog.Views.Admin open Giraffe.ViewEngine open MyWebLog.ViewModels diff --git a/src/MyWebLog/AdminViews/Helpers.fs b/src/MyWebLog/Views/Helpers.fs similarity index 99% rename from src/MyWebLog/AdminViews/Helpers.fs rename to src/MyWebLog/Views/Helpers.fs index 7a6bcdb..0c90f15 100644 --- a/src/MyWebLog/AdminViews/Helpers.fs +++ b/src/MyWebLog/Views/Helpers.fs @@ -1,5 +1,5 @@ [] -module MyWebLog.AdminViews.Helpers +module MyWebLog.Views.Helpers open Microsoft.AspNetCore.Antiforgery open Giraffe.ViewEngine diff --git a/src/MyWebLog/AdminViews/Post.fs b/src/MyWebLog/Views/Post.fs similarity index 87% rename from src/MyWebLog/AdminViews/Post.fs rename to src/MyWebLog/Views/Post.fs index b1d4994..f64eb48 100644 --- a/src/MyWebLog/AdminViews/Post.fs +++ b/src/MyWebLog/Views/Post.fs @@ -1,4 +1,4 @@ -module MyWebLog.AdminViews.Post +module MyWebLog.Views.Post open Giraffe.ViewEngine open Giraffe.ViewEngine.Htmx @@ -9,6 +9,7 @@ open NodaTime.Text /// The pattern for chapter start times let startTimePattern = DurationPattern.CreateWithInvariantCulture "H:mm:ss.FF" +/// The form to add or edit a chapter let chapterEdit (model: EditChapterModel) app = [ let postUrl = relUrl app $"admin/post/{model.PostId}/chapter/{model.Index}" h3 [ _class "my-3" ] [ raw (if model.Index < 0 then "Add" else "Edit"); raw " Chapter" ] @@ -128,6 +129,7 @@ let chapterEdit (model: EditChapterModel) app = [ else input [ _type "hidden"; _name "AddAnother"; _value "false" ] button [ _type "submit"; _class "btn btn-primary" ] [ raw "Save" ] + raw "   " a [ _href cancelLink; _hxGet cancelLink; _class "btn btn-secondary"; _hxTarget "body" ] [ raw "Cancel" ] ] ] @@ -146,20 +148,37 @@ let chapterList withNew (model: ManageChaptersModel) app = div [ _class "col" ] [ raw "Location?" ] ] yield! model.Chapters |> List.mapi (fun idx chapter -> - div [ _class "row pb-3 mwl-table-detail"; _id $"chapter{idx}" ] [ + div [ _class "row mwl-table-detail"; _id $"chapter{idx}" ] [ div [ _class "col" ] [ txt (startTimePattern.Format chapter.StartTime) ] - div [ _class "col" ] [ txt (defaultArg chapter.Title "") ] + div [ _class "col" ] [ + txt (defaultArg chapter.Title ""); br [] + small [] [ + if withNew then + raw " " + else + let chapterUrl = relUrl app $"admin/post/{model.Id}/chapter/{idx}" + a [ _href chapterUrl; _hxGet chapterUrl; _hxTarget $"#chapter{idx}" + _hxSwap $"innerHTML show:#chapter{idx}:top" ] [ + raw "Edit" + ] + span [ _class "text-muted" ] [ raw " • " ] + a [ _href chapterUrl; _hxDelete chapterUrl; _class "text-danger" ] [ + raw "Delete" + ] + ] + ] div [ _class "col" ] [ raw (if Option.isSome chapter.ImageUrl then "Y" else "N") ] div [ _class "col" ] [ raw (if Option.isSome chapter.Location then "Y" else "N") ] ]) div [ _class "row pb-3"; _id "chapter-1" ] [ + let newLink = relUrl app $"admin/post/{model.Id}/chapter/-1" if withNew then - yield! chapterEdit (EditChapterModel.FromChapter (PostId model.Id) -1 Chapter.Empty) app + span [ _hxGet newLink; _hxTarget "#chapter-1"; _hxTrigger "load"; _hxSwap "show:#chapter-1:top" ] [] else - let newLink = relUrl app $"admin/post/{model.Id}/chapter/-1" div [ _class "row pb-3 mwl-table-detail" ] [ div [ _class "col-12" ] [ - a [ _class "btn btn-primary"; _href newLink; _hxGet newLink; _hxTarget "#chapter-1" ] [ + a [ _class "btn btn-primary"; _href newLink; _hxGet newLink; _hxTarget "#chapter-1" + _hxSwap "show:#chapter-1:top" ] [ raw "Add a New Chapter" ] ] diff --git a/src/MyWebLog/AdminViews/User.fs b/src/MyWebLog/Views/User.fs similarity index 99% rename from src/MyWebLog/AdminViews/User.fs rename to src/MyWebLog/Views/User.fs index 3e58130..d395825 100644 --- a/src/MyWebLog/AdminViews/User.fs +++ b/src/MyWebLog/Views/User.fs @@ -1,4 +1,4 @@ -module MyWebLog.AdminViews.User +module MyWebLog.Views.User open Giraffe.ViewEngine open Giraffe.ViewEngine.Htmx