KB53 Support bookmark edition.

Change-Id: Ieacbb5c448b9afa4bc9524167e0c73618de6db48
diff --git a/src/main/java/eu/mulk/mulkcms2/benki/bookmarks/BookmarkResource.java b/src/main/java/eu/mulk/mulkcms2/benki/bookmarks/BookmarkResource.java
index 495e511..059335c 100644
--- a/src/main/java/eu/mulk/mulkcms2/benki/bookmarks/BookmarkResource.java
+++ b/src/main/java/eu/mulk/mulkcms2/benki/bookmarks/BookmarkResource.java
@@ -27,10 +27,13 @@
 import javax.validation.constraints.NotEmpty;
 import javax.validation.constraints.NotNull;
 import javax.ws.rs.Consumes;
+import javax.ws.rs.ForbiddenException;
 import javax.ws.rs.FormParam;
 import javax.ws.rs.GET;
+import javax.ws.rs.NotFoundException;
 import javax.ws.rs.POST;
 import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
 import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.Response;
@@ -76,6 +79,43 @@
     return Response.seeOther(new URI("/bookmarks")).build();
   }
 
+  @POST
+  @Transactional
+  @Authenticated
+  @Produces(WILDCARD)
+  @Consumes({APPLICATION_FORM_URLENCODED, MULTIPART_FORM_DATA})
+  @Path("{id}/edit")
+  public Response patchMessage(
+      @PathParam("id") int id,
+      @FormParam("uri") @NotNull URI uri,
+      @FormParam("title") @NotEmpty String title,
+      @FormParam("description") @CheckForNull String description,
+      @FormParam("visibility") Post.Visibility visibility)
+      throws URISyntaxException {
+
+    var user = Objects.requireNonNull(getCurrentUser());
+
+    var bookmark = getSession().byId(Bookmark.class).load(id);
+
+    if (bookmark == null) {
+      throw new NotFoundException();
+    }
+
+    if (bookmark.owner == null || !Objects.equals(bookmark.owner.id, user.id)) {
+      throw new ForbiddenException();
+    }
+
+    bookmark.uri = uri.toString();
+    bookmark.title = title;
+    bookmark.tags = Set.of();
+    bookmark.description = description;
+    bookmark.owner = user;
+
+    assignPostTargets(visibility, user, bookmark);
+
+    return Response.seeOther(new URI("/bookmarks")).build();
+  }
+
   @GET
   @Authenticated
   @Path("new")
diff --git a/src/main/resources/META-INF/resources/bookmarks/MlkBookmarkSubmissionForm.js b/src/main/resources/META-INF/resources/bookmarks/MlkBookmarkSubmissionForm.js
index 3dd3754..3b93305 100644
--- a/src/main/resources/META-INF/resources/bookmarks/MlkBookmarkSubmissionForm.js
+++ b/src/main/resources/META-INF/resources/bookmarks/MlkBookmarkSubmissionForm.js
@@ -8,7 +8,7 @@
   <link rel="stylesheet" type="text/css" href="/cms2/base.css" />
   <link rel="stylesheet" type="text/css" href="/bookmarks/MlkBookmarkSubmissionForm.css" />
 
-  <form class="pure-form" method="post" action="/bookmarks">
+  <form id="main-form" class="pure-form" method="post" action="/bookmarks">
     <fieldset>
       <legend>Edit Bookmark</legend>
 
@@ -37,10 +37,13 @@
 
 export class MlkBookmarkSubmissionForm extends HTMLElement {
   /*::
+  mainForm: HTMLFormElement;
   descriptionInput: HTMLTextAreaElement;
   titleInput: HTMLInputElement;
   uriInput: HTMLInputElement;
   uriSpinner: ProgressSpinner;
+  visibilityInput: HTMLInputElement;
+  loaded: boolean;
   */
 
   constructor() {
@@ -49,6 +52,8 @@
     let shadow = this.attachShadow({mode: "open"});
     shadow.appendChild(template.content.cloneNode(true));
 
+    this.mainForm =
+        cast(shadow.getElementById('main-form'));
     this.descriptionInput =
         cast(shadow.getElementById('description-input'));
     this.titleInput =
@@ -57,13 +62,34 @@
         cast(shadow.getElementById('uri-input'));
     this.uriSpinner =
         cast(shadow.getElementById('uri-spinner'));
+    this.visibilityInput =
+        cast(shadow.getElementById('visibility-input'));
+
+    this.loaded = false;
   }
 
   static get observedAttributes() {
     return [];
   }
 
-  connectedCallback () {
+  get editedId() /*:number | null*/ {
+    let attr = this.getAttribute("edited-id");
+    if (attr === undefined || attr === null) {
+      return null;
+    }
+    return parseInt(attr, 10);
+  }
+
+  get isEditor() {
+    return this.editedId !== null;
+  }
+
+  connectedCallback() {
+    if (this.editedId !== null) {
+      this.mainForm.method = "post";
+      this.mainForm.action = `/bookmarks/${this.editedId}/edit`;
+    }
+
     this.uriInput.addEventListener('blur', this.onUriBlur.bind(this));
     this.uriInput.value = this.uri || "";
     this.titleInput.value = this.titleText || "";
@@ -98,10 +124,11 @@
     } else {
       this.descriptionInput.focus();
     }
+    this.load();
   }
 
   async onUriBlur() {
-    if (!this.uriInput.value) {
+    if (this.isEditor || !this.uriInput.value) {
       return;
     }
 
@@ -121,6 +148,27 @@
     let pageInfo = await r.json();
     this.titleInput.value = pageInfo.title;
   }
+
+  async load() {
+    if (this.editedId === null || this.loaded) {
+      return;
+    }
+
+    let fetchUrl = new URL(`/posts/${this.editedId}`, document.URL);
+    let r = await fetch(fetchUrl);
+
+    if (!r.ok) {
+      return;
+    }
+
+    let post = await r.json();
+    this.uriInput.value = post.uri;
+    this.titleInput.value = post.title;
+    this.descriptionInput.innerText = post.description;
+    this.visibilityInput.value = post.visibility;
+
+    this.loaded = true;
+  }
 }
 
 customElements.define("mlk-bookmark-submission-form", MlkBookmarkSubmissionForm);
diff --git a/src/main/resources/META-INF/resources/cms2/base.css b/src/main/resources/META-INF/resources/cms2/base.css
index 8811bb3..bc95300 100644
--- a/src/main/resources/META-INF/resources/cms2/base.css
+++ b/src/main/resources/META-INF/resources/cms2/base.css
@@ -153,10 +153,15 @@
   border-top: lightgray solid 1px;
 }
 
+a.bookmark-title {
+  text-decoration: none;
+}
+
 h1.bookmark-title {
   font-size: 1em;
   margin: 0;
   padding: 0;
+  display: inline;
 }
 
 .bookmark-info {
@@ -164,6 +169,7 @@
   font-size: smaller;
   margin: 0;
   padding: 0;
+  flex: auto;
 }
 
 article.bookmark {
@@ -173,6 +179,10 @@
   background: #f8f0f0;
 }
 
+article.bookmark > header {
+  display: flex
+}
+
 .lazychat-message-info {
   font-style: italic;
   font-size: smaller;
@@ -209,6 +219,14 @@
   display: inline-block;
 }
 
+.bookmark-edit-button {
+  font-size: small;
+}
+
+.bookmark-message-controls {
+  flex: initial;
+}
+
 .lazychat-edit-button {
   font-size: small;
 }
diff --git a/src/main/resources/META-INF/resources/lazychat/MlkLazychatSubmissionForm.js b/src/main/resources/META-INF/resources/lazychat/MlkLazychatSubmissionForm.js
index e43e135..4c4aca8 100644
--- a/src/main/resources/META-INF/resources/lazychat/MlkLazychatSubmissionForm.js
+++ b/src/main/resources/META-INF/resources/lazychat/MlkLazychatSubmissionForm.js
@@ -61,7 +61,7 @@
     if (attr === undefined || attr === null) {
       return null;
     }
-    return parseInt(attr);
+    return parseInt(attr, 10);
   }
 
   get isEditor() {
diff --git a/src/main/resources/META-INF/resources/posts/postList.js b/src/main/resources/META-INF/resources/posts/postList.js
index 7bab4f9..8ffb7ca 100644
--- a/src/main/resources/META-INF/resources/posts/postList.js
+++ b/src/main/resources/META-INF/resources/posts/postList.js
@@ -23,4 +23,17 @@
       });
     }
   }
+
+  let bookmarks = document.getElementsByClassName('bookmark');
+  for (let bookmark of bookmarks) {
+    let editorPane = bookmark.getElementsByClassName('editor-pane')[0];
+    if (editorPane) {
+      let form = bookmark.getElementsByTagName('mlk-bookmark-submission-form')[0];
+      let editButton = bookmark.getElementsByClassName('bookmark-edit-button')[0];
+      editButton.addEventListener('click', () => {
+        editorPane.toggle();
+        form.focus();
+      });
+    }
+  }
 });
diff --git a/src/main/resources/templates/benki/posts/postList.html b/src/main/resources/templates/benki/posts/postList.html
index 5f88757..bc479f4 100644
--- a/src/main/resources/templates/benki/posts/postList.html
+++ b/src/main/resources/templates/benki/posts/postList.html
@@ -57,15 +57,32 @@
       {#if post.isBookmark}
         <article class="bookmark">
           <header>
-            <a href="{uri}"><h1 class="bookmark-title">{title}</h1></a>
             <div class="bookmark-info">
               <a class="post-link" href="/posts/{post.id}">
                 <time datetime="{date.htmlDateTime}">{date.humanDateTime}</time>
                 <span class="bookmark-owner">{owner.firstName} {owner.lastName}</span>
               </a>
             </div>
+
+            <div class="bookmark-controls">
+              {#if showBookmarkForm}
+              <button class="pure-button bookmark-edit-button">Edit</button>
+              {/if}
+            </div>
           </header>
 
+          <section class="bookmark-editor post-editor">
+            {#if showBookmarkForm}
+            <elix-expandable-panel class="bookmark-editor-pane editor-pane">
+              <mlk-bookmark-submission-form edited-id="{post.id}"></mlk-bookmark-submission-form>
+            </elix-expandable-panel>
+            {/if}
+          </section>
+
+          <section class="bookmark-title-section">
+            <a href="{uri}" class="bookmark-title"><h1 class="bookmark-title">⇢&nbsp;{title}</h1></a>
+          </section>
+
           <section class="bookmark-description">
             {descriptionHtml.raw}
           </section>
@@ -87,7 +104,7 @@
             </div>
           </header>
 
-          <section class="lazychat-editor">
+          <section class="lazychat-editor post-editor">
             {#if showLazychatForm}
               <elix-expandable-panel class="lazychat-editor-pane editor-pane">
                 <mlk-lazychat-submission-form edited-id="{post.id}"></mlk-lazychat-submission-form>