Add post edit / index view
This commit is contained in:
@@ -246,7 +246,7 @@ module Admin =
|
||||
let updated =
|
||||
{ webLog with
|
||||
name = model.name
|
||||
subtitle = match model.subtitle with "" -> None | it -> Some it
|
||||
subtitle = if model.subtitle = "" then None else Some model.subtitle
|
||||
defaultPage = model.defaultPage
|
||||
postsPerPage = model.postsPerPage
|
||||
timeZone = model.timeZone
|
||||
@@ -315,8 +315,8 @@ module Category =
|
||||
{ cat with
|
||||
name = model.name
|
||||
slug = model.slug
|
||||
description = match model.description with "" -> None | it -> Some it
|
||||
parentId = match model.parentId with "" -> None | it -> Some (CategoryId it)
|
||||
description = if model.description = "" then None else Some model.description
|
||||
parentId = if model.parentId = "" then None else Some (CategoryId model.parentId)
|
||||
}
|
||||
do! (match model.categoryId with "new" -> Data.Category.add | _ -> Data.Category.update) cat conn
|
||||
do! addMessage ctx { UserMessage.success with message = "Category saved successfully" }
|
||||
@@ -383,10 +383,10 @@ module Page =
|
||||
| "new" ->
|
||||
return Some
|
||||
{ Page.empty with
|
||||
id = PageId.create ()
|
||||
webLogId = webLogId
|
||||
authorId = userId ctx
|
||||
publishedOn = now
|
||||
id = PageId.create ()
|
||||
webLogId = webLogId
|
||||
authorId = userId ctx
|
||||
publishedOn = now
|
||||
}
|
||||
| pgId -> return! Data.Page.findByFullId (PageId pgId) webLogId conn
|
||||
}
|
||||
@@ -421,16 +421,41 @@ module Page =
|
||||
/// Handlers to manipulate posts
|
||||
module Post =
|
||||
|
||||
/// Convert a list of posts into items ready to be displayed
|
||||
let private preparePostList (webLog : WebLog) (posts : Post list) pageNbr perPage conn = task {
|
||||
let! authors =
|
||||
Data.WebLogUser.findNames (posts |> List.map (fun p -> p.authorId) |> List.distinct) webLog.id conn
|
||||
let! cats =
|
||||
Data.Category.findNames (posts |> List.map (fun c -> c.categoryIds) |> List.concat |> List.distinct)
|
||||
webLog.id conn
|
||||
let postItems =
|
||||
posts
|
||||
|> Seq.ofList
|
||||
|> Seq.truncate perPage
|
||||
|> Seq.map PostListItem.fromPost
|
||||
|> Seq.map (fun pi -> { pi with authorName = authors[pi.authorId] })
|
||||
|> Array.ofSeq
|
||||
let model =
|
||||
{ posts = postItems
|
||||
categories = cats
|
||||
subtitle = None
|
||||
hasNewer = pageNbr <> 1
|
||||
hasOlder = posts |> List.length > perPage
|
||||
}
|
||||
return Hash.FromAnonymousObject {| model = model |}
|
||||
}
|
||||
|
||||
// GET /page/{pageNbr}
|
||||
let pageOfPosts (pageNbr : int) : HttpHandler = fun next ctx -> task {
|
||||
let webLog = WebLogCache.get ctx
|
||||
let! posts = Data.Post.findPageOfPublishedPosts webLog.id pageNbr webLog.postsPerPage (conn ctx)
|
||||
let hash = Hash.FromAnonymousObject {| posts = posts |}
|
||||
let conn = conn ctx
|
||||
let! posts = Data.Post.findPageOfPublishedPosts webLog.id pageNbr webLog.postsPerPage conn
|
||||
let! hash = preparePostList webLog posts pageNbr webLog.postsPerPage conn
|
||||
let title =
|
||||
match pageNbr, webLog.defaultPage with
|
||||
| 1, "posts" -> None
|
||||
| _, "posts" -> Some $"Page {pageNbr}"
|
||||
| _, _ -> Some $"Page {pageNbr} « Posts"
|
||||
| _, _ -> Some $"Page {pageNbr} « Posts"
|
||||
match title with Some ttl -> hash.Add ("page_title", ttl) | None -> ()
|
||||
return! themedView "index" next ctx hash
|
||||
}
|
||||
@@ -482,39 +507,86 @@ module Post =
|
||||
// GET /posts
|
||||
// GET /posts/page/{pageNbr}
|
||||
let all pageNbr : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||
let webLog = WebLogCache.get ctx
|
||||
let conn = conn ctx
|
||||
let! posts = Data.Post.findPageOfPosts webLog.id pageNbr 25 conn
|
||||
let! authors =
|
||||
Data.WebLogUser.findNames (posts |> List.map (fun p -> p.authorId) |> List.distinct) webLog.id conn
|
||||
let! cats =
|
||||
Data.Category.findNames (posts |> List.map (fun c -> c.categoryIds) |> List.concat |> List.distinct)
|
||||
webLog.id conn
|
||||
let tags = posts
|
||||
|> List.map (fun p -> PostId.toString p.id, p.tags |> List.fold (fun t tag -> $"{t}, {tag}") "")
|
||||
|> dict
|
||||
let model =
|
||||
{ posts = posts |> Seq.ofList |> Seq.truncate 25 |> Seq.map PostListItem.fromPost |> Array.ofSeq
|
||||
authors = authors
|
||||
categories = cats
|
||||
hasNewer = pageNbr <> 1
|
||||
hasOlder = posts |> List.length > webLog.postsPerPage
|
||||
}
|
||||
return!
|
||||
Hash.FromAnonymousObject {| model = model; tags = tags; page_title = "Posts" |}
|
||||
|> viewForTheme "admin" "post-list" next ctx
|
||||
let webLog = WebLogCache.get ctx
|
||||
let conn = conn ctx
|
||||
let! posts = Data.Post.findPageOfPosts webLog.id pageNbr 25 conn
|
||||
let! hash = preparePostList webLog posts pageNbr 25 conn
|
||||
hash.Add ("page_title", "Posts")
|
||||
return! viewForTheme "admin" "post-list" next ctx hash
|
||||
}
|
||||
|
||||
// GET /post/{id}/edit
|
||||
let edit _ : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||
// TODO: write handler
|
||||
return! Error.notFound next ctx
|
||||
let edit postId : HttpHandler = requireUser >=> fun next ctx -> task {
|
||||
let webLogId = webLogId ctx
|
||||
let conn = conn ctx
|
||||
let! result = task {
|
||||
match postId with
|
||||
| "new" -> return Some ("Write a New Post", { Post.empty with id = PostId "new" })
|
||||
| _ ->
|
||||
match! Data.Post.findByFullId (PostId postId) webLogId conn with
|
||||
| Some post -> return Some ("Edit Post", post)
|
||||
| None -> return None
|
||||
}
|
||||
match result with
|
||||
| Some (title, post) ->
|
||||
let! cats = Data.Category.findAllForView webLogId conn
|
||||
return!
|
||||
Hash.FromAnonymousObject {|
|
||||
csrf = csrfToken ctx
|
||||
model = EditPostModel.fromPost post
|
||||
page_title = title
|
||||
categories = cats
|
||||
|}
|
||||
|> viewForTheme "admin" "post-edit" next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
// POST /post/{id}/edit
|
||||
// POST /post/save
|
||||
let save : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task {
|
||||
// TODO: write handler
|
||||
return! Error.notFound next ctx
|
||||
let! model = ctx.BindFormAsync<EditPostModel> ()
|
||||
let webLogId = webLogId ctx
|
||||
let conn = conn ctx
|
||||
let now = DateTime.UtcNow
|
||||
let! pst = task {
|
||||
match model.postId with
|
||||
| "new" ->
|
||||
return Some
|
||||
{ Post.empty with
|
||||
id = PostId.create ()
|
||||
webLogId = webLogId
|
||||
authorId = userId ctx
|
||||
}
|
||||
| postId -> return! Data.Post.findByFullId (PostId postId) webLogId conn
|
||||
}
|
||||
match pst with
|
||||
| Some post ->
|
||||
let revision = { asOf = now; text = MarkupText.parse $"{model.source}: {model.text}" }
|
||||
// Detect a permalink change, and add the prior one to the prior list
|
||||
let page =
|
||||
match Permalink.toString post.permalink with
|
||||
| "" -> post
|
||||
| link when link = model.permalink -> post
|
||||
| _ -> { post with priorPermalinks = post.permalink :: post.priorPermalinks }
|
||||
let post =
|
||||
{ post with
|
||||
title = model.title
|
||||
permalink = Permalink model.permalink
|
||||
publishedOn = if model.doPublish then Some now else post.publishedOn
|
||||
updatedOn = now
|
||||
text = MarkupText.toHtml revision.text
|
||||
tags = model.tags.Split ","
|
||||
|> Seq.ofArray
|
||||
|> Seq.map (fun it -> it.Trim().ToLower ())
|
||||
|> Seq.sort
|
||||
|> List.ofSeq
|
||||
categoryIds = model.categoryIds |> Array.map CategoryId |> List.ofArray
|
||||
status = if model.doPublish then Published else post.status
|
||||
revisions = revision :: page.revisions
|
||||
}
|
||||
do! (match model.postId with "new" -> Data.Post.add | _ -> Data.Post.update) post conn
|
||||
do! addMessage ctx { UserMessage.success with message = "Post saved successfully" }
|
||||
return! redirectToGet $"/post/{PostId.toString post.id}/edit" next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -194,23 +194,16 @@ let main args =
|
||||
Template.RegisterFilter typeof<DotLiquidBespoke.NavLinkFilter>
|
||||
Template.RegisterTag<DotLiquidBespoke.UserLinksTag> "user_links"
|
||||
|
||||
let all = [| "*" |]
|
||||
Template.RegisterSafeType (typeof<Page>, all)
|
||||
Template.RegisterSafeType (typeof<WebLog>, all)
|
||||
|
||||
Template.RegisterSafeType (typeof<DashboardModel>, all)
|
||||
Template.RegisterSafeType (typeof<DisplayCategory>, all)
|
||||
Template.RegisterSafeType (typeof<DisplayPage>, all)
|
||||
Template.RegisterSafeType (typeof<EditCategoryModel>, all)
|
||||
Template.RegisterSafeType (typeof<EditPageModel>, all)
|
||||
Template.RegisterSafeType (typeof<PostDisplay>, all)
|
||||
Template.RegisterSafeType (typeof<PostListItem>, all)
|
||||
Template.RegisterSafeType (typeof<SettingsModel>, all)
|
||||
Template.RegisterSafeType (typeof<UserMessage>, all)
|
||||
|
||||
Template.RegisterSafeType (typeof<AntiforgeryTokenSet>, all)
|
||||
Template.RegisterSafeType (typeof<string option>, all)
|
||||
Template.RegisterSafeType (typeof<KeyValuePair>, all)
|
||||
[ // Domain types
|
||||
typeof<Page>; typeof<WebLog>
|
||||
// View models
|
||||
typeof<DashboardModel>; typeof<DisplayCategory>; typeof<DisplayPage>; typeof<EditCategoryModel>
|
||||
typeof<EditPageModel>; typeof<EditPostModel>; typeof<PostDisplay>; typeof<PostListItem>
|
||||
typeof<SettingsModel>; typeof<UserMessage>
|
||||
// Framework types
|
||||
typeof<AntiforgeryTokenSet>; typeof<string option>; typeof<KeyValuePair>
|
||||
]
|
||||
|> List.iter (fun it -> Template.RegisterSafeType (it, [| "*" |]))
|
||||
|
||||
let app = builder.Build ()
|
||||
|
||||
|
||||
67
src/MyWebLog/themes/admin/post-edit.liquid
Normal file
67
src/MyWebLog/themes/admin/post-edit.liquid
Normal file
@@ -0,0 +1,67 @@
|
||||
<h2 class="my-3">{{ page_title }}</h2>
|
||||
<article>
|
||||
<form action="/post/save" method="post">
|
||||
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
||||
<input type="hidden" name="postId" value="{{ model.post_id }}">
|
||||
<div class="container">
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 col-lg-9">
|
||||
<div class="form-floating pb-3">
|
||||
<input type="text" name="title" id="title" class="form-control" placeholder="Title" autofocus required
|
||||
value="{{ model.title }}">
|
||||
<label for="title">Title</label>
|
||||
</div>
|
||||
<div class="form-floating pb-3">
|
||||
<input type="text" name="permalink" id="permalink" class="form-control" placeholder="Permalink" required
|
||||
value="{{ model.permalink }}">
|
||||
<label for="permalink">Permalink</label>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="text">Text</label>
|
||||
<input type="radio" name="source" id="source_html" class="btn-check" value="HTML"
|
||||
{%- if model.source == "HTML" %} checked="checked"{% endif %}>
|
||||
<label class="btn btn-sm btn-outline-secondary" for="source_html">HTML</label>
|
||||
<input type="radio" name="source" id="source_md" class="btn-check" value="Markdown"
|
||||
{%- if model.source == "Markdown" %} checked="checked"{% endif %}>
|
||||
<label class="btn btn-sm btn-outline-secondary" for="source_md">Markdown</label>
|
||||
</div>
|
||||
<div class="pb-3">
|
||||
<textarea name="text" id="text" class="form-control" rows="10">{{ model.text }}</textarea>
|
||||
</div>
|
||||
<div class="form-floating pb-3">
|
||||
<input type="text" name="tags" id="tags" class="form-control" placeholder="Tags" required
|
||||
value="{{ model.tags }}">
|
||||
<label for="tags">Tags</label>
|
||||
<div class="form-text">comma-delimited</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-3">
|
||||
<fieldset>
|
||||
<legend>Categories</legend>
|
||||
{% for cat in categories %}
|
||||
<div class="form-check">
|
||||
<input type="checkbox" name="categoryIds" id="categoryId_{{ cat.id }}" class="form-check-input"
|
||||
value="{{ cat.id }}" {% if model.category_ids contains cat.id %} checked="checked"{% endif %}>
|
||||
<label for="categoryId_{{ cat.id }}" class="form-check-label"
|
||||
{%- if cat.description %} title="{{ cat.description.value | escape }}"{% endif %}>
|
||||
{%- for it in cat.parent_names %} ⟩ {% endfor %}{{ cat.name }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
{% if model.status == "Draft" %}
|
||||
<div class="form-check pb-2">
|
||||
<input type="checkbox" name="doPublish" id="doPublish" class="form-check-input" value="true">
|
||||
<label for="doPublish" class="form-check-label">Publish This Post</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
@@ -31,9 +31,9 @@
|
||||
<a href="#" class="text-danger">Delete</a>
|
||||
</small>
|
||||
</td>
|
||||
<td>{{ model.authors[post.author_id] }}</td>
|
||||
<td>{{ post.author_name }}</td>
|
||||
<td>{{ post.status }}</td>
|
||||
<td>{{ tags[post.id] }}</td>
|
||||
<td>{{ post.tags | join: ", " }}</td>
|
||||
</tr>
|
||||
{%- endfor %}
|
||||
</tbody>
|
||||
|
||||
23
src/MyWebLog/themes/default/index.liquid
Normal file
23
src/MyWebLog/themes/default/index.liquid
Normal file
@@ -0,0 +1,23 @@
|
||||
{% if model.subtitle %}
|
||||
<h2>{{ model.subtitle.value }}</h2>
|
||||
{% endif %}
|
||||
<section class="container" aria-label="The posts for the page">
|
||||
{% for post in model.posts %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<article>
|
||||
<h1>
|
||||
<a href="/{{ post.permalink }}" title="Permanent link to "{{ post.title | escape }}"">
|
||||
{{ post.title }}
|
||||
</a>
|
||||
</h1>
|
||||
<p>
|
||||
Published on {{ post.published_on | date: "MMMM d, yyyy" }}
|
||||
at {{ post.published_on | date: "h:mmtt" | downcase }}
|
||||
</p>
|
||||
{{ post.text }}
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
|
||||
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/themes/{{ web_log.theme_path }}/style.css">
|
||||
<title>{{ page_title | escape }} « {{ web_log.name | escape }}</title>
|
||||
<title>{{ page_title | escape }}{% if page_title %} « {% endif %}{{ web_log.name | escape }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
|
||||
@@ -11,3 +11,12 @@
|
||||
width: 1rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
fieldset {
|
||||
border: solid 1px lightgray;
|
||||
border-radius: .5rem;
|
||||
padding: 0 1rem 1rem;
|
||||
}
|
||||
legend {
|
||||
float: unset;
|
||||
width: unset;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user