From 1f0e940ba5590b0a707d96692defbdce92fb1c1a Mon Sep 17 00:00:00 2001
From: Lukasz Sojka <lukasz.sojka@scylladb.com>
Date: Mon, 23 Dec 2024 15:24:18 +0100
Subject: [PATCH] improvement(highlights): add markdown support

Added support for highlights widget.
Reused 'comments' editor known from job discussions.
Thus implementing all that is supported by this component:
titles, links, multi-line content, code blocks etc.

One caveat: mentioning does not trigger notifications yet. This is going
to be done in followup task.

closes: https://github.com/scylladb/argus/issues/510
---
 frontend/Discussion/CommentEditor.svelte      |  11 +-
 .../Widgets/ViewHighlights/ActionItem.svelte  | 128 +++++++++++-------
 .../ViewHighlights/HighlightItem.svelte       |  91 ++++++++-----
 .../ViewHighlights/ViewHighlights.svelte      |  41 ++++--
 4 files changed, 171 insertions(+), 100 deletions(-)

diff --git a/frontend/Discussion/CommentEditor.svelte b/frontend/Discussion/CommentEditor.svelte
index 5c487bd3..a0bf3ea9 100644
--- a/frontend/Discussion/CommentEditor.svelte
+++ b/frontend/Discussion/CommentEditor.svelte
@@ -1,5 +1,5 @@
 <script>
-    import { createEventDispatcher } from "svelte";
+    import { createEventDispatcher, onMount } from "svelte";
     import { parse as markdownParse } from "marked";
     import Fa from "svelte-fa";
     import {
@@ -33,6 +33,11 @@
         test_run_id: "",
         posted_at: new Date(),
     };
+    export let entryType = "comment";
+
+    onMount(() => {
+        textArea?.focus();
+    });
 
     const randomTip = function () {
         const editorTips = [
@@ -332,7 +337,7 @@
             <span class="fw-bold"><Fa icon={faLightbulb} /> Tip</span>: {randomTip()}
         </div>
         <div class="ms-auto text-end">
-            {#if mode == "edit"}
+            {#if mode == "edit" || entryType != "comment"}
                 <button
                     class="btn btn-danger"
                     on:click={() => {
@@ -341,7 +346,7 @@
                 >
             {/if}
             <button class="btn btn-success" on:click={handleSubmitComment}
-                >{mode == "post" ? "Submit" : "Update"} comment</button
+            >{mode == "post" ? "Submit" : "Update"} {entryType}</button
             >
         </div>
     </div>
diff --git a/frontend/Views/Widgets/ViewHighlights/ActionItem.svelte b/frontend/Views/Widgets/ViewHighlights/ActionItem.svelte
index c08c0112..83291816 100644
--- a/frontend/Views/Widgets/ViewHighlights/ActionItem.svelte
+++ b/frontend/Views/Widgets/ViewHighlights/ActionItem.svelte
@@ -5,6 +5,10 @@
     import {createEventDispatcher} from 'svelte';
     import {getPicture} from "../../../Common/UserUtils";
     import AssigneeSelector from "../../../Common/AssigneeSelector.svelte";
+    import CommentEditor from "../../../Discussion/CommentEditor.svelte";
+    import * as marked from "marked";
+    import {MarkdownUserMention} from "../../../Discussion/MarkedMentionExtension";
+    import {markdownRendererOptions} from "../../../markdownOptions";
 
     export let action;
     export let currentUserId;
@@ -16,7 +20,20 @@
     let assignee_id = action.assignee_id;
     let isArchived = action.isArchived;
     const dispatch = createEventDispatcher();
-
+    let commentBody = {
+        id: '',
+        message: action.content,
+        release: '',
+        reactions: {},
+        mentions: [],
+        user_id: '',
+        release_id: '',
+        test_run_id: '',
+        posted_at: new Date(),
+    };
+    marked.use({
+        extensions: [MarkdownUserMention]
+    });
     const toggleArchive = () => dispatch('toggleArchive', {action});
     const toggleComments = () => {
         action.showComments = !action.showComments;
@@ -24,9 +41,10 @@
             dispatch('loadComments', {action});
         }
     };
-    const updateContent = () => {
-        dispatch('updateContent', {action, newContent: action.content});
+    const updateContent = (e) => {
+        commentBody.message = e.detail.message;
         isEditing = false;
+        dispatch('updateContent', {action, newContent: commentBody.message});
     };
     const addComment = () => {
         if (!newComment.trim()) return;
@@ -42,57 +60,63 @@
 
 <li class="list-group-item" class:bg-light={isArchived}>
     <div class="d-flex justify-content-between align-items-center">
-        <div class="img-profile me-2" style="background-image: url('{getPicture(creator?.picture_id)}');" data-bs-toggle="tooltip"
-             title="{creator?.username}"/>
-        <div class="d-flex align-items-center flex-grow-1">
-            <input id="checkbox-{action.id}" class="form-check-input me-2" type="checkbox" disabled={isArchived}
-                   bind:checked={action.isCompleted} on:change={toggleComplete}>
-            {#if isEditing}
-                <input type="text" class="form-control me-2" bind:value={action.content}
-                       on:keydown={(e) => { if(e.key === 'Enter') updateContent(); }}>
-                <button class="btn btn-sm btn-primary me-2" on:click={updateContent}>Save</button>
-                <button class="btn btn-sm btn-secondary me-2" on:click={() => isEditing = false}>Cancel</button>
-            {:else}
-                <label class="form-check-label" for="checkbox-{action.id}">{action.content}</label>
-            {/if}
-        </div>
-        <div class="d-flex align-items-center">
-            <small class="text-muted me-2">{action.createdAt.toLocaleDateString("en-CA")}</small>
-            <div class="select-width ">
-                <AssigneeSelector {assignee_id} on:select={handleAssignee} disabled={isArchived} on:clear={handleAssignee}/>
+        {#if !isEditing}
+            <div class="img-profile me-2" style="background-image: url('{getPicture(creator?.picture_id)}');" data-bs-toggle="tooltip"
+                 title="{creator?.username}"/>
+            <div class="d-flex align-items-center flex-grow-1">
+                <input id="checkbox-{action.id}" class="form-check-input me-2 mt-0" type="checkbox" disabled={isArchived}
+                       bind:checked={action.isCompleted} on:change={toggleComplete}>
+                <label class="form-check-label no-bottom-margin"
+                       for="checkbox-{action.id}">{@html marked.parse(action.content, markdownRendererOptions)}
+                </label>
             </div>
-            <button class="btn btn-sm btn-outline-secondary mx-1" on:click={toggleComments}>
-                <Fa icon={faComment}/>
-                <span class="ms-1">{action.comments_count}</span>
-            </button>
-            {#if !action.isArchived}
-                {#if action.creator_id === currentUserId}
-                    <button class="btn btn-sm btn-outline-secondary me-1" on:click={() => isEditing = !isEditing}>
-                        <Fa icon={faEdit}/>
-                    </button>
-                {/if}
-            {/if}
-            <button class="btn btn-sm btn-outline-secondary" on:click={toggleArchive}>
-                <Fa icon={action.isArchived ? faUndo : faArchive}/>
-            </button>
-        </div>
-    </div>
-    {#if action.showComments}
-        <div class="mt-2 col-md-10">
-            <ul class="list-group list-group-flush">
-                {#each action.comments as comment (comment.id)}
-                    <Comment {comment} {currentUserId} {action} on:deleteComment on:updateCommentContent/>
-                {/each}
-            </ul>
-            {#if !action.isArchived}
-                <div class="input-group mt-2">
-                    <input type="text" class="form-control" placeholder="Add a comment" bind:value={newComment}
-                           on:keydown={handleCommentKeydown}>
-                    <button class="btn btn-outline-primary" on:click={addComment}>Add</button>
+            <div class="d-flex align-items-center">
+                <small class="text-muted me-2">{action.createdAt.toLocaleDateString("en-CA")}</small>
+                <div class="select-width ">
+                    <AssigneeSelector {assignee_id} on:select={handleAssignee} disabled={isArchived} on:clear={handleAssignee}/>
                 </div>
-            {/if}
-        </div>
-    {/if}
+                <button class="btn btn-sm btn-outline-secondary mx-1" on:click={toggleComments}>
+                    <Fa icon={faComment}/>
+                    <span class="ms-1">{action.comments_count}</span>
+                </button>
+                {#if !action.isArchived}
+                    {#if action.creator_id === currentUserId}
+                        <button class="btn btn-sm btn-outline-secondary me-1" on:click={() => isEditing = !isEditing}>
+                            <Fa icon={faEdit}/>
+                        </button>
+                    {/if}
+                {/if}
+                <button class="btn btn-sm btn-outline-secondary" on:click={toggleArchive}>
+                    <Fa icon={action.isArchived ? faUndo : faArchive}/>
+                </button>
+            </div>
+        {:else}
+            <div class="flex-grow-1">
+                <CommentEditor
+                        commentBody={Object.assign({}, commentBody)}
+                        mode="edit"
+                        entryType="action item"
+                        on:submitComment={updateContent}
+                        on:cancelEditing={() => (isEditing = false)}
+                />
+            </div>
+        {/if}
+        {#if action.showComments}
+            <div class="mt-2 col-md-10">
+                <ul class="list-group list-group-flush">
+                    {#each action.comments as comment (comment.id)}
+                        <Comment {comment} {currentUserId} {action} on:deleteComment on:updateCommentContent/>
+                    {/each}
+                </ul>
+                {#if !action.isArchived}
+                    <div class="input-group mt-2">
+                        <input type="text" class="form-control" placeholder="Add a comment" bind:value={newComment}
+                               on:keydown={handleCommentKeydown}>
+                        <button class="btn btn-outline-primary" on:click={addComment}>Add</button>
+                    </div>
+                {/if}
+            </div>
+        {/if}
 </li>
 
 <style>
diff --git a/frontend/Views/Widgets/ViewHighlights/HighlightItem.svelte b/frontend/Views/Widgets/ViewHighlights/HighlightItem.svelte
index d6e8a866..32bc377b 100644
--- a/frontend/Views/Widgets/ViewHighlights/HighlightItem.svelte
+++ b/frontend/Views/Widgets/ViewHighlights/HighlightItem.svelte
@@ -5,6 +5,10 @@
     import {createEventDispatcher} from 'svelte';
     import UserProfile from "../../../Discussion/UserProfile.svelte";
     import {getPicture} from "../../../Common/UserUtils";
+    import CommentEditor from "../../../Discussion/CommentEditor.svelte";
+    import * as marked from "marked";
+    import {markdownRendererOptions} from "../../../markdownOptions";
+    import {MarkdownUserMention} from "../../../Discussion/MarkedMentionExtension";
 
     export let highlight;
     export let currentUserId;
@@ -13,6 +17,20 @@
     let isEditing = false;
     let newComment = '';
     let creationTimeStr = highlight.createdAt.toLocaleDateString("en-CA");
+    let commentBody = {
+        id: '',
+        message: highlight.content,
+        release: '',
+        reactions: {},
+        mentions: [],
+        user_id: '',
+        release_id: '',
+        test_run_id: '',
+        posted_at: new Date(),
+    };
+    marked.use({
+        extensions: [MarkdownUserMention]
+    });
     const dispatch = createEventDispatcher();
 
     const toggleArchive = () => dispatch('toggleArchive', {highlight});
@@ -22,10 +40,7 @@
             dispatch('loadComments', {highlight});
         }
     };
-    const updateContent = () => {
-        dispatch('updateContent', {highlight, newContent: highlight.content});
-        isEditing = false;
-    };
+
     const addComment = () => {
         if (!newComment.trim()) return;
         dispatch('addComment', {highlight, content: newComment});
@@ -34,39 +49,49 @@
     const handleCommentKeydown = (event: KeyboardEvent) => {
         if (event.key === 'Enter') addComment();
     };
-
+    const handleCommentUpdate = function (e) {
+        commentBody.message = e.detail.message;
+        isEditing = false;
+        dispatch('updateContent', {highlight, newContent: commentBody.message});
+    };
 </script>
 
 <li class="list-group-item" class:bg-light={highlight.isArchived}>
-    <div class="d-flex justify-content-between align-items-center">
-        <div class="img-profile me-2" style="background-image: url('{getPicture(creator?.picture_id)}');" data-bs-toggle="tooltip" title="{creator?.username}" />
-        <div class="d-flex align-items-center flex-grow-1">
-            {#if isEditing}
-                <input type="text" class="form-control me-2" bind:value={highlight.content}
-                       on:keydown={(e) => { if(e.key === 'Enter') updateContent(); }}>
-                <button class="btn btn-sm btn-primary me-2" on:click={updateContent}>Save</button>
-                <button class="btn btn-sm btn-secondary me-2" on:click={() => isEditing = false}>Cancel</button>
-            {:else}
-                <span>{highlight.content}</span>
-            {/if}
-        </div>
-        <div class="d-flex align-items-center">
-            <small class="text-muted me-2">{creationTimeStr}</small>
-            <button class="btn btn-sm btn-outline-secondary me-1" on:click={toggleComments}>
-                <Fa icon={faComment}/>
-                <span class="ms-1">{highlight.comments_count}</span>
-            </button>
-            {#if !highlight.isArchived}
-                {#if highlight.creator_id === currentUserId}
-                    <button class="btn btn-sm btn-outline-secondary me-1" on:click={() => isEditing = !isEditing}>
-                        <Fa icon={faEdit}/>
-                    </button>
+    <div class="d-flex justify-content-between  align-items-center">
+        {#if !isEditing}
+            <div class="img-profile me-2" style="background-image: url('{getPicture(creator?.picture_id)}');" data-bs-toggle="tooltip"
+                 title="{creator?.username}"/>
+            <div class="d-flex align-items-center flex-grow-1 no-bottom-margin-p">
+                <span class="no-bottom-margin">{@html marked.parse(highlight.content, markdownRendererOptions)}</span>
+            </div>
+            <div class="d-flex align-items-center">
+                <small class="text-muted me-2">{creationTimeStr}</small>
+                <button class="btn btn-sm btn-outline-secondary me-1" on:click={toggleComments}>
+                    <Fa icon={faComment}/>
+                    <span class="ms-1">{highlight.comments_count}</span>
+                </button>
+                {#if !highlight.isArchived}
+                    {#if highlight.creator_id === currentUserId}
+                        <button class="btn btn-sm btn-outline-secondary me-1" on:click={() => isEditing = !isEditing}>
+                            <Fa icon={faEdit}/>
+                        </button>
+                    {/if}
                 {/if}
-            {/if}
-            <button class="btn btn-sm btn-outline-secondary" on:click={toggleArchive}>
-                <Fa icon={highlight.isArchived ? faUndo : faArchive}/>
-            </button>
-        </div>
+                <button class="btn btn-sm btn-outline-secondary" on:click={toggleArchive}>
+                    <Fa icon={highlight.isArchived ? faUndo : faArchive}/>
+                </button>
+            </div>
+        {:else}
+            <div class="flex-grow-1">
+                <CommentEditor
+                        commentBody={Object.assign({}, commentBody)}
+                        mode="edit"
+                        entryType="highlight"
+                        on:submitComment={handleCommentUpdate}
+                        on:cancelEditing={() => (isEditing = false)}
+                />
+            </div>
+        {/if}
     </div>
     {#if highlight.showComments}
         <div class="mt-2 col-md-10">
diff --git a/frontend/Views/Widgets/ViewHighlights/ViewHighlights.svelte b/frontend/Views/Widgets/ViewHighlights/ViewHighlights.svelte
index 1ed4f75f..abfcc292 100644
--- a/frontend/Views/Widgets/ViewHighlights/ViewHighlights.svelte
+++ b/frontend/Views/Widgets/ViewHighlights/ViewHighlights.svelte
@@ -6,6 +6,7 @@
     import {userList} from "../../../Stores/UserlistSubscriber";
     import HighlightItem from "./HighlightItem.svelte";
     import ActionItem from "./ActionItem.svelte";
+    import CommentEditor from "../../../Discussion/CommentEditor.svelte";
 
     export let dashboardObject;
     export let settings;
@@ -90,6 +91,7 @@
         };
     }
 
+
     const addEntry = async (type: "highlight" | "action", content: string) => {
         if (!content.trim()) return;
         const response = await fetch("/api/v1/views/widgets/highlights/create", {
@@ -116,6 +118,12 @@
             }
         }
     };
+    const handleHightlightCreate = function (e) {
+        addEntry("highlight", e.detail.message);
+    };
+    const handleActionItemCreate = function (e) {
+        addEntry("action", e.detail.message);
+    };
 
     const handleToggleArchive = async (event) => {
         const entry = event.detail.highlight || event.detail.action;
@@ -314,12 +322,13 @@
                     {/each}
                     <li class="list-group-item">
                         {#if showNewHighlight}
-                            <div class="input-group">
-                                <input type="text" class="form-control" placeholder="New highlight" bind:value={newHighlight}
-                                       on:keydown={(e) => e.key === "Enter" && addEntry("highlight", newHighlight)}
-                                       bind:this={newHighlightInput}>
-                                <button class="btn btn-primary" on:click={() => addEntry("highlight", newHighlight)}>Add</button>
-                                <button class="btn btn-outline-secondary" on:click={() => showNewHighlight = false}>Cancel</button>
+                            <div class="flex-grow-1">
+                                <CommentEditor
+                                        mode="post"
+                                        entryType="highlight"
+                                        on:submitComment={handleHightlightCreate}
+                                        on:cancelEditing={() => (showNewHighlight = false)}
+                                />
                             </div>
                         {:else}
                             <button class="btn w-100 text-start text-muted" on:click={() => showNewHighlight = true}>
@@ -375,12 +384,13 @@
                     {/each}
                     <li class="list-group-item">
                         {#if showNewActionItem}
-                            <div class="input-group">
-                                <input type="text" class="form-control" placeholder="New action item" bind:value={newActionItem}
-                                       on:keydown={(e) => e.key === "Enter" && addEntry("action", newActionItem)}
-                                       bind:this={newActionInput}>
-                                <button class="btn btn-primary" on:click={() => addEntry("action", newActionItem)}>Add</button>
-                                <button class="btn btn-outline-secondary" on:click={() => showNewActionItem = false}>Cancel</button>
+                            <div class="flex-grow-1">
+                                <CommentEditor
+                                        mode="post"
+                                        entryType="action item"
+                                        on:submitComment={handleActionItemCreate}
+                                        on:cancelEditing={() => (showNewActionItem = false)}
+                                />
                             </div>
                         {:else}
                             <button class="btn w-100 text-start text-muted" on:click={() => showNewActionItem = true}>
@@ -418,3 +428,10 @@
         </div>
     {/key}
 </div>
+
+<style>
+    :global(.no-bottom-margin *:last-child) {
+    margin-bottom: 0;
+}
+
+</style>