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>