From 698d871711899fc58c48c1a6af12c405f79caca8 Mon Sep 17 00:00:00 2001 From: Scott Chacon <schacon@gmail.com> Date: Thu, 3 Oct 2024 11:04:32 +0200 Subject: [PATCH 1/9] simplify dumb styles, exercise new apis style changes --- apps/web/src/lib/components/Navigation.svelte | 13 +- apps/web/src/lib/styles/global.css | 7 - apps/web/src/routes/+layout.svelte | 18 -- .../routes/projects/[projectId]/+page.svelte | 2 +- .../branches/[branchId]/stack/+page.svelte | 8 +- .../[branchId]/stack/[changeId]/+page.svelte | 283 +++++++++++++++--- 6 files changed, 251 insertions(+), 80 deletions(-) diff --git a/apps/web/src/lib/components/Navigation.svelte b/apps/web/src/lib/components/Navigation.svelte index 9e5757e14e..1110628825 100644 --- a/apps/web/src/lib/components/Navigation.svelte +++ b/apps/web/src/lib/components/Navigation.svelte @@ -19,8 +19,8 @@ <header> <a href="/" class="nav__left"> - <img src={GitButler} width="48" alt="github" /> - <h2>GitButler</h2> + <img src={GitButler} width="32" alt="gitbutler" /> + <div class="word-mark">GitButler</div> </a> <div> <a href="/downloads">Downloads</a> @@ -45,13 +45,18 @@ <style> header { - max-width: 64rem; width: 100%; margin: 0 auto; display: flex; justify-content: space-between; align-items: center; - padding: 16px; + padding: 8px; + background: var(--clr-bg-1-muted); + } + + .word-mark { + font-size: 1.5rem; + font-weight: bold; } a { diff --git a/apps/web/src/lib/styles/global.css b/apps/web/src/lib/styles/global.css index 452518c9ca..6d0b5e734e 100644 --- a/apps/web/src/lib/styles/global.css +++ b/apps/web/src/lib/styles/global.css @@ -21,14 +21,7 @@ body { min-height: 100vh; margin: 0; background-attachment: fixed; - background-color: var(--color-bg-1); background-size: 100vw 100vh; - background-image: radial-gradient( - 50% 50% at 50% 50%, - rgba(255, 255, 255, 0.75) 0%, - rgba(255, 255, 255, 0) 100% - ), - linear-gradient(180deg, var(--color-bg-0) 0%, var(--color-bg-1) 15%, var(--color-bg-2) 50%); } h1, diff --git a/apps/web/src/routes/+layout.svelte b/apps/web/src/routes/+layout.svelte index a95ae031e7..9b33536e51 100644 --- a/apps/web/src/routes/+layout.svelte +++ b/apps/web/src/routes/+layout.svelte @@ -35,10 +35,6 @@ <main> {@render children()} </main> - - <footer> - <p>GitButler</p> - </footer> </div> <style> @@ -57,18 +53,4 @@ max-width: 84rem; margin: 0 auto; } - - footer { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - padding: 12px; - } - - @media (min-width: 480px) { - footer { - padding: 12px 0; - } - } </style> diff --git a/apps/web/src/routes/projects/[projectId]/+page.svelte b/apps/web/src/routes/projects/[projectId]/+page.svelte index 9ff60573ea..670e7e3666 100644 --- a/apps/web/src/routes/projects/[projectId]/+page.svelte +++ b/apps/web/src/routes/projects/[projectId]/+page.svelte @@ -94,7 +94,7 @@ <div>{project.name}</div> <div class="columns"> <div class="column"> - <h2>Patch Stacks</h2> + <h3>Patch Stacks</h3> {#each patchStacks as stack} <div> {stack.title}<br /> diff --git a/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/+page.svelte b/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/+page.svelte index 58a7585f0e..2a070cfb8b 100644 --- a/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/+page.svelte +++ b/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/+page.svelte @@ -48,7 +48,7 @@ {:else} <div><a href="/projects/{data.projectId}">project</a></div> - <h1>Patch Stack</h1> + <h2>Patch Stack</h2> <div class="columns"> <div class="column"> Title: <strong>{stackData.title}</strong><br /> @@ -91,6 +91,10 @@ Additions: {patch.statistics.lines - patch.statistics.deletions}, Deletions: {patch .statistics.deletions}, Files: {patch.statistics.file_count} </div> + <hr /> + <div>Viewed: {patch.review.viewed}</div> + <div>Signed Off:{patch.review.signed_off}</div> + <div>Rejected: {patch.review.rejected}</div> </div> </div> {/each} @@ -113,7 +117,7 @@ margin: 4px 0; } .patch { - background-color: #fff; + background-color: var(--clr-bg-1-muted); border: 1px solid #ccc; padding: 15px 20px; margin: 10px 0; diff --git a/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte b/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte index a8f0359f64..babca9ba1a 100644 --- a/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte +++ b/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte @@ -7,6 +7,8 @@ let state = 'loading'; let patch: any = {}; let stack: any = {}; + let status: any = {}; + let chats: any = []; let key: any = ''; let uuid: any = ''; @@ -18,6 +20,8 @@ let branchId = data.branchId; let changeId = data.changeId; + // scroll chatWindow to bottom + if (key) { fetch(env.PUBLIC_APP_HOST + 'api/patch_stack/' + projectId + '/' + branchId, { method: 'GET', @@ -31,12 +35,21 @@ stack = data; uuid = data.uuid; fetchPatch(data.uuid, changeId, key); + getPatchStatus(); + fetchAndUpdateChat(); }); } else { state = 'unauthorized'; } }); + function scrollToBottom() { + let chatWindow = document.querySelector<HTMLElement>('.chatWindow'); + if (chatWindow) { + chatWindow.scrollTop = chatWindow.scrollHeight; + } + } + function fetchPatch(uuid: string, changeId: string, key: string) { fetch(env.PUBLIC_APP_HOST + 'api/patch_stack/' + uuid + '/patch/' + changeId, { method: 'GET', @@ -144,6 +157,7 @@ }); } } + function moveSection(position: number, change: number) { console.log('Moving section at position', position, 'by', change); let ids = patch.sections.map((section: any) => section.identifier); @@ -209,11 +223,106 @@ } } } + function updatePatch() { setTimeout(() => { fetchPatch(uuid, data.changeId, key); }, 500); } + + function getPatchStatus() { + //GET /api/patch_stack/:project_id/:branch_id/patch_status + fetch( + env.PUBLIC_APP_HOST + + 'api/patch_stack/' + + data.projectId + + '/' + + data.branchId + + '/patch_status', + { + method: 'GET', + headers: { + 'X-AUTH-TOKEN': key || '' + } + } + ) + .then(async (response) => await response.json()) + .then((data) => { + status = data; + console.log('patch status'); + console.log(data); + }); + } + + function fetchAndUpdateChat() { + fetch(env.PUBLIC_APP_HOST + 'api/chat_messages/' + data.projectId + '/chats/' + data.changeId, { + method: 'GET', + headers: { + 'X-AUTH-TOKEN': key || '' + } + }) + .then(async (response) => await response.json()) + .then((data) => { + console.log(data); + setTimeout(() => { + chats = data; + setTimeout(() => { + scrollToBottom(); + }, 150); // I don't know how to DOM in Svelte, but it takes a second + }, 50); // I don't know how to DOM in Svelte, but it takes a second + }); + } + + function createChatMessage() { + let chatBox = document.querySelector<HTMLElement>('.chatBox'); + if (chatBox) { + let text = chatBox.querySelector('textarea')!.value; + let opts = { + method: 'POST', + headers: { + 'X-AUTH-TOKEN': key || '', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + chat: text, + change_id: data.changeId + }) + }; + if (key) { + fetch( + env.PUBLIC_APP_HOST + 'api/chat_messages/' + data.projectId + '/branch/' + data.branchId, + opts + ) + .then(async (response) => await response.json()) + .then((data) => { + chatBox.querySelector('textarea')!.value = ''; + fetchAndUpdateChat(); + console.log(data); + }); + } + } + } + + function signOff(signoff: boolean) { + let opts = { + method: 'PATCH', + headers: { + 'X-AUTH-TOKEN': key || '', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + sign_off: signoff + }) + }; + if (key) { + fetch(env.PUBLIC_APP_HOST + 'api/patch_stack/' + uuid + '/patch/' + data.changeId, opts) + .then(async (response) => await response.json()) + .then((data) => { + console.log('sign off', data); + getPatchStatus(); + }); + } + } </script> {#if state === 'loading'} @@ -221,58 +330,63 @@ {:else if state === 'unauthorized'} <p>Unauthorized</p> {:else} - <h2>Patch Stack: <a href="../stack">{stack.title}</a></h2> - {#each stack.patches as stackPatch} - <div> - <code - ><a href="/projects/{data.projectId}/branches/{data.branchId}/stack/{stackPatch.change_id}" - >{stackPatch.change_id.substr(0, 8)}</a - ></code - >: - {#if patch.change_id === stackPatch.change_id} - <strong>{stackPatch.title}</strong> - {:else} - {stackPatch.title} - {/if} - </div> - {/each} - <hr /> - - <h2>Patch</h2> <div class="columns"> <div class="column"> - <div>Title: <strong>{patch.title}</strong></div> - {#if patch.description} - <div>Desc: {patch.description}</div> - {/if} - <div>Change Id: <code>{patch.change_id}</code></div> - <div>Commit: <code>{patch.commit_sha}</code></div> - </div> - <div class="column"> - <div>Patch Version: {patch.version}</div> - <div>Stack Position: {patch.position + 1}/{stack.stack_size}</div> - <div>Contributors: {patch.contributors}</div> - <div> - Additions: {patch.statistics.lines - patch.statistics.deletions}, Deletions: {patch - .statistics.deletions}, Files: {patch.statistics.file_count} - </div> - </div> - </div> - - <div class="columns"> - <div class="column outline"> - <h3>Outline</h3> - <div class="sections"> - {#each patch.sections as section} - {#if section.section_type === 'diff'} - <div><a href="#section-{section.id}">{section.new_path}</a></div> + <h3>Patch Series: <a href="../stack">{stack.title}</a></h3> + {#each stack.patches as stackPatch} + <div> + <code + ><a + href="/projects/{data.projectId}/branches/{data.branchId}/stack/{stackPatch.change_id}" + >{stackPatch.change_id.substr(0, 8)}</a + ></code + >: + {#if patch.change_id === stackPatch.change_id} + <strong>{stackPatch.title}</strong> {:else} - <div><a href="#section-{section.id}">{section.title}</a></div> + {stackPatch.title} {/if} - {/each} + </div> + {/each} + <hr /> + + <h3>Patch</h3> + <div class="columns"> + <div class="column"> + <div>Title: <strong>{patch.title}</strong></div> + {#if patch.description} + <div>Desc: {patch.description}</div> + {/if} + <div>Change Id: <code>{patch.change_id.substr(0, 13)}</code></div> + <div>Commit SHA: <code>{patch.commit_sha.substr(0, 10)}</code></div> + <div>Patch Version: {patch.version}</div> + <div>Series Position: {patch.position + 1}/{stack.stack_size}</div> + <div>Contributors: {patch.contributors}</div> + <div>Review:</div> + <div>Viewed: {patch.review.viewed}</div> + <div>Signed Off: {patch.review.signed_off}</div> + <div>Rejected: {patch.review.rejected}</div> + <div> + Additions: {patch.statistics.lines - patch.statistics.deletions}, Deletions: {patch + .statistics.deletions}, Files: {patch.statistics.file_count} + </div> + </div> + <div class="column"> + <h3>Sign off</h3> + {#if status[data.changeId]} + <div>Last View: {status[data.changeId].last_viewed}</div> + <div>Last Review: {status[data.changeId].last_reviewed}</div> + <div>Last Signoff: {status[data.changeId].last_signoff}</div> + {/if} + <div> + <button class="button" on:click={() => signOff(true)}>Sign Off</button> + <button class="button" on:click={() => signOff(false)}>Reject</button> + </div> + </div> </div> - </div> - <div class="column"> + + <hr /> + <div class="patch"> {#each patch.sections as section} <div id="section-{section.id}"> @@ -289,7 +403,7 @@ <div> <strong>{section.new_path}</strong> </div> - <div><pre><code>{section.diff_patch}</code></pre></div> + <div><pre><code class="patch-diff">{section.diff_patch}</code></pre></div> {:else} <div class="right"> <button class="action" on:click={() => addSection(section.position)}>add</button> @@ -316,6 +430,26 @@ </div> </div> </div> + <div class="column chatArea"> + <h3>Chat</h3> + <div class="chatWindow"> + {#each chats as chat} + <div class="chatEntry"> + <div class="chatHeader"> + <div>{chat.user.email}</div> + <div>{chat.created_at}</div> + </div> + <div class="chatComment">{chat.comment}</div> + </div> + {/each} + </div> + <div class="chatBox"> + <div class="input"> + <textarea></textarea> + <button class="action" on:click={() => createChatMessage()}>send</button> + </div> + </div> + </div> </div> {/if} <link @@ -375,4 +509,57 @@ border-radius: 10px; padding: 10px 20px; } + .patch-diff { + font-family: monospace; + font-size: small; + } + h3 { + margin-bottom: 0.5rem; + font-weight: bold; + } + .button { + background-color: #f4f4f4; + border: 1px solid #ccc; + padding: 5px; + } + .chatWindow { + border: 1px solid #ccc; + border-radius: 5px; + padding: 5px; + margin: 5px 0; + max-height: 500px; + height: 500px; + overflow-y: scroll; + } + .chatEntry { + border: 1px solid #ccc; + border-radius: 5px; + background-color: #f4f4f4; + padding: 5px; + margin: 5px 0; + } + .chatHeader { + display: flex; + flex-direction: row; + justify-content: space-between; + font-size: small; + } + .chatComment { + margin-top: 5px; + background-color: #ffffff; + padding: 5px; + } + .chatBox { + border: 1px solid #ccc; + border-radius: 5px; + background-color: #f4f4f4; + padding: 5px; + margin: 5px 0; + } + .chatBox textarea { + width: 100%; + height: 30px; + font-family: monospace; + font-size: large; + } </style> From 7c86840e61800643e2f5afa555616c6a2d31ed60 Mon Sep 17 00:00:00 2001 From: Scott Chacon <schacon@gmail.com> Date: Thu, 3 Oct 2024 11:18:43 +0200 Subject: [PATCH 2/9] patch clicks reload the page --- .../branches/[branchId]/stack/[changeId]/+page.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte b/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte index babca9ba1a..9969491f33 100644 --- a/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte +++ b/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte @@ -337,6 +337,7 @@ <div> <code ><a + rel="external" href="/projects/{data.projectId}/branches/{data.branchId}/stack/{stackPatch.change_id}" >{stackPatch.change_id.substr(0, 8)}</a ></code From 14acbf584384175fa969aeda80ad83dc992364c8 Mon Sep 17 00:00:00 2001 From: Scott Chacon <schacon@gmail.com> Date: Thu, 3 Oct 2024 11:43:51 +0200 Subject: [PATCH 3/9] also set localstorage for now not sure how to get the other thing to work --- apps/web/src/routes/+layout.svelte | 1 + .../branches/[branchId]/stack/[changeId]/+page.svelte | 2 ++ 2 files changed, 3 insertions(+) diff --git a/apps/web/src/routes/+layout.svelte b/apps/web/src/routes/+layout.svelte index 9b33536e51..be50284023 100644 --- a/apps/web/src/routes/+layout.svelte +++ b/apps/web/src/routes/+layout.svelte @@ -21,6 +21,7 @@ const token = $page.url.searchParams.get('gb_access_token'); if (token && token.length > 0) { $page.data.authService.setToken(token); + localStorage.setItem('gb_access_token', token); $page.url.searchParams.delete('gb_access_token'); goto(`?${$page.url.searchParams.toString()}`); diff --git a/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte b/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte index 9969491f33..710bdb54d1 100644 --- a/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte +++ b/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte @@ -16,6 +16,8 @@ onMount(() => { key = localStorage.getItem('gb_access_token'); + console.log(key); + let projectId = data.projectId; let branchId = data.branchId; let changeId = data.changeId; From 200cfba9de50652c221346d5e993ef6c42424e3a Mon Sep 17 00:00:00 2001 From: Scott Chacon <schacon@gmail.com> Date: Thu, 3 Oct 2024 17:50:28 +0200 Subject: [PATCH 4/9] can comment on diff ranges --- apps/web/package.json | 3 +- apps/web/src/lib/components/DiffPatch.svelte | 304 ++++++++++++++++++ .../src/lib/components/DiffPatchArray.svelte | 46 +++ .../branches/[branchId]/stack/+page.svelte | 49 ++- .../[branchId]/stack/[changeId]/+page.svelte | 171 ++++++++-- pnpm-lock.yaml | 87 +++-- 6 files changed, 605 insertions(+), 55 deletions(-) create mode 100644 apps/web/src/lib/components/DiffPatch.svelte create mode 100644 apps/web/src/lib/components/DiffPatchArray.svelte diff --git a/apps/web/package.json b/apps/web/package.json index 958f0d540d..350483e427 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -26,6 +26,7 @@ "@sentry/sveltekit": "^8.9.2", "highlight.js": "^11.10.0", "marked": "^10.0.0", - "moment": "^2.30.1" + "moment": "^2.30.1", + "svelte-gravatar": "^1.0.3" } } diff --git a/apps/web/src/lib/components/DiffPatch.svelte b/apps/web/src/lib/components/DiffPatch.svelte new file mode 100644 index 0000000000..11f08a29b6 --- /dev/null +++ b/apps/web/src/lib/components/DiffPatch.svelte @@ -0,0 +1,304 @@ +<script lang="ts"> + // Define types for the parsed lines and hunk information + type DiffLine = { + type: 'added' | 'removed' | 'context' | 'header' | 'hunk'; + content: string; + lineNumber: number; + leftLineNumber: number | null; // Use null for lines without line numbers + rightLineNumber: number | null; // Use null for lines without line numbers + }; + + // Props for the component (with type annotations) + export let diff: string = ''; + export let diffPath: string = ''; + export let diffSha: string = ''; + + // eslint-disable-next-line func-style + export let onRangeSelect: (range: string, diff_path: string, diff_sha: string) => void = () => {}; + + let selectedRange: { startLine: number | null; endLine: number | null } = { + startLine: null, + endLine: null + }; + + $: selectedRange.startLine === null; // just to trigger reactivity + + // Handle click event on the gutter line number + function handleLineNumberClick(line: DiffLine, event: MouseEvent) { + if (line === null) return; + if (line.type === 'header' || line.type === 'hunk') return; + /* dont highlight text when clicking on line number */ + document.getSelection()?.removeAllRanges(); + + // Check if Shift key is held to extend selection range + if (event.shiftKey && selectedRange.startLine !== null) { + if (line.lineNumber < selectedRange.startLine) { + selectedRange.endLine = selectedRange.startLine; + selectedRange.startLine = line.lineNumber; + } else { + selectedRange.endLine = line.lineNumber; + } + let range = rangeToString(selectedRange); + onRangeSelect(range, diffPath, diffSha); + } else { + // Start a new selection range + if (selectedRange.startLine === line.lineNumber) { + selectedRange = { startLine: null, endLine: null }; + onRangeSelect('', '', ''); + } else { + selectedRange = { startLine: line.lineNumber, endLine: null }; + let range = rangeToString(selectedRange); + onRangeSelect(range, diffPath, diffSha); + } + } + } + + function rangeToString(range: { startLine: number | null; endLine: number | null }): string { + let rangeString = ''; + parsedLines.forEach((line) => { + if (line.lineNumber === range.startLine) { + if (line.leftLineNumber !== null) { + rangeString = `L${line.leftLineNumber}`; + } + if (line.rightLineNumber !== null) { + rangeString = `R${line.rightLineNumber}`; + } else { + rangeString = ''; // selected a header or something + } + } + }); + if (range.endLine !== null) { + parsedLines.forEach((line) => { + if (line.lineNumber === range.endLine) { + if (line.leftLineNumber !== null) { + rangeString += `-L${line.leftLineNumber}`; + } else { + rangeString += `-R${line.rightLineNumber}`; + } + } + }); + } + return rangeString; + } + + // Function to parse the diff string and extract meaningful lines and line numbers + function parseDiff(diff: string): DiffLine[] { + const lines = diff.split('\n'); + const parsedLines: DiffLine[] = []; + let lineNumber: number = 0; + let leftLineNumber: number = 0; + let rightLineNumber: number = 0; + + for (const line of lines) { + lineNumber++; + // Skip the diff header lines + if ( + line.startsWith('diff ') || + line.startsWith('index ') || + line.startsWith('---') || + line.startsWith('+++') + ) { + parsedLines.push({ + type: 'header', + content: line, + lineNumber, + leftLineNumber: null, + rightLineNumber: null + }); + continue; + } + + // If the line starts with '@@', it's a hunk header; extract the starting line number + if (line.startsWith('@@')) { + const match = line.match(/@@ -(\d+)(,\d+)? \+(\d+)(,\d+)? @@/); + if (match) { + console.log(match); + leftLineNumber = parseInt(match[1], 10); + rightLineNumber = parseInt(match[3], 10); + } + parsedLines.push({ + type: 'hunk', + content: line, + lineNumber, + leftLineNumber: null, + rightLineNumber: null + }); // Display hunk header with no line number + continue; + } + + // Determine the type of each line and assign line numbers accordingly + let type: 'added' | 'removed' | 'context' = 'context'; + let showLeftLineNumber: number | null = null; + let showRightLineNumber: number | null = null; + if (line.startsWith('+') && !line.startsWith('+++')) { + type = 'added'; + rightLineNumber++; + showRightLineNumber = rightLineNumber - 1; + } else if (line.startsWith('-') && !line.startsWith('---')) { + type = 'removed'; + leftLineNumber++; + showLeftLineNumber = leftLineNumber - 1; + } else { + type = 'context'; + rightLineNumber++; + leftLineNumber++; + showLeftLineNumber = leftLineNumber - 1; + showRightLineNumber = rightLineNumber - 1; + } + + parsedLines.push({ + type, + content: line, + lineNumber, + leftLineNumber: showLeftLineNumber, + rightLineNumber: showRightLineNumber + }); + } + + return parsedLines; + } + + function inRangeClass(lineNumber: number): string { + if (selectedRange.startLine === null) { + return ''; + } + if (selectedRange.endLine === null) { + if (lineNumber === selectedRange.startLine) { + return 'inRange startRange endRange'; + } + return ''; + } + if (lineNumber >= selectedRange.startLine && lineNumber <= selectedRange.endLine) { + let rangeClasses = 'inRange'; + if (lineNumber === selectedRange.startLine) { + rangeClasses += ' startRange'; + } + if (lineNumber === selectedRange.endLine) { + rangeClasses += ' endRange'; + } + return rangeClasses; + } + return ''; + } + + // Store parsed lines in a reactive variable + let parsedLines: DiffLine[] = parseDiff(diff); +</script> + +<div class="diff-container"> + <!-- Gutter with line numbers --> + <div class="gutter"> + {#each parsedLines as line} + {#if line.type !== 'header'} + <!-- svelte-ignore a11y_click_events_have_key_events --> + <!-- svelte-ignore a11y_no_static_element_interactions --> + <div + class={`gutterEntry ${line.type}`} + on:click={(event) => handleLineNumberClick(line, event)} + > + {line.leftLineNumber !== null ? line.leftLineNumber : ' '} + </div> + {/if} + {/each} + </div> + + <div class="gutter"> + {#each parsedLines as line} + {#if line.type !== 'header'} + <!-- svelte-ignore a11y_click_events_have_key_events --> + <!-- svelte-ignore a11y_no_static_element_interactions --> + <div + class={`gutterEntry ${line.type}`} + on:click={(event) => handleLineNumberClick(line, event)} + > + {line.rightLineNumber !== null ? line.rightLineNumber : ' '} + </div> + {/if} + {/each} + </div> + + <!-- Content of the diff --> + <div class="content"> + {#each parsedLines as line} + {#if line.type !== 'header'} + <div class={`line ${line.type} ${inRangeClass(line.lineNumber)}`}> + ͏{line.content} + </div> + {/if} + {/each} + </div> +</div> + +<style> + .diff-container { + display: flex; + font-family: monospace; + } + + .gutter { + width: 50px; + background-color: #f7f7f7; + padding: 0 10px; + text-align: right; + color: #999; + border-right: 1px solid #ddd; + cursor: pointer; + } + + .content { + width: 100%; + overflow-x: auto; + } + + .line { + display: flex; + white-space: pre; /* Preserve whitespace */ + padding: 4px; + } + + .line.added { + background-color: #e6ffed; + } + + .line.removed { + background-color: #ffeef0; + } + + .line.header { + color: #999; + } + + .line.hunk { + color: #556; + background-color: #cef; + } + + .gutterEntry { + padding: 4px; + } + + .startRange { + border-top: 2px solid #2076e7; + } + + .endRange { + border-bottom: 2px solid #2076e7; + } + + .inRange { + border-left: 2px solid #2076e7; + border-right: 2px solid #2076e7; + background-color: #e4e4e4; + color: #000000; + } + + .inRange.line.added { + background-color: #9be19b; + color: #1e4505; + } + + .inRange.line.removed { + background-color: #f0bcc2; + color: rgb(79, 5, 5); + } +</style> diff --git a/apps/web/src/lib/components/DiffPatchArray.svelte b/apps/web/src/lib/components/DiffPatchArray.svelte new file mode 100644 index 0000000000..94b1f6d291 --- /dev/null +++ b/apps/web/src/lib/components/DiffPatchArray.svelte @@ -0,0 +1,46 @@ +<script lang="ts"> + // Props for the component (with type annotations) + export let diffArray: string[] = []; + console.log(diffArray); +</script> + +<div class="diff"> + <div class="gutter"> + {#each diffArray as line} + <div class="line">{line.right}</div> + {/each} + </div> + <div class="lines"> + {#each diffArray as line} + <div class="line {line.type}">{line.line}</div> + {/each} + </div> +</div> + +<style> + .diff { + display: flex; + font-family: monospace; + white-space: pre; + background-color: #fff; + padding: 8px; + border: 1px solid #ccc; + border-radius: 10px; + width: 100%; + } + .gutter { + padding: 0 8px; + } + .lines { + width: 100%; + } + .line { + padding: 2px; + } + .diff .added { + background-color: #dfd; + } + .diff .removed { + background-color: #fdd; + } +</style> diff --git a/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/+page.svelte b/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/+page.svelte index 2a070cfb8b..ed4a536730 100644 --- a/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/+page.svelte +++ b/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/+page.svelte @@ -1,7 +1,9 @@ <script lang="ts"> import moment from 'moment'; import { onMount } from 'svelte'; + import Gravatar from 'svelte-gravatar'; import { env } from '$env/dynamic/public'; + // load moment let state = 'loading'; let stackData: any = {}; @@ -92,9 +94,32 @@ .statistics.deletions}, Files: {patch.statistics.file_count} </div> <hr /> - <div>Viewed: {patch.review.viewed}</div> - <div>Signed Off:{patch.review.signed_off}</div> - <div>Rejected: {patch.review.rejected}</div> + <div> + Viewed: + {#each patch.review.viewed as email} + <Gravatar {email} size={20} /> + {/each} + </div> + <div> + Signed Off: + {#each patch.review.signed_off as email} + <Gravatar {email} size={20} /> + {/each} + </div> + <div> + Rejected: + {#each patch.review.rejected as email} + <Gravatar {email} size={20} /> + {/each} + </div> + <hr /> + {#if patch.review.rejected.length > 0} + <div class="rejected">X</div> + {:else if patch.review.signed_off.length > 0} + <div class="signoff">✓</div> + {:else} + <div class="unreviewed">?</div> + {/if} </div> </div> {/each} @@ -123,4 +148,22 @@ margin: 10px 0; border-radius: 10px; } + .rejected { + background-color: rgb(224, 92, 92); + color: white; + padding: 5px; + border-radius: 5px; + } + .signoff { + background-color: rgb(77, 219, 77); + color: white; + padding: 5px; + border-radius: 5px; + } + .unreviewed { + background-color: rgb(204, 204, 69); + color: black; + padding: 5px; + border-radius: 5px; + } </style> diff --git a/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte b/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte index 710bdb54d1..1d1928c5cf 100644 --- a/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte +++ b/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte @@ -1,7 +1,9 @@ <script lang="ts"> - import hljs from 'highlight.js'; + import DiffPatch from '$lib/components/DiffPatch.svelte'; + import DiffPatchArray from '$lib/components/DiffPatchArray.svelte'; import { marked } from 'marked'; import { onMount } from 'svelte'; + import Gravatar from 'svelte-gravatar'; import { env } from '$env/dynamic/public'; let state = 'loading'; @@ -16,7 +18,6 @@ onMount(() => { key = localStorage.getItem('gb_access_token'); - console.log(key); let projectId = data.projectId; let branchId = data.branchId; @@ -66,8 +67,6 @@ state = 'loaded'; // wait a second setTimeout(() => { - console.log('Highlighting code'); - hljs.highlightAll(); // render markdowns let markdowns = document.querySelectorAll('.markdown'); markdowns.forEach((markdown) => { @@ -279,16 +278,29 @@ let chatBox = document.querySelector<HTMLElement>('.chatBox'); if (chatBox) { let text = chatBox.querySelector('textarea')!.value; + let params: { + chat: string; + change_id: any; + range?: string; + diff_path?: string; + diff_sha?: string; + } = { + chat: text, + change_id: data.changeId + }; + if (commentRange.length > 0) { + params.range = commentRange; + params.diff_path = diffPath; + params.diff_sha = diffSha; + } + console.log('FUCKING POST', params); let opts = { method: 'POST', headers: { 'X-AUTH-TOKEN': key || '', 'Content-Type': 'application/json' }, - body: JSON.stringify({ - chat: text, - change_id: data.changeId - }) + body: JSON.stringify(params) }; if (key) { fetch( @@ -325,6 +337,16 @@ }); } } + + let commentRange: string = ''; + let diffPath: string = ''; + let diffSha: string = ''; + + function rangeSelect(range: string, diff_path: string, diff_sha: string) { + commentRange = range; + diffPath = diff_path; + diffSha = diff_sha; + } </script> {#if state === 'loading'} @@ -333,7 +355,7 @@ <p>Unauthorized</p> {:else} <div class="columns"> - <div class="column"> + <div class="column patchArea"> <h3>Patch Series: <a href="../stack">{stack.title}</a></h3> {#each stack.patches as stackPatch} <div> @@ -364,26 +386,63 @@ <div>Commit SHA: <code>{patch.commit_sha.substr(0, 10)}</code></div> <div>Patch Version: {patch.version}</div> <div>Series Position: {patch.position + 1}/{stack.stack_size}</div> - <div>Contributors: {patch.contributors}</div> - <div>Review:</div> - <div>Viewed: {patch.review.viewed}</div> - <div>Signed Off: {patch.review.signed_off}</div> - <div>Rejected: {patch.review.rejected}</div> + <div> + Contributors: + {#each patch.contributors as email} + <Gravatar {email} size={20} /> + {/each} + </div> <div> Additions: {patch.statistics.lines - patch.statistics.deletions}, Deletions: {patch .statistics.deletions}, Files: {patch.statistics.file_count} </div> </div> <div class="column"> - <h3>Sign off</h3> + {#if patch.review.viewed.length > 0} + <div> + <div class="title">Viewed:</div> + {#each patch.review.viewed as email} + <Gravatar {email} size={20} /> + {/each} + </div> + {/if} + + {#if patch.review.signed_off.length > 0} + <div> + <div class="title">Signed Off:</div> + {#each patch.review.signed_off as email} + <Gravatar {email} size={20} /> + {/each} + </div> + {/if} + + {#if patch.review.length > 0} + <div> + <div class="title">Rejected:</div> + {#each patch.review.rejected as email} + <Gravatar {email} size={20} /> + {/each} + </div> + {/if} + + <h3>Your sign off status</h3> {#if status[data.changeId]} - <div>Last View: {status[data.changeId].last_viewed}</div> - <div>Last Review: {status[data.changeId].last_reviewed}</div> - <div>Last Signoff: {status[data.changeId].last_signoff}</div> + {#if status[data.changeId].last_signoff} + <div class="signoff">You signed off on this patch</div> + {/if} + {#if !status[data.changeId].last_reviewed} + <div class="rejected">You have not reviewed this patch</div> + {:else if !status[data.changeId].last_signoff} + <div class="rejected">You have rejected this patch</div> + {/if} {/if} <div> - <button class="button" on:click={() => signOff(true)}>Sign Off</button> - <button class="button" on:click={() => signOff(false)}>Reject</button> + {#if !status[data.changeId].last_signoff} + <button class="button" on:click={() => signOff(true)}>Sign Off</button> + {/if} + {#if status[data.changeId].last_signoff || !status[data.changeId].last_reviewed} + <button class="button" on:click={() => signOff(false)}>Reject</button> + {/if} </div> </div> </div> @@ -403,10 +462,18 @@ >down</button >] </div> + <div class="filePath"> + {section.new_path} + </div> <div> - <strong>{section.new_path}</strong> + <DiffPatch + onRangeSelect={(range, diff_path, diff_sha) => + rangeSelect(range, diff_path, diff_sha)} + diffPath={section.new_path} + diffSha={section.diff_sha} + diff={section.diff_patch} + /> </div> - <div><pre><code class="patch-diff">{section.diff_patch}</code></pre></div> {:else} <div class="right"> <button class="action" on:click={() => addSection(section.position)}>add</button> @@ -439,9 +506,18 @@ {#each chats as chat} <div class="chatEntry"> <div class="chatHeader"> - <div>{chat.user.email}</div> + <div class="avatar"> + <Gravatar email={chat.user.email} size={20} /> + </div> <div>{chat.created_at}</div> </div> + {#if chat.diff_patch_array} + <div> + <div class="diffPath">{chat.diff_path}</div> + <!-- {chat.diff_sha} --> + <DiffPatchArray diffArray={chat.diff_patch_array} /> + </div> + {/if} <div class="chatComment">{chat.comment}</div> </div> {/each} @@ -455,12 +531,12 @@ </div> </div> {/if} -<link - rel="stylesheet" - href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/default.min.css" -/> <style> + .patchArea { + max-width: 800px; + overflow-x: scroll; + } hr { margin: 1rem 0; } @@ -525,13 +601,17 @@ border: 1px solid #ccc; padding: 5px; } + .chatArea { + width: 600px; + min-width: 600px; + } .chatWindow { border: 1px solid #ccc; border-radius: 5px; padding: 5px; margin: 5px 0; - max-height: 500px; - height: 500px; + max-height: 600px; + height: 600px; overflow-y: scroll; } .chatEntry { @@ -565,4 +645,37 @@ font-family: monospace; font-size: large; } + .avatar { + border-radius: 50%; + overflow: hidden; + } + + .filePath { + font-family: monospace; + font-weight: bold; + font-size: 1.4em; + margin-bottom: 10px; + } + .signoff { + background-color: #e6ffed; + padding: 5px; + border-radius: 5px; + } + .rejected { + background-color: #ffeef0; + padding: 5px; + border-radius: 5px; + } + .title { + min-width: 100px; + display: inline-block; + border-right: 1px solid #eee; + } + .diffPath { + font-family: monospace; + font-weight: bold; + font-size: 1.1em; + margin-top: 10px; + margin-bottom: 6px; + } </style> diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f1ed1846ac..eb7641bcd3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,28 +4,6 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -catalogs: - default: - vite: - specifier: 5.2.13 - version: 5.2.13 - svelte: - '@sveltejs/adapter-static': - specifier: 3.0.4 - version: 3.0.4 - '@sveltejs/kit': - specifier: 2.5.25 - version: 2.5.25 - '@sveltejs/vite-plugin-svelte': - specifier: 4.0.0-next.6 - version: 4.0.0-next.6 - svelte: - specifier: 5.0.0-next.243 - version: 5.0.0-next.243 - svelte-check: - specifier: 4.0.1 - version: 4.0.1 - importers: .: @@ -307,6 +285,9 @@ importers: moment: specifier: ^2.30.1 version: 2.30.1 + svelte-gravatar: + specifier: ^1.0.3 + version: 1.0.3(svelte@5.0.0-next.243) devDependencies: '@fontsource/fira-mono': specifier: ^4.5.10 @@ -2525,6 +2506,9 @@ packages: chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + charenc@0.0.2: + resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + check-error@2.1.1: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} @@ -2666,6 +2650,9 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} + crypt@0.0.2: + resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} + css-shorthand-properties@1.1.1: resolution: {integrity: sha512-Md+Juc7M3uOdbAFwOYlTrccIZ7oCFuzrhKYQjdeUEW/sE1hv17Jp/Bws+ReOPpGVBTYCBoYo+G17V5Qo8QQ75A==} @@ -3752,6 +3739,9 @@ packages: resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} engines: {node: '>= 0.4'} + is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} @@ -4164,6 +4154,9 @@ packages: engines: {node: '>= 18'} hasBin: true + md5@2.3.0: + resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} + media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -5312,6 +5305,11 @@ packages: peerDependencies: svelte: ^3.57.0 || ^4.0.0 + svelte-gravatar@1.0.3: + resolution: {integrity: sha512-CNxIV2lAuiqwdaPrGAM/BFj5U1dNNQXzeyh+HVi/48BODFXoDy0L1CMqYyvM+aKiF4ideZUBwT0S9/C1BeL5oA==} + peerDependencies: + svelte: '*' + svelte-preprocess@5.1.3: resolution: {integrity: sha512-xxAkmxGHT+J/GourS5mVJeOXZzne1FR5ljeOUAMXUkfEhkLEllRreXpbl3dIYJlcJRfL1LO1uIAPpBpBfiqGPw==} engines: {node: '>= 16.0.0', pnpm: ^8.0.0} @@ -5349,6 +5347,9 @@ packages: typescript: optional: true + svelte-waypoint@0.1.4: + resolution: {integrity: sha512-UEqoXZjJeKj2sWlAIsBOFjxjMn+KP8aFCc/zjdmZi1cCOE59z6T2C+I6ZaAf8EmNQqNzfZVB/Lci4Ci9spzXAw==} + svelte-writable-derived@3.1.0: resolution: {integrity: sha512-cTvaVFNIJ036vSDIyPxJYivKC7ZLtcFOPm1Iq6qWBDo1fOHzfk6ZSbwaKrxhjgy52Rbl5IHzRcWgos6Zqn9/rg==} peerDependencies: @@ -5975,6 +5976,28 @@ packages: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} engines: {node: '>= 14'} +catalogs: + default: + vite: + specifier: 5.2.13 + version: 5.2.13 + svelte: + '@sveltejs/adapter-static': + specifier: 3.0.4 + version: 3.0.4 + '@sveltejs/kit': + specifier: 2.5.25 + version: 2.5.25 + '@sveltejs/vite-plugin-svelte': + specifier: 4.0.0-next.6 + version: 4.0.0-next.6 + svelte: + specifier: 5.0.0-next.243 + version: 5.0.0-next.243 + svelte-check: + specifier: 4.0.1 + version: 4.0.1 + snapshots: '@aashutoshrathi/word-wrap@1.2.6': {} @@ -8639,6 +8662,8 @@ snapshots: chardet@0.7.0: {} + charenc@0.0.2: {} + check-error@2.1.1: {} chokidar@3.6.0: @@ -8782,6 +8807,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crypt@0.0.2: {} + css-shorthand-properties@1.1.1: {} css-value@0.0.1: {} @@ -10152,6 +10179,8 @@ snapshots: call-bind: 1.0.7 has-tostringtag: 1.0.2 + is-buffer@1.1.6: {} + is-callable@1.2.7: {} is-core-module@2.13.1: @@ -10531,6 +10560,12 @@ snapshots: marked@10.0.0: {} + md5@2.3.0: + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + is-buffer: 1.1.6 + media-typer@0.3.0: {} memoizerific@1.11.3: @@ -11738,6 +11773,12 @@ snapshots: svelte: 5.0.0-next.243 svelte-writable-derived: 3.1.0(svelte@5.0.0-next.243) + svelte-gravatar@1.0.3(svelte@5.0.0-next.243): + dependencies: + md5: 2.3.0 + svelte: 5.0.0-next.243 + svelte-waypoint: 0.1.4 + svelte-preprocess@5.1.3(@babel/core@7.24.7)(postcss-load-config@5.1.0(postcss@8.4.39))(postcss@8.4.39)(svelte@5.0.0-next.243)(typescript@5.4.5): dependencies: '@types/pug': 2.0.6 @@ -11752,6 +11793,8 @@ snapshots: postcss-load-config: 5.1.0(postcss@8.4.39) typescript: 5.4.5 + svelte-waypoint@0.1.4: {} + svelte-writable-derived@3.1.0(svelte@5.0.0-next.243): dependencies: svelte: 5.0.0-next.243 From bdcdf028a009febf1873f0003ed49de406496d65 Mon Sep 17 00:00:00 2001 From: Scott Chacon <schacon@gmail.com> Date: Fri, 4 Oct 2024 10:03:20 +0200 Subject: [PATCH 5/9] add ability to create issue comments and resovle --- .../[branchId]/stack/[changeId]/+page.svelte | 88 +++++++++++++++++-- 1 file changed, 82 insertions(+), 6 deletions(-) diff --git a/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte b/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte index 1d1928c5cf..3049692ce9 100644 --- a/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte +++ b/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte @@ -277,6 +277,13 @@ function createChatMessage() { let chatBox = document.querySelector<HTMLElement>('.chatBox'); if (chatBox) { + // check if this is an issue + var checkbox = document.getElementById('issue'); + let is_issue = false; + if ((checkbox as HTMLInputElement).checked) { + is_issue = true; + } + let text = chatBox.querySelector('textarea')!.value; let params: { chat: string; @@ -284,16 +291,17 @@ range?: string; diff_path?: string; diff_sha?: string; + issue?: boolean; } = { chat: text, - change_id: data.changeId + change_id: data.changeId, + issue: is_issue }; if (commentRange.length > 0) { params.range = commentRange; params.diff_path = diffPath; params.diff_sha = diffSha; } - console.log('FUCKING POST', params); let opts = { method: 'POST', headers: { @@ -338,11 +346,35 @@ } } + function resolveIssue(uuid: string) { + //:project_id/chat/:chat_uuid + console.log('Resolving issue', uuid); + let opts = { + method: 'PATCH', + headers: { + 'X-AUTH-TOKEN': key || '', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + resolved: true + }) + }; + if (key) { + fetch(env.PUBLIC_APP_HOST + 'api/chat_messages/' + data.projectId + '/chat/' + uuid, opts) + .then(async (response) => await response.json()) + .then((data) => { + console.log('resolved', data); + fetchAndUpdateChat(); + }); + } + } + let commentRange: string = ''; let diffPath: string = ''; let diffSha: string = ''; function rangeSelect(range: string, diff_path: string, diff_sha: string) { + console.log('range selected', range, diff_path, diff_sha); commentRange = range; diffPath = diff_path; diffSha = diff_sha; @@ -504,7 +536,7 @@ <h3>Chat</h3> <div class="chatWindow"> {#each chats as chat} - <div class="chatEntry"> + <div class="chatEntry {chat.issue ? 'issue' : ''} {chat.resolved ? 'resolved' : ''}"> <div class="chatHeader"> <div class="avatar"> <Gravatar email={chat.user.email} size={20} /> @@ -519,13 +551,28 @@ </div> {/if} <div class="chatComment">{chat.comment}</div> + {#if chat.issue} + {#if chat.resolved} + <div class="right">resolved</div> + {:else} + <button class="action" on:click={() => resolveIssue(chat.uuid)}>resolve</button> + {/if} + {/if} </div> {/each} </div> <div class="chatBox"> <div class="input"> - <textarea></textarea> - <button class="action" on:click={() => createChatMessage()}>send</button> + <div class="messageBox"> + <textarea></textarea> + </div> + <div class="chatOptions"> + <div class="issueOptions"> + <input type="checkbox" id="issue" name="issue" value="issue" /> + <div>issue</div> + </div> + <button class="actionChat" on:click={() => createChatMessage()}>send</button> + </div> </div> </div> </div> @@ -533,6 +580,27 @@ {/if} <style> + .actionChat { + cursor: pointer; + color: #ffffff; + padding: 5px 10px; + background-color: var(--clr-theme-pop-element); + border-radius: 5px; + } + .issueOptions { + display: flex; + flex-direction: row; + gap: 5px; + color: #626262; + /* justify vertically */ + align-items: center; + } + .chatOptions { + padding: 4px; + display: flex; + flex-direction: row; + justify-content: space-between; + } .patchArea { max-width: 800px; overflow-x: scroll; @@ -621,6 +689,12 @@ padding: 5px; margin: 5px 0; } + .chatEntry.issue { + background-color: #ffeef0; + } + .chatEntry.issue.resolved { + background-color: #eeeeee; + } .chatHeader { display: flex; flex-direction: row; @@ -641,9 +715,11 @@ } .chatBox textarea { width: 100%; - height: 30px; + height: 40px; + border-radius: 5px; font-family: monospace; font-size: large; + padding: 5px; } .avatar { border-radius: 50%; From 8b2aedfdaa804881c5608c08f5b1f949f5b337aa Mon Sep 17 00:00:00 2001 From: Scott Chacon <schacon@gmail.com> Date: Fri, 4 Oct 2024 10:03:36 +0200 Subject: [PATCH 6/9] update to patch series styles --- .../branches/[branchId]/stack/+page.svelte | 103 +++++++++--------- 1 file changed, 53 insertions(+), 50 deletions(-) diff --git a/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/+page.svelte b/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/+page.svelte index ed4a536730..54fe740cd7 100644 --- a/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/+page.svelte +++ b/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/+page.svelte @@ -75,51 +75,54 @@ <h2>Patches</h2> {#each stackData.patches as patch} - <div class="columns patch"> - <div class="column"> - <div>Title: <strong>{patch.title}</strong></div> - <div>Change Id: <code><a href="./stack/{patch.change_id}">{patch.change_id}</a></code></div> - <div>Commit: <code>{patch.commit_sha}</code></div> - <div>Version: {patch.version}</div> - <div><strong>Files:</strong></div> - {#each patch.statistics.files as file} - <div><code>{file}</code></div> - {/each} - </div> - <div class="column"> - <div>Created: <span class="dtime">{patch.created_at}</span></div> - <div>Contributors: {patch.contributors}</div> - <div> - Additions: {patch.statistics.lines - patch.statistics.deletions}, Deletions: {patch - .statistics.deletions}, Files: {patch.statistics.file_count} - </div> - <hr /> - <div> - Viewed: - {#each patch.review.viewed as email} - <Gravatar {email} size={20} /> - {/each} - </div> - <div> - Signed Off: - {#each patch.review.signed_off as email} - <Gravatar {email} size={20} /> + <div class="patch"> + {#if patch.review.rejected.length > 0} + <div class="patchHeader rejected">X</div> + {:else if patch.review.signed_off.length > 0} + <div class="patchHeader signoff">✓</div> + {:else} + <div class="patchHeader unreviewed">?</div> + {/if} + <div class="columns patchData"> + <div class="column"> + <div>Title: <strong>{patch.title}</strong></div> + <div> + Change Id: <code><a href="./stack/{patch.change_id}">{patch.change_id}</a></code> + </div> + <div>Commit: <code>{patch.commit_sha}</code></div> + <div>Version: {patch.version}</div> + <div><strong>Files:</strong></div> + {#each patch.statistics.files as file} + <div><code>{file}</code></div> {/each} </div> - <div> - Rejected: - {#each patch.review.rejected as email} - <Gravatar {email} size={20} /> - {/each} + <div class="column"> + <div>Created: <span class="dtime">{patch.created_at}</span></div> + <div>Contributors: {patch.contributors}</div> + <div> + Additions: {patch.statistics.lines - patch.statistics.deletions}, Deletions: {patch + .statistics.deletions}, Files: {patch.statistics.file_count} + </div> + <hr /> + <div> + Viewed: + {#each patch.review.viewed as email} + <Gravatar {email} size={20} /> + {/each} + </div> + <div> + Signed Off: + {#each patch.review.signed_off as email} + <Gravatar {email} size={20} /> + {/each} + </div> + <div> + Rejected: + {#each patch.review.rejected as email} + <Gravatar {email} size={20} /> + {/each} + </div> </div> - <hr /> - {#if patch.review.rejected.length > 0} - <div class="rejected">X</div> - {:else if patch.review.signed_off.length > 0} - <div class="signoff">✓</div> - {:else} - <div class="unreviewed">?</div> - {/if} </div> </div> {/each} @@ -144,26 +147,26 @@ .patch { background-color: var(--clr-bg-1-muted); border: 1px solid #ccc; - padding: 15px 20px; margin: 10px 0; border-radius: 10px; } + .patchData { + padding: 15px 20px; + } + .patchHeader { + padding: 5px; + border-radius: 5px 5px 0 0; + } .rejected { background-color: rgb(224, 92, 92); color: white; - padding: 5px; - border-radius: 5px; } .signoff { background-color: rgb(77, 219, 77); color: white; - padding: 5px; - border-radius: 5px; } .unreviewed { - background-color: rgb(204, 204, 69); + background-color: rgb(215, 215, 144); color: black; - padding: 5px; - border-radius: 5px; } </style> From 1aaf2029659170fb2538a393813a81de6abd9746 Mon Sep 17 00:00:00 2001 From: Scott Chacon <schacon@gmail.com> Date: Fri, 4 Oct 2024 10:49:06 +0200 Subject: [PATCH 7/9] new review_all data --- .../branches/[branchId]/stack/+page.svelte | 73 ++++++++++++++----- 1 file changed, 54 insertions(+), 19 deletions(-) diff --git a/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/+page.svelte b/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/+page.svelte index 54fe740cd7..d3fae94529 100644 --- a/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/+page.svelte +++ b/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/+page.svelte @@ -76,9 +76,9 @@ <h2>Patches</h2> {#each stackData.patches as patch} <div class="patch"> - {#if patch.review.rejected.length > 0} + {#if patch.review_all.rejected.length > 0} <div class="patchHeader rejected">X</div> - {:else if patch.review.signed_off.length > 0} + {:else if patch.review_all.signed_off.length > 0} <div class="patchHeader signoff">✓</div> {:else} <div class="patchHeader unreviewed">?</div> @@ -104,23 +104,49 @@ .statistics.deletions}, Files: {patch.statistics.file_count} </div> <hr /> - <div> - Viewed: - {#each patch.review.viewed as email} - <Gravatar {email} size={20} /> - {/each} - </div> - <div> - Signed Off: - {#each patch.review.signed_off as email} - <Gravatar {email} size={20} /> - {/each} - </div> - <div> - Rejected: - {#each patch.review.rejected as email} - <Gravatar {email} size={20} /> - {/each} + <div class="columns"> + <div class="column"> + <h3>This Version</h3> + <div> + <div class="title">Viewed:</div> + {#each patch.review.viewed as email} + <Gravatar {email} size={20} /> + {/each} + </div> + <div> + <div class="title">Signed Off:</div> + {#each patch.review.signed_off as email} + <Gravatar {email} size={20} /> + {/each} + </div> + <div> + <div class="title">Rejected:</div> + {#each patch.review.rejected as email} + <Gravatar {email} size={20} /> + {/each} + </div> + </div> + <div class="column"> + <h3>All Versions</h3> + <div> + <div class="title">Viewed:</div> + {#each patch.review_all.viewed as email} + <Gravatar {email} size={20} /> + {/each} + </div> + <div> + <div class="title">Signed Off:</div> + {#each patch.review_all.signed_off as email} + <Gravatar {email} size={20} /> + {/each} + </div> + <div> + <div class="title">Rejected:</div> + {#each patch.review_all.rejected as email} + <Gravatar {email} size={20} /> + {/each} + </div> + </div> </div> </div> </div> @@ -135,6 +161,10 @@ h2 { font-size: 1.5rem; } + h3 { + font-size: 1.1rem; + font-weight: bold; + } .columns { display: flex; } @@ -169,4 +199,9 @@ background-color: rgb(215, 215, 144); color: black; } + .title { + min-width: 100px; + display: inline-block; + border-right: 1px solid #eee; + } </style> From be491c6cb9e8c3f2963c4d8ab851286a11eccfca Mon Sep 17 00:00:00 2001 From: Scott Chacon <schacon@gmail.com> Date: Fri, 4 Oct 2024 14:02:09 +0200 Subject: [PATCH 8/9] move chat rendering to patch_events --- .../[branchId]/stack/[changeId]/+page.svelte | 146 ++++++++++++++---- 1 file changed, 119 insertions(+), 27 deletions(-) diff --git a/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte b/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte index 3049692ce9..8a7d2b3f78 100644 --- a/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte +++ b/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte @@ -10,7 +10,7 @@ let patch: any = {}; let stack: any = {}; let status: any = {}; - let chats: any = []; + let events: any = []; let key: any = ''; let uuid: any = ''; @@ -256,7 +256,7 @@ } function fetchAndUpdateChat() { - fetch(env.PUBLIC_APP_HOST + 'api/chat_messages/' + data.projectId + '/chats/' + data.changeId, { + fetch(env.PUBLIC_APP_HOST + 'api/patch_events/' + data.projectId + '/patch/' + data.changeId, { method: 'GET', headers: { 'X-AUTH-TOKEN': key || '' @@ -266,7 +266,7 @@ .then((data) => { console.log(data); setTimeout(() => { - chats = data; + events = data; setTimeout(() => { scrollToBottom(); }, 150); // I don't know how to DOM in Svelte, but it takes a second @@ -342,6 +342,7 @@ .then((data) => { console.log('sign off', data); getPatchStatus(); + fetchAndUpdateChat(); }); } } @@ -469,11 +470,13 @@ {/if} {/if} <div> - {#if !status[data.changeId].last_signoff} - <button class="button" on:click={() => signOff(true)}>Sign Off</button> - {/if} - {#if status[data.changeId].last_signoff || !status[data.changeId].last_reviewed} - <button class="button" on:click={() => signOff(false)}>Reject</button> + {#if status[data.changeId]} + {#if !status[data.changeId].last_signoff} + <button class="button" on:click={() => signOff(true)}>Sign Off</button> + {/if} + {#if status[data.changeId].last_signoff || !status[data.changeId].last_reviewed} + <button class="button" on:click={() => signOff(false)}>Reject</button> + {/if} {/if} </div> </div> @@ -535,30 +538,85 @@ <div class="column chatArea"> <h3>Chat</h3> <div class="chatWindow"> - {#each chats as chat} - <div class="chatEntry {chat.issue ? 'issue' : ''} {chat.resolved ? 'resolved' : ''}"> - <div class="chatHeader"> - <div class="avatar"> - <Gravatar email={chat.user.email} size={20} /> + {#each events as event} + {#if event.event_type === 'chat'} + <div + class="chatEntry {event.object.issue ? 'issue' : ''} {event.object.resolved + ? 'resolved' + : ''}" + > + <div class="chatHeader"> + <div class="avatar"> + <Gravatar email={event.object.user.email} size={20} /> + </div> + <div>{event.object.created_at}</div> </div> - <div>{chat.created_at}</div> + {#if event.object.diff_patch_array} + <div> + <div class="diffPath">{event.object.diff_path}</div> + <!-- {chat.diff_sha} --> + <DiffPatchArray diffArray={event.object.diff_patch_array} /> + </div> + {/if} + <div class="chatComment">{event.object.comment}</div> + {#if event.object.issue} + {#if event.object.resolved} + <div class="right">resolved</div> + {:else} + <button class="action" on:click={() => resolveIssue(event.object.uuid)} + >resolve</button + > + {/if} + {/if} </div> - {#if chat.diff_patch_array} - <div> - <div class="diffPath">{chat.diff_path}</div> - <!-- {chat.diff_sha} --> - <DiffPatchArray diffArray={chat.diff_patch_array} /> + {/if} + {#if event.event_type === 'issue_status'} + {#if event.data.resolution} + <div class="issueEvent event"> + <div class="eventDetail"> + {event.user.email} resolved issue {event.object.uuid.substr(0, 8)} + </div> + <div class="eventDate"> + {event.created_at} + </div> </div> {/if} - <div class="chatComment">{chat.comment}</div> - {#if chat.issue} - {#if chat.resolved} - <div class="right">resolved</div> - {:else} - <button class="action" on:click={() => resolveIssue(chat.uuid)}>resolve</button> - {/if} + {/if} + + {#if event.event_type === 'patch_status'} + {#if event.data.status} + <div class="signoffEvent event"> + <div class="eventDetail"> + {event.user.email} + signed off + </div> + <div class="eventDate"> + {event.created_at} + </div> + </div> + {:else} + <div class="rejectEvent event"> + <div class="eventDetail"> + {event.user.email} + requested changes + </div> + <div class="eventDate"> + {event.created_at} + </div> + </div> {/if} - </div> + {/if} + + {#if event.event_type === 'patch_version'} + <div class="versionEvent event"> + <div class="eventDetail"> + new patch version: v{event.object.version} + </div> + <div class="eventDate"> + {event.created_at} + </div> + </div> + {/if} {/each} </div> <div class="chatBox"> @@ -580,6 +638,40 @@ {/if} <style> + .event { + display: flex; + flex-direction: row; + justify-content: space-between; + margin-bottom: 0.5em; + } + .eventDate { + color: #888; + font-size: small; + } + + .issueEvent { + background-color: #dbecff; + padding: 5px; + border-radius: 5px; + } + .signoffEvent { + background-color: #e6ffed; + padding: 5px; + border-radius: 5px; + } + .rejectEvent { + background-color: #ffeef0; + padding: 5px; + border-radius: 5px; + } + .versionEvent { + padding: 5px; + border-bottom: 1px solid rgb(96, 43, 43); + } + .versionEvent .eventDetail { + color: #844; + } + .actionChat { cursor: pointer; color: #ffffff; From b244955bca24dc38eb867cf788bb42de1632e25d Mon Sep 17 00:00:00 2001 From: Scott Chacon <schacon@gmail.com> Date: Thu, 10 Oct 2024 11:10:52 +0200 Subject: [PATCH 9/9] add actioncable listening for real time events --- apps/web/package.json | 1 + .../[branchId]/stack/[changeId]/+page.svelte | 19 +++++++++++++++++++ pnpm-lock.yaml | 8 ++++++++ 3 files changed, 28 insertions(+) diff --git a/apps/web/package.json b/apps/web/package.json index 350483e427..bed8262eb9 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -23,6 +23,7 @@ "vite": "catalog:" }, "dependencies": { + "@rails/actioncable": "^7.2.100", "@sentry/sveltekit": "^8.9.2", "highlight.js": "^11.10.0", "marked": "^10.0.0", diff --git a/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte b/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte index 8a7d2b3f78..70f20be350 100644 --- a/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte +++ b/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte @@ -1,6 +1,7 @@ <script lang="ts"> import DiffPatch from '$lib/components/DiffPatch.svelte'; import DiffPatchArray from '$lib/components/DiffPatchArray.svelte'; + import { createConsumer } from '@rails/actioncable'; import { marked } from 'marked'; import { onMount } from 'svelte'; import Gravatar from 'svelte-gravatar'; @@ -13,12 +14,15 @@ let events: any = []; let key: any = ''; let uuid: any = ''; + let consumer; export let data: any; onMount(() => { key = localStorage.getItem('gb_access_token'); + listenToChat(key); + let projectId = data.projectId; let branchId = data.branchId; let changeId = data.changeId; @@ -46,6 +50,21 @@ } }); + function listenToChat(token: string) { + // connect to actioncable to subscribe to chat events + let wsHost = env.PUBLIC_APP_HOST.replace('http', 'ws') + 'cable'; + consumer = createConsumer(wsHost + '?token=' + token); + consumer.subscriptions.create( + { channel: 'ChatChannel', change_id: data.changeId, project_id: data.projectId }, + { + received(data: any) { + // todo: update chat window with new message + console.log('received', data); + } + } + ); + } + function scrollToBottom() { let chatWindow = document.querySelector<HTMLElement>('.chatWindow'); if (chatWindow) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eb7641bcd3..d43ed08a55 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -273,6 +273,9 @@ importers: apps/web: dependencies: + '@rails/actioncable': + specifier: ^7.2.100 + version: 7.2.100 '@sentry/sveltekit': specifier: ^8.9.2 version: 8.9.2(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.52.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.25.0)(@sveltejs/kit@2.5.25(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@5.0.0-next.243)(vite@5.2.13(@types/node@22.3.0)))(svelte@5.0.0-next.243)(vite@5.2.13(@types/node@22.3.0)))(svelte@5.0.0-next.243) @@ -1200,6 +1203,9 @@ packages: engines: {node: '>=16.3.0'} hasBin: true + '@rails/actioncable@7.2.100': + resolution: {integrity: sha512-7xtIENf0Yw59AFDM3+xqxPCZxev3QVAqjPmUzmgsB9eL8S/zTpB0IU9srNc7XknzJI4e09XKNnCaJRx3gfYzXA==} + '@replit/codemirror-lang-svelte@6.0.0': resolution: {integrity: sha512-U2OqqgMM6jKelL0GNWbAmqlu1S078zZNoBqlJBW+retTc5M4Mha6/Y2cf4SVg6ddgloJvmcSpt4hHrVoM4ePRA==} peerDependencies: @@ -6982,6 +6988,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@rails/actioncable@7.2.100': {} + '@replit/codemirror-lang-svelte@6.0.0(@codemirror/autocomplete@6.16.2(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.2.1))(@codemirror/lang-css@6.2.1(@codemirror/view@6.26.3))(@codemirror/lang-html@6.4.9)(@codemirror/lang-javascript@6.2.2)(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.2.1)(@lezer/highlight@1.2.0)(@lezer/javascript@1.4.16)(@lezer/lr@1.4.1)': dependencies: '@codemirror/autocomplete': 6.16.2(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.2.1)