V2 #1
| @ -394,6 +394,21 @@ module Page = | |||||||
|                 ] |                 ] | ||||||
|             write; withRetryDefault; ignoreResult |             write; withRetryDefault; ignoreResult | ||||||
|         } |         } | ||||||
|  |      | ||||||
|  |     /// Update prior permalinks for a page | ||||||
|  |     let updatePriorPermalinks pageId webLogId (permalinks : Permalink list) conn = task { | ||||||
|  |         match! findById pageId webLogId conn with | ||||||
|  |         | Some _ -> | ||||||
|  |             do! rethink { | ||||||
|  |                 withTable Table.Page | ||||||
|  |                 get pageId | ||||||
|  |                 update [ "priorPermalinks", permalinks :> obj ] | ||||||
|  |                 write; withRetryDefault; ignoreResult conn | ||||||
|  |             } | ||||||
|  |             return true | ||||||
|  |         | None -> return false | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| /// Functions to manipulate posts | /// Functions to manipulate posts | ||||||
| module Post = | module Post = | ||||||
| @ -542,6 +557,27 @@ module Post = | |||||||
|             write; withRetryDefault; ignoreResult |             write; withRetryDefault; ignoreResult | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |     /// Update prior permalinks for a post | ||||||
|  |     let updatePriorPermalinks (postId : PostId) webLogId (permalinks : Permalink list) conn = task { | ||||||
|  |         match! ( | ||||||
|  |             rethink<Post> { | ||||||
|  |                 withTable Table.Post | ||||||
|  |                 get postId | ||||||
|  |                 without [ "revisions"; "priorPermalinks" ] | ||||||
|  |                 resultOption; withRetryOptionDefault | ||||||
|  |             } | ||||||
|  |             |> verifyWebLog webLogId (fun p -> p.webLogId)) conn with | ||||||
|  |         | Some _ -> | ||||||
|  |             do! rethink { | ||||||
|  |                 withTable Table.Post | ||||||
|  |                 get postId | ||||||
|  |                 update [ "priorPermalinks", permalinks :> obj ] | ||||||
|  |                 write; withRetryDefault; ignoreResult conn | ||||||
|  |             } | ||||||
|  |             return true | ||||||
|  |         | None -> return false | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| /// Functions to manipulate web logs | /// Functions to manipulate web logs | ||||||
| module WebLog = | module WebLog = | ||||||
|  | |||||||
| @ -299,6 +299,44 @@ type LogOnModel = | |||||||
|         { emailAddress = ""; password = ""; returnTo = None } |         { emailAddress = ""; password = ""; returnTo = None } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | /// View model to manage permalinks | ||||||
|  | [<CLIMutable; NoComparison; NoEquality>] | ||||||
|  | type ManagePermalinksModel = | ||||||
|  |     {   /// The ID for the entity being edited | ||||||
|  |         id : string | ||||||
|  |          | ||||||
|  |         /// The type of entity being edited ("page" or "post") | ||||||
|  |         entity : string | ||||||
|  |          | ||||||
|  |         /// The current title of the page or post | ||||||
|  |         currentTitle : string | ||||||
|  |          | ||||||
|  |         /// The current permalink of the page or post | ||||||
|  |         currentPermalink : string | ||||||
|  |          | ||||||
|  |         /// The prior permalinks for the page or post | ||||||
|  |         prior : string[] | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     /// Create a permalink model from a page | ||||||
|  |     static member fromPage (pg : Page) = | ||||||
|  |         { id               = PageId.toString pg.id | ||||||
|  |           entity           = "page" | ||||||
|  |           currentTitle     = pg.title | ||||||
|  |           currentPermalink = Permalink.toString pg.permalink | ||||||
|  |           prior            = pg.priorPermalinks |> List.map Permalink.toString |> Array.ofList | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     /// Create a permalink model from a post | ||||||
|  |     static member fromPost (post : Post) = | ||||||
|  |         { id               = PostId.toString post.id | ||||||
|  |           entity           = "post" | ||||||
|  |           currentTitle     = post.title | ||||||
|  |           currentPermalink = Permalink.toString post.permalink | ||||||
|  |           prior            = post.priorPermalinks |> List.map Permalink.toString |> Array.ofList | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| /// View model for posts in a list | /// View model for posts in a list | ||||||
| [<NoComparison; NoEquality>] | [<NoComparison; NoEquality>] | ||||||
| type PostListItem = | type PostListItem = | ||||||
|  | |||||||
| @ -45,6 +45,31 @@ let edit pgId : HttpHandler = requireUser >=> fun next ctx -> task { | |||||||
|     | None -> return! Error.notFound next ctx |     | None -> return! Error.notFound next ctx | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // GET /page/{id}/permalinks | ||||||
|  | let editPermalinks pgId : HttpHandler = requireUser >=> fun next ctx -> task { | ||||||
|  |     match! Data.Page.findByFullId (PageId pgId) (webLogId ctx) (conn ctx) with | ||||||
|  |     | Some pg -> | ||||||
|  |         return! | ||||||
|  |             Hash.FromAnonymousObject {| | ||||||
|  |                 csrf       = csrfToken ctx | ||||||
|  |                 model      = ManagePermalinksModel.fromPage pg | ||||||
|  |                 page_title = $"Manage Prior Permalinks" | ||||||
|  |             |} | ||||||
|  |             |> viewForTheme "admin" "permalinks" next ctx | ||||||
|  |     | None -> return! Error.notFound next ctx | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // POST /page/permalinks | ||||||
|  | let savePermalinks : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | ||||||
|  |     let! model = ctx.BindFormAsync<ManagePermalinksModel> () | ||||||
|  |     let  links = model.prior |> Array.map Permalink |> List.ofArray | ||||||
|  |     match! Data.Page.updatePriorPermalinks (PageId model.id) (webLogId ctx) links (conn ctx) with | ||||||
|  |     | true -> | ||||||
|  |         do! addMessage ctx { UserMessage.success with message = "Page permalinks saved successfully" } | ||||||
|  |         return! redirectToGet $"/page/{model.id}/permalinks" next ctx | ||||||
|  |     | false -> return! Error.notFound next ctx | ||||||
|  | } | ||||||
|  | 
 | ||||||
| open System | open System | ||||||
| 
 | 
 | ||||||
| #nowarn "3511" | #nowarn "3511" | ||||||
|  | |||||||
| @ -328,6 +328,31 @@ let edit postId : HttpHandler = requireUser >=> fun next ctx -> task { | |||||||
|     | None -> return! Error.notFound next ctx |     | None -> return! Error.notFound next ctx | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // GET /post/{id}/permalinks | ||||||
|  | let editPermalinks postId : HttpHandler = requireUser >=> fun next ctx -> task { | ||||||
|  |     match! Data.Post.findByFullId (PostId postId) (webLogId ctx) (conn ctx) with | ||||||
|  |     | Some post -> | ||||||
|  |         return! | ||||||
|  |             Hash.FromAnonymousObject {| | ||||||
|  |                 csrf       = csrfToken ctx | ||||||
|  |                 model      = ManagePermalinksModel.fromPost post | ||||||
|  |                 page_title = $"Manage Prior Permalinks" | ||||||
|  |             |} | ||||||
|  |             |> viewForTheme "admin" "permalinks" next ctx | ||||||
|  |     | None -> return! Error.notFound next ctx | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // POST /post/permalinks | ||||||
|  | let savePermalinks : HttpHandler = requireUser >=> validateCsrf >=> fun next ctx -> task { | ||||||
|  |     let! model = ctx.BindFormAsync<ManagePermalinksModel> () | ||||||
|  |     let  links = model.prior |> Array.map Permalink |> List.ofArray | ||||||
|  |     match! Data.Post.updatePriorPermalinks (PostId model.id) (webLogId ctx) links (conn ctx) with | ||||||
|  |     | true -> | ||||||
|  |         do! addMessage ctx { UserMessage.success with message = "Post permalinks saved successfully" } | ||||||
|  |         return! redirectToGet $"/post/{model.id}/permalinks" next ctx | ||||||
|  |     | false -> return! Error.notFound next ctx | ||||||
|  | } | ||||||
|  | 
 | ||||||
| #nowarn "3511" | #nowarn "3511" | ||||||
| 
 | 
 | ||||||
| // POST /post/save | // POST /post/save | ||||||
|  | |||||||
| @ -30,24 +30,27 @@ let endpoints = [ | |||||||
|     ] |     ] | ||||||
|     subRoute "/page" [ |     subRoute "/page" [ | ||||||
|         GET [ |         GET [ | ||||||
|             routef "/%d"       Post.pageOfPosts |             routef "/%d"            Post.pageOfPosts | ||||||
|             //routef "/%d/"      (fun pg -> redirectTo true $"/page/{pg}") |             routef "/%s/edit"       Page.edit | ||||||
|             routef "/%s/edit"  Page.edit |             routef "/%s/permalinks" Page.editPermalinks | ||||||
|             route  "s"         (Page.all 1) |             route  "s"              (Page.all 1) | ||||||
|             routef "s/page/%d" Page.all |             routef "s/page/%d"      Page.all | ||||||
|         ] |         ] | ||||||
|         POST [ |         POST [ | ||||||
|             route "/save" Page.save |             route "/permalinks" Page.savePermalinks | ||||||
|  |             route "/save"       Page.save | ||||||
|         ] |         ] | ||||||
|     ] |     ] | ||||||
|     subRoute "/post" [ |     subRoute "/post" [ | ||||||
|         GET [ |         GET [ | ||||||
|             routef "/%s/edit"  Post.edit |             routef "/%s/edit"       Post.edit | ||||||
|             route  "s"         (Post.all 1) |             routef "/%s/permalinks" Post.editPermalinks | ||||||
|             routef "s/page/%d" Post.all |             route  "s"              (Post.all 1) | ||||||
|  |             routef "s/page/%d"      Post.all | ||||||
|         ] |         ] | ||||||
|         POST [ |         POST [ | ||||||
|             route "/save" Post.save |             route "/permalinks" Post.savePermalinks | ||||||
|  |             route "/save"       Post.save | ||||||
|         ] |         ] | ||||||
|     ] |     ] | ||||||
|     subRoute "/tag" [ |     subRoute "/tag" [ | ||||||
|  | |||||||
| @ -253,9 +253,10 @@ let main args = | |||||||
|     [   // Domain types |     [   // Domain types | ||||||
|         typeof<MetaItem>; typeof<Page>; typeof<WebLog> |         typeof<MetaItem>; typeof<Page>; typeof<WebLog> | ||||||
|         // View models |         // View models | ||||||
|         typeof<DashboardModel>; typeof<DisplayCategory>; typeof<DisplayPage>;   typeof<EditCategoryModel> |         typeof<DashboardModel>;        typeof<DisplayCategory>; typeof<DisplayPage>;   typeof<EditCategoryModel> | ||||||
|         typeof<EditPageModel>;  typeof<EditPostModel>;   typeof<EditUserModel>; typeof<LogOnModel> |         typeof<EditPageModel>;         typeof<EditPostModel>;   typeof<EditUserModel>; typeof<LogOnModel> | ||||||
|         typeof<PostDisplay>;    typeof<PostListItem>;    typeof<SettingsModel>; typeof<UserMessage> |         typeof<ManagePermalinksModel>; typeof<PostDisplay>;     typeof<PostListItem>;  typeof<SettingsModel> | ||||||
|  |         typeof<UserMessage> | ||||||
|         // Framework types |         // Framework types | ||||||
|         typeof<AntiforgeryTokenSet>; typeof<KeyValuePair>; typeof<MetaItem list>; typeof<string list> |         typeof<AntiforgeryTokenSet>; typeof<KeyValuePair>; typeof<MetaItem list>; typeof<string list> | ||||||
|         typeof<string option> |         typeof<string option> | ||||||
|  | |||||||
| @ -3,5 +3,5 @@ | |||||||
|     "hostname": "data02.bitbadger.solutions", |     "hostname": "data02.bitbadger.solutions", | ||||||
|     "database": "myWebLog_dev" |     "database": "myWebLog_dev" | ||||||
|   }, |   }, | ||||||
|   "Generator": "myWebLog 2.0-alpha05" |   "Generator": "myWebLog 2.0-alpha06" | ||||||
| } | } | ||||||
|  | |||||||
| @ -15,6 +15,9 @@ | |||||||
|             <input type="text" name="permalink" id="permalink" class="form-control" required |             <input type="text" name="permalink" id="permalink" class="form-control" required | ||||||
|                    value="{{ model.permalink }}"> |                    value="{{ model.permalink }}"> | ||||||
|             <label for="permalink">Permalink</label> |             <label for="permalink">Permalink</label> | ||||||
|  |             {%- if model.page_id != "new" %} | ||||||
|  |               <span class="form-text"><a href="/page/{{ model.page_id }}/permalinks">Manage Permalinks</a></span> | ||||||
|  |             {% endif -%} | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|         <div class="col-3"> |         <div class="col-3"> | ||||||
|  | |||||||
							
								
								
									
										57
									
								
								src/MyWebLog/themes/admin/permalinks.liquid
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/MyWebLog/themes/admin/permalinks.liquid
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,57 @@ | |||||||
|  | <h2 class="my-3">{{ page_title }}</h2> | ||||||
|  | <article> | ||||||
|  |   <form action="/{{ model.entity }}/permalinks" method="post"> | ||||||
|  |     <input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}"> | ||||||
|  |     <input type="hidden" name="id" value="{{ model.id }}"> | ||||||
|  |     <div class="container"> | ||||||
|  |       <div class="row"> | ||||||
|  |         <div class="col"> | ||||||
|  |           <p style="line-height:1.2rem;"> | ||||||
|  |             <strong>{{ model.current_title }}</strong><br> | ||||||
|  |             <small class="text-muted"> | ||||||
|  |               <span class="fst-italic">{{ model.current_permalink }}</span><br> | ||||||
|  |               <a href="/{{ model.entity }}/{{ model.id }}/edit">« Back to Edit {{ model.entity | capitalize }}</a> | ||||||
|  |             </small> | ||||||
|  |           </p> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |       <div class="row mb-3"> | ||||||
|  |         <div class="col"> | ||||||
|  |           <button type="button" class="btn btn-sm btn-secondary" onclick="Admin.addPermalink()">Add a Permalink</button> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |       <div class="row mb-3"> | ||||||
|  |         <div class="col"> | ||||||
|  |           <div id="permalinks" class="container"> | ||||||
|  |             {%- assign link_count = 0 -%} | ||||||
|  |             {%- for link in model.prior %} | ||||||
|  |               <div id="link_{{ link_count }}" class="row mb-3"> | ||||||
|  |                 <div class="col-1 text-center align-self-center"> | ||||||
|  |                   <button type="button" class="btn btn-sm btn-danger" onclick="Admin.removePermalink({{ link_count }})"> | ||||||
|  |                     − | ||||||
|  |                   </button> | ||||||
|  |                 </div> | ||||||
|  |                 <div class="col-11"> | ||||||
|  |                   <div class="form-floating"> | ||||||
|  |                     <input type="text" name="prior" id="prior_{{ link_count }}" class="form-control" | ||||||
|  |                            placeholder="Link" value="{{ link }}"> | ||||||
|  |                     <label for="prior_{{ link_count }}">Link</label> | ||||||
|  |                   </div> | ||||||
|  |                 </div> | ||||||
|  |               </div> | ||||||
|  |               {%- assign link_count = link_count | plus: 1 -%} | ||||||
|  |             {% endfor -%} | ||||||
|  |             <script> | ||||||
|  |               document.addEventListener("DOMContentLoaded", () => Admin.setPermalinkIndex({{ link_count }})) | ||||||
|  |             </script> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |       <div class="row pb-3"> | ||||||
|  |         <div class="col"> | ||||||
|  |           <button type="submit" class="btn btn-primary">Save Changes</button> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </form> | ||||||
|  | </article> | ||||||
| @ -15,6 +15,9 @@ | |||||||
|             <input type="text" name="permalink" id="permalink" class="form-control" placeholder="Permalink" required |             <input type="text" name="permalink" id="permalink" class="form-control" placeholder="Permalink" required | ||||||
|                    value="{{ model.permalink }}"> |                    value="{{ model.permalink }}"> | ||||||
|             <label for="permalink">Permalink</label> |             <label for="permalink">Permalink</label> | ||||||
|  |             {%- if model.page_id != "new" %} | ||||||
|  |               <span class="form-text"><a href="/post/{{ model.post_id }}/permalinks">Manage Permalinks</a></span> | ||||||
|  |             {% endif -%} | ||||||
|           </div> |           </div> | ||||||
|           <div class="mb-2"> |           <div class="mb-2"> | ||||||
|             <label for="text">Text</label>     |             <label for="text">Text</label>     | ||||||
|  | |||||||
| @ -9,6 +9,7 @@ body { | |||||||
|   scrollbar-color: var(--dark-gray) gray; |   scrollbar-color: var(--dark-gray) gray; | ||||||
|   padding-top: 3rem; |   padding-top: 3rem; | ||||||
|   padding-bottom: 2rem; |   padding-bottom: 2rem; | ||||||
|  |   min-height: 100vh; | ||||||
| } | } | ||||||
| body::-webkit-scrollbar { | body::-webkit-scrollbar { | ||||||
|   width: 12px; |   width: 12px; | ||||||
|  | |||||||
| @ -2,15 +2,25 @@ | |||||||
|   /** The next index for a metadata item */ |   /** The next index for a metadata item */ | ||||||
|   nextMetaIndex : 0, |   nextMetaIndex : 0, | ||||||
| 
 | 
 | ||||||
|  |   /** The next index for a permalink */ | ||||||
|  |   nextPermalink : 0, | ||||||
|  |    | ||||||
|   /** |   /** | ||||||
|    * Set the next meta item index |    * Set the next meta item index | ||||||
|    * @param idx The index to set |    * @param idx The index to set | ||||||
|    */ |    */ | ||||||
|   // Calling a function with a Liquid variable does not look like an error in the IDE...
 |  | ||||||
|   setNextMetaIndex(idx) { |   setNextMetaIndex(idx) { | ||||||
|     this.nextMetaIndex = idx |     this.nextMetaIndex = idx | ||||||
|   }, |   }, | ||||||
|    | 
 | ||||||
|  |   /** | ||||||
|  |    * Set the next permalink index | ||||||
|  |    * @param idx The index to set | ||||||
|  |    */ | ||||||
|  |   setPermalinkIndex(idx) { | ||||||
|  |     this.nextPermalink = idx | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * Add a new row for metadata entry |    * Add a new row for metadata entry | ||||||
|    */ |    */ | ||||||
| @ -80,7 +90,55 @@ | |||||||
|     document.getElementById(nameField.id).focus() |     document.getElementById(nameField.id).focus() | ||||||
|     this.nextMetaIndex++ |     this.nextMetaIndex++ | ||||||
|   }, |   }, | ||||||
|    | 
 | ||||||
|  |   /** | ||||||
|  |    * Add a new row for a permalink | ||||||
|  |    */ | ||||||
|  |   addPermalink() { | ||||||
|  |     // Remove button
 | ||||||
|  |     const removeBtn = document.createElement("button") | ||||||
|  |     removeBtn.type      = "button" | ||||||
|  |     removeBtn.className = "btn btn-sm btn-danger" | ||||||
|  |     removeBtn.innerHTML = "−" | ||||||
|  |     removeBtn.setAttribute("onclick", `Admin.removePermalink(${this.nextPermalink})`) | ||||||
|  | 
 | ||||||
|  |     const removeCol = document.createElement("div") | ||||||
|  |     removeCol.className = "col-1 text-center align-self-center" | ||||||
|  |     removeCol.appendChild(removeBtn) | ||||||
|  | 
 | ||||||
|  |     // Link
 | ||||||
|  |     const linkField = document.createElement("input") | ||||||
|  |     linkField.type        = "text" | ||||||
|  |     linkField.name        = "prior" | ||||||
|  |     linkField.id          = `prior_${this.nextPermalink}` | ||||||
|  |     linkField.className   = "form-control" | ||||||
|  |     linkField.placeholder = "Link" | ||||||
|  | 
 | ||||||
|  |     const linkLabel = document.createElement("label") | ||||||
|  |     linkLabel.htmlFor   = linkField.id | ||||||
|  |     linkLabel.innerText = linkField.placeholder | ||||||
|  | 
 | ||||||
|  |     const linkFloat = document.createElement("div") | ||||||
|  |     linkFloat.className = "form-floating" | ||||||
|  |     linkFloat.appendChild(linkField) | ||||||
|  |     linkFloat.appendChild(linkLabel) | ||||||
|  | 
 | ||||||
|  |     const linkCol = document.createElement("div") | ||||||
|  |     linkCol.className = "col-11" | ||||||
|  |     linkCol.appendChild(linkFloat) | ||||||
|  | 
 | ||||||
|  |     // Put it all together
 | ||||||
|  |     const newRow = document.createElement("div") | ||||||
|  |     newRow.className = "row mb-3" | ||||||
|  |     newRow.id        = `meta_${this.nextPermalink}` | ||||||
|  |     newRow.appendChild(removeCol) | ||||||
|  |     newRow.appendChild(linkCol) | ||||||
|  | 
 | ||||||
|  |     document.getElementById("permalinks").appendChild(newRow) | ||||||
|  |     document.getElementById(linkField.id).focus() | ||||||
|  |     this.nextPermalink++ | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * Remove a metadata item |    * Remove a metadata item | ||||||
|    * @param idx The index of the metadata item to remove |    * @param idx The index of the metadata item to remove | ||||||
| @ -88,7 +146,15 @@ | |||||||
|   removeMetaItem(idx) { |   removeMetaItem(idx) { | ||||||
|     document.getElementById(`meta_${idx}`).remove() |     document.getElementById(`meta_${idx}`).remove() | ||||||
|   }, |   }, | ||||||
|    | 
 | ||||||
|  |   /** | ||||||
|  |    * Remove a permalink | ||||||
|  |    * @param idx The index of the permalink to remove | ||||||
|  |    */ | ||||||
|  |   removePermalink(idx) { | ||||||
|  |     document.getElementById(`link_${idx}`).remove() | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * Confirm and delete a category |    * Confirm and delete a category | ||||||
|    * @param id The ID of the category to be deleted |    * @param id The ID of the category to be deleted | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user