Skip to content

Commit

Permalink
feat(patch-detail): support creating general revision comments
Browse files Browse the repository at this point in the history
Some edge cases not handled yet

Also check CHANGELOG.md diff for more coming with this commit

Signed-off-by: Konstantinos Maninakis <maninak@protonmail.com>
  • Loading branch information
maninak committed Nov 4, 2024
1 parent 07c833d commit 5e0eb22
Show file tree
Hide file tree
Showing 10 changed files with 249 additions and 53 deletions.
28 changes: 27 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
# Radicle VS Code Extension Change Log

## _(WIP, yet unreleased version)_

### 🚀 Enhancements

- **patch-detail:** add a new "Comment" button next to the revision selector
- clicking the button shows a new create-new-comment form in the top of the Activity section
- if in single-column mode due to narrow viewport the active tab will automatically switch to the Activity section
- form state is preserved as described in [v0.5.0#enhancements](#v050-july-22nd-2024)
- clicking the form's "Comment" button or the keyboard combo Ctrl/Cmd+Enter will submitting the form
- submitting the form attempts to create a new comment on radicle, informing the user of the action's result (created and synced / created only locally / failed) and offering follow-up actions as needed
- upon the comment's successful creation, it is shown directly in place of the create-new-comment form as a new event at the top of the Activity section with the time-ago indicator showing "now"
- **patch-detail:** in the edit-title-&-description form's
- show placeholder text when either text-field is empty
- polish text-field sizing dynamics:
- use 1 line of text as starting height when empty for the patch title field and 4 lines for the description
- respectively limit the max vertical lines for the former and the latter
- fields offer 65 characters of horizontal space (when there's enough viewport width, whatever fits otherwise), which also happens to be the Markdown renderer's wrapping limit (with exceptions). This can double as a subtle hint that we may be typing too much. Longer lines of text will widen the fields to fit them as long as there's enough viewport space, at which point they'll wrap into a new line, breaking at an appropriate manner.
- while both fields remain manually user-resizeable (pursposefully only across height) by mouse-dragging the bottom-right handle of each field _and_ dynamically resizeable as content grows (with contextual restrictions) and shrinks, if the user indeed defines a preferred height using the former method, then it will be respected by the latter
- the aforementioned coupled with the pre-existing feature of optimally auto-aligning the form as it resizes should seamlessly provide a smooth authoring experience
- change button label "Save" to "Update"
- remove button icons

### 🩹 Fixes

- **patch-detail:** make "Refresh" button work again, fetching latest patch data

## **v0.5.1** (September 10th, 2024)

### 🩹 Fixes
Expand Down Expand Up @@ -45,7 +71,7 @@
- responds to viewport size changes applying sizing limits on top of the aforementioned content-relative resizing
- is set with (generous) max char count limits to limit abuse
- pressing the Enter key enters a new line but pressing Ctrl/Cmd+Enter behaves as if the "Save" button was clicked
- the value of each text-area is auto-saved in memory as a draft while typing, as well as the "is editing" status of the form, and those will be attempted to be restored:
- the value of each text-area is preserved as an in-extension draft while typing, as well as the "is editing" status of the form, and those will be attempted to be restored:
- if the editor panel is hidden (another panel is selected placing it in the background) and then re-viewed (same session)
- if VS Code is terminated or crashes (across sessions)
- if the form submission fails
Expand Down
6 changes: 3 additions & 3 deletions src/helpers/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@ export function registerAllCommands(): void {
registerVsCodeCmd('radicle.refreshAllPatches', () => {
usePatchStore().resetAllPatches()
})
registerVsCodeCmd('radicle.refreshOnePatch', (patch: Patch | undefined) => {
assert(patch)
usePatchStore().refetchPatch(patch?.id)
registerVsCodeCmd('radicle.refreshOnePatch', (patchId: Patch['id'] | undefined) => {
assert(patchId)
usePatchStore().refetchPatch(patchId)
})
registerVsCodeCmd('radicle.checkoutPatch', checkOutPatch)
registerVsCodeCmd('radicle.checkoutDefaultBranch', checkOutDefaultBranch)
Expand Down
8 changes: 8 additions & 0 deletions src/helpers/webview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,14 @@ function handleMessageFromWebviewPatchDetail(
}
}
break
case 'createPatchComment':
mutatePatch(message.payload.patchId, message.payload.comment, (timeout?: number) =>
execPatchMutation(
['comment', message.payload.revisionId, '--message', message.payload.comment],
timeout,
),
)
break
default:
assertUnreachable(message)
}
Expand Down
5 changes: 5 additions & 0 deletions src/utils/webview-messaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
Patch,
PatchDetailWebviewInjectedState,
PatchStatus,
Revision,
} from '../types'
import { getVscodeRef } from '../webviews/src/utils/getVscodeRef'

Expand All @@ -28,6 +29,10 @@ type MessageToExtension =
{ patchId: Patch['id']; newTitle: string; newDescr: string; oldTitle: string }
>
| Message<'updatePatchStatus', { patch: Patch; newStatus: Exclude<PatchStatus, 'merged'> }>
| Message<
'createPatchComment',
{ patchId: Patch['id']; revisionId: Revision['id']; comment: string }
>

/**
* Sends a message, usually from the host window, to the provided webview.
Expand Down
10 changes: 7 additions & 3 deletions src/webviews/src/components/EventItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@ const { timeLocale } = storeToRefs(usePatchDetailStore())

<template>
<li class="grid grid-cols-subgrid col-span-3">
<pre :title="getFormattedDate(when, timeLocale)" class="justify-self-end">{{
getTimeAgo(when, 'mini')
}}</pre>
<pre
v-if="!Number.isNaN(when)"
:title="getFormattedDate(when, timeLocale)"
class="justify-self-end"
>{{ getTimeAgo(when, 'mini') }}</pre
>
<span v-else class="no-underline codicon codicon-blank"></span>
<span :class="['no-underline codicon', codicon]"></span>
<div><slot></slot></div>
</li>
Expand Down
13 changes: 12 additions & 1 deletion src/webviews/src/components/PatchDetail.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ import PatchDetailRevision from '@/components/PatchDetailRevision.vue'
provideVSCodeDesignSystem().register(vsCodePanels(), vsCodePanelTab(), vsCodePanelView())
const { patch, authors, firstRevision, latestRevision } = storeToRefs(usePatchDetailStore())
const { patch, authors, firstRevision, latestRevision, patchCommentForm } =
storeToRefs(usePatchDetailStore())
const activityTabRef = ref<HTMLElement>()
const revisionTabRef = ref<HTMLElement>()
const revisionSectionRef = ref<HTMLElement>()
Expand Down Expand Up @@ -62,6 +64,12 @@ function selectAndScrollToRevision(revision: Revision) {
scrollToTemplateRef(revisionSectionRef.value)
}
function showCreateCommentForm() {
// patchCommentForm.value.comment ||= '' // TODO: maninak delete
patchCommentForm.value.isEditing = true
isWindowNarrowerThanSm.value && activityTabRef.value?.click()
}
function assembleRevisionOptionLabel(revision: Revision): string {
const id = shortenHash(revision.id)
const timeAgo = getTimeAgo(revision.timestamp, 'mini')
Expand Down Expand Up @@ -109,6 +117,7 @@ function assembleRevisionOptionLabel(revision: Revision): string {
aria-label="Detailed patch information"
>
<vscode-panel-tab
ref="activityTabRef"
title="Click to See All Events That Took Place During the Lifetime of the Patch"
class="text-lg"
>
Expand All @@ -131,6 +140,7 @@ function assembleRevisionOptionLabel(revision: Revision): string {
<PatchDetailRevision
ref="revisionSectionRef"
@did-select-option="(newOption) => (selectedRevisionOption = newOption)"
@show-create-comment-form="showCreateCommentForm"
:show-heading="false"
:selected-revision="selectedRevision"
:selected-revision-option="selectedRevisionOption"
Expand All @@ -150,6 +160,7 @@ function assembleRevisionOptionLabel(revision: Revision): string {
v-if="!isWindowNarrowerThanSm"
ref="revisionSectionRef"
@did-select-option="(newOption) => (selectedRevisionOption = newOption)"
@show-create-comment-form="showCreateCommentForm"
show-heading
:selected-revision="selectedRevision"
:selected-revision-option="selectedRevisionOption"
Expand Down
125 changes: 123 additions & 2 deletions src/webviews/src/components/PatchDetailActivity.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import {
provideVSCodeDesignSystem,
vsCodeButton,
vsCodeTextArea,
} from '@vscode/webview-ui-toolkit'
import { ref, computed, watchEffect } from 'vue'
import { storeToRefs } from 'pinia'
import { useEventListener } from '@vueuse/core'
import {
getIdentityAliasOrId,
shortenHash,
Expand All @@ -14,12 +20,15 @@ import Markdown from '@/components/Markdown.vue'
import EventList from '@/components/EventList.vue'
import EventItem from '@/components/EventItem.vue'
import Reactions from '@/components/Reactions.vue'
import { notifyExtension } from 'extensionUtils/webview-messaging'
provideVSCodeDesignSystem().register(vsCodeButton(), vsCodeTextArea())
defineProps<{ showHeading: boolean }>()
const emit = defineEmits<{ showRevision: [revision: Revision] }>()
const { patch, firstRevision } = storeToRefs(usePatchDetailStore())
const { patch, firstRevision, patchCommentForm } = storeToRefs(usePatchDetailStore())
function getRevisionHoverTitle(text: string) {
return `Click to See Revision Details\n\nRevision Description:\n"${text}"`
Expand Down Expand Up @@ -63,13 +72,109 @@ const patchEvents = computed(() =>
.flat()
.sort((ev1, ev2) => ev2.ts - ev1.ts),
)
// TODO: maninak extract to usePatchCommentForm? composable
interface VscodeTextAreaEvent {
target: { _value: string }
}
const formEl = ref<HTMLElement>()
const commentTextAreaEl = ref<HTMLElement>()
watchEffect(() => {
const commentEl = commentTextAreaEl.value?.shadowRoot?.querySelector('textarea')
const els = [commentEl].filter(Boolean)
els.forEach((el) => {
useEventListener(
el,
'keydown',
(ev) => {
if (ev.key === 'Escape') {
patchCommentForm.value.isEditing = false
}
if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey)) {
submitPatchCommentForm()
}
},
{ passive: true },
)
useEventListener(el, 'focus', alignViewportWithForm, { passive: true })
useEventListener(el, 'input', alignViewportWithForm, { passive: true })
})
})
function alignViewportWithForm() {
formEl.value?.scrollIntoView({ block: 'end', behavior: 'instant' })
}
watchEffect(() => {
if (patchCommentForm.value.isEditing) {
setTimeout(() => {
commentTextAreaEl.value?.focus()
}, 0) // Vue.nextTick isn't cutting it
}
})
function submitPatchCommentForm() {
patchCommentForm.value.isEditing = false
notifyExtension({
command: 'createPatchComment',
payload: {
patchId: patch.value.id,
revisionId: patch.value.id, // TODO: maninak use the selectedRevision from global store (migrated there from PatchDetail.vue)
comment: patchCommentForm.value.comment.trim(),
},
})
}
function discardPatchCommentForm() {
patchCommentForm.value.isEditing = false
patchCommentForm.value.comment = ''
}
</script>

<template>
<section>
<!-- TODO: add button to expand/collapse all -->
<h2 v-if="showHeading" class="text-lg font-normal mt-0 mb-4">Activity</h2>
<EventList>
<EventItem v-if="patchCommentForm.isEditing" :when="NaN" codicon="codicon-comment">
<form
@submit.prevent
ref="formEl"
name="Edit patch title and description"
class="pb-2 flex flex-col gap-y-3"
>
<vscode-text-area
ref="commentTextAreaEl"
:value="patchCommentForm.comment"
@input="(ev: VscodeTextAreaEvent) => (patchCommentForm.comment = ev.target._value)"
placeholder="Share your kind thoughts…"
name="patch comment"
resize="vertical"
maxlength="50000"
>
New Patch Comment:
</vscode-text-area>
<div class="w-full flex flex-row-reverse justify-start gap-x-2">
<vscode-button
appearance="primary"
title="Save New Comment to Radicle"
@click="submitPatchCommentForm"
>
Comment
</vscode-button>
<vscode-button
appearance="secondary"
title="Stop Editing and Discard Current Changes"
@click="discardPatchCommentForm"
>
Discard
</vscode-button>
</div>
</form>
</EventItem>
<!-- TODO: list committer's email as tooltip -->
<!--<div class="grid grid-cols-subgrid gap-x-3 items-center">
<span
Expand Down Expand Up @@ -232,6 +337,22 @@ const patchEvents = computed(() =>
</template>

<style scoped>
form {
@apply w-fit font-mono text-sm leading-[unset];
min-width: min(100%, 68ch); /* results to allowing 65 chars before resizing to be wider */
}
vscode-text-area::part(control) {
@apply font-mono text-sm;
field-sizing: content;
max-height: min(80ch, 65vh);
word-break: break-word;
}
vscode-text-area::part(label) {
margin-bottom: 0.5em;
}
:deep(.pulse-outline) {
@keyframes outline-pulse {
from {
Expand Down
41 changes: 27 additions & 14 deletions src/webviews/src/components/PatchDetailRevision.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
vsCodeDropdown,
vsCodeOption,
} from '@vscode/webview-ui-toolkit'
import { computed } from 'vue'
import { computed, defineEmits } from 'vue'
import { storeToRefs } from 'pinia'
import { getIdentityAliasOrId, shortenHash } from 'extensionUtils/string'
import { getDateInIsoWithZeroedTimezone, getFormattedDate } from 'extensionUtils/time'
Expand All @@ -25,7 +25,10 @@ const props = defineProps<{
revisionOptionsMap: Map<string, Revision>
}>()
defineEmits<{ didSelectOption: [option: string] }>()
defineEmits<{
didSelectOption: [option: string]
showCreateCommentForm: []
}>()
const { firstRevision, timeLocale } = storeToRefs(usePatchDetailStore())
Expand All @@ -44,19 +47,29 @@ const selectedRevisionRejectedReviews = computed(() =>
<template>
<section>
<h2 v-if="showHeading" class="text-lg font-normal mt-0 mb-3">Revision</h2>
<vscode-dropdown
@change="(ev: CustomEvent) => $emit('didSelectOption', ev.detail._value)"
:value="selectedRevisionOption"
title="Select a Patch Revision to See More Info About It"
class="max-w-full mb-3 font-mono rounded-none"
>
<vscode-option
v-for="revisionOption in revisionOptionsMap.keys()"
:key="revisionOption"
class="font-mono"
>{{ revisionOption }}</vscode-option
<div class="mb-3 flex flex-wrap gap-2">
<vscode-dropdown
@change="(ev: CustomEvent) => $emit('didSelectOption', ev.detail._value)"
:value="selectedRevisionOption"
title="Select a Patch Revision to See More Info About It"
class="max-w-full font-mono rounded-none"
>
</vscode-dropdown>
<vscode-option
v-for="revisionOption in revisionOptionsMap.keys()"
:key="revisionOption"
class="font-mono"
>{{ revisionOption }}</vscode-option
>
</vscode-dropdown>
<div class="flex gap-x-1">
<vscode-button
appearance="secondary"
title="Begin Authoring a Comment on the Selected Revision"
@click="$emit('showCreateCommentForm')"
>Comment</vscode-button
>
</div>
</div>
<Metadatum label="Id">
<pre :title="selectedRevision.id">{{ shortenHash(selectedRevision.id) }}</pre>
<template #aside>
Expand Down
Loading

0 comments on commit 5e0eb22

Please sign in to comment.