pr details follow up (#5054)

* PR Details: CMD or Ctrl + Click opens the browser

* Segment: Optionally make it unfocusable

Control whethe the segment can be focused on tab, or not

* PR Details: Remove unused 'e' handler

* Borderless Textarea: Ability to autofocus

Optionally, autofocus the input field on mount

* PR Details: Update focus behavior

- Focus on the title input filed on mount
- Make the segments unfocusable

* design update

* Update PrDetailsModal.svelte

---------

Co-authored-by: Pavel Laptev <pawellaptew@gmail.com>
This commit is contained in:
Esteban Vega 2024-10-08 15:50:43 +02:00 committed by GitHub
parent 0658920abc
commit 72e981f8cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 159 additions and 108 deletions

View File

@ -22,6 +22,7 @@
import { isFailure } from '$lib/result';
import ScrollableContainer from '$lib/scroll/ScrollableContainer.svelte';
import BorderlessTextarea from '$lib/shared/BorderlessTextarea.svelte';
import TextBox from '$lib/shared/TextBox.svelte';
import Toggle from '$lib/shared/Toggle.svelte';
import { User } from '$lib/stores/user';
import { autoHeight } from '$lib/utils/autoHeight';
@ -245,9 +246,9 @@
onToken: (t) => {
if (firstToken) {
firstToken = false;
inputBody = '';
}
inputBody += t;
inputBody = '';
updateFieldsHeight();
}
});
@ -260,7 +261,6 @@
inputBody = descriptionResult.value;
aiIsLoading = false;
aiDescriptionDirective = undefined;
await tick();
updateFieldsHeight();
@ -268,12 +268,6 @@
function handleModalKeydown(e: KeyboardEvent) {
switch (e.key) {
case 'e':
if (e.metaKey || e.ctrlKey) {
e.stopPropagation();
e.preventDefault();
}
break;
case 'g':
if ((e.metaKey || e.ctrlKey) && e.shiftKey) {
e.stopPropagation();
@ -335,111 +329,129 @@
const isPreviewOnly = props.type === 'display';
</script>
<Modal bind:this={modal} width="medium-large" noPadding {onClose} onKeyDown={handleModalKeydown}>
<div class="pr-content">
<!-- MAIN FIELDS -->
<div class="pr-header">
<div class="pr-title">
<BorderlessTextarea
placeholder="PR title"
value={actualTitle}
fontSize={18}
readonly={!isEditing || isPreviewOnly}
oninput={(e) => {
inputTitle = e.currentTarget.value;
}}
/>
</div>
<Modal bind:this={modal} width="default" noPadding {onClose} onKeyDown={handleModalKeydown}>
<div class="pr-header">
{#if !isPreviewOnly}
<h3 class="text-14 text-semibold pr-title">
{!isEditing ? actualTitle : 'Create a pull request'}
</h3>
<SegmentControl
defaultIndex={isPreviewOnly ? 1 : 0}
onselect={(id) => {
if (id === 'write') {
isEditing = true;
} else {
isEditing = false;
}
}}
>
<Segment unfocusable id="write">Edit</Segment>
<Segment unfocusable id="preview">Preview</Segment>
</SegmentControl>
{:else}
<h3 class="text-14 text-semibold pr-title">{actualTitle}</h3>
{/if}
</div>
{#if !isPreviewOnly}
<SegmentControl
defaultIndex={isPreviewOnly ? 1 : 0}
onselect={(id) => {
if (id === 'write') {
isEditing = true;
} else {
isEditing = false;
}
}}
>
<Segment id="write">Edit</Segment>
<Segment id="preview">Preview</Segment>
</SegmentControl>
{/if}
</div>
<!-- HEADER -->
<ScrollableContainer wide maxHeight="66vh" onscroll={showBorderOnScroll}>
<!-- MAIN FIELDS -->
<ScrollableContainer wide maxHeight="66vh" onscroll={showBorderOnScroll}>
<div class="pr-content">
{#if isPreviewOnly || !isEditing}
<div class="pr-description-preview">
<Markdown content={actualBody} />
</div>
{:else}
<BorderlessTextarea
value={actualBody}
padding={{ top: 0, right: 16, bottom: 16, left: 20 }}
placeholder="Add description…"
oninput={(e) => {
inputBody = e.currentTarget.value;
}}
/>
{/if}
<div class="pr-fields">
<TextBox
placeholder="PR title"
value={actualTitle}
readonly={!isEditing || isPreviewOnly}
on:change={(e) => {
inputTitle = e.detail;
}}
/>
<!-- AI GENRATION -->
{#if !isPreviewOnly && canUseAI && isEditing}
<div class="pr-ai" class:show-ai-box={showAiBox}>
{#if showAiBox}
<!-- DESCRIPTION FIELD -->
<div class="pr-description-field text-input">
<BorderlessTextarea
bind:value={aiDescriptionDirective}
padding={{ top: 16, right: 16, bottom: 0, left: 20 }}
placeholder={aiService.prSummaryMainDirective}
onkeydown={onMetaEnter(handleAIButtonPressed)}
value={actualBody}
autofocus
padding={{ top: 12, right: 12, bottom: 0, left: 12 }}
placeholder="Add description…"
oninput={(e) => {
aiDescriptionDirective = e.currentTarget.value;
inputBody = e.currentTarget.value;
}}
/>
<div class="pr-ai__actions">
<Button style="ghost" outline onclick={() => (showAiBox = false)}>Hide</Button>
<Button
style="neutral"
kind="solid"
icon="ai-small"
tooltip={!aiConfigurationValid
? 'You must be logged in or have provided your own API key'
: !$aiGenEnabled
? 'You must have summary generation enabled'
: undefined}
disabled={!canUseAI || aiIsLoading}
isLoading={aiIsLoading}
onclick={handleAIButtonPressed}
>
Generate
</Button>
</div>
{:else}
<div class="pr-ai__actions">
<Button
style="ghost"
outline
icon="ai-small"
tooltip={!aiConfigurationValid
? 'You must be logged in or have provided your own API key'
: !$aiGenEnabled
? 'You must have summary generation enabled'
: undefined}
disabled={!canUseAI || aiIsLoading}
isLoading={aiIsLoading}
onclick={() => {
showAiBox = true;
}}
>
Generate description
</Button>
</div>
{/if}
<!-- AI GENRATION -->
{#if !isPreviewOnly && canUseAI && isEditing}
<div class="pr-ai" class:show-ai-box={showAiBox}>
{#if showAiBox}
<BorderlessTextarea
autofocus
bind:value={aiDescriptionDirective}
padding={{ top: 12, right: 12, bottom: 0, left: 12 }}
placeholder={aiService.prSummaryMainDirective}
onkeydown={onMetaEnter(handleAIButtonPressed)}
oninput={(e) => {
aiDescriptionDirective = e.currentTarget.value;
}}
/>
<div class="pr-ai__actions">
<Button
style="ghost"
outline
onclick={() => {
showAiBox = false;
aiDescriptionDirective = undefined;
}}>Hide</Button
>
<Button
style="neutral"
kind="solid"
icon="ai-small"
tooltip={!aiConfigurationValid
? 'You must be logged in or have provided your own API key'
: !$aiGenEnabled
? 'You must have summary generation enabled'
: undefined}
disabled={!canUseAI || aiIsLoading}
isLoading={aiIsLoading}
onclick={handleAIButtonPressed}
>
Generate
</Button>
</div>
{:else}
<div class="pr-ai__actions">
<Button
style="ghost"
outline
icon="ai-small"
tooltip={!aiConfigurationValid
? 'You must be logged in or have provided your own API key'
: !$aiGenEnabled
? 'You must have summary generation enabled'
: undefined}
disabled={!canUseAI || aiIsLoading}
isLoading={aiIsLoading}
onclick={() => {
showAiBox = true;
}}
>
Generate description
</Button>
</div>
{/if}
</div>
{/if}
</div>
</div>
{/if}
</ScrollableContainer>
</div>
</div>
</ScrollableContainer>
<!-- FOOTER -->
@ -492,14 +504,34 @@
.pr-content {
display: flex;
flex-direction: column;
padding: 0 16px 16px;
}
.pr-header {
display: flex;
align-items: center;
gap: 16px;
padding: 16px 16px 12px 20px;
padding: 16px 16px 14px;
}
/* FIELDS */
.pr-fields {
display: flex;
flex-direction: column;
gap: 10px;
}
.pr-description-field {
flex: 1;
display: flex;
flex-direction: column;
/* reset .text-input padding */
padding: 0;
}
/* PREVIEW */
.pr-title {
flex: 1;
margin-top: 4px;
@ -508,7 +540,6 @@
.pr-description-preview {
overflow-y: auto;
display: flex;
padding: 0 16px 16px 20px;
}
/* AI BOX */
@ -519,13 +550,14 @@
}
.show-ai-box {
margin-top: 12px;
border-top: 1px solid var(--clr-border-3);
}
.pr-ai__actions {
display: flex;
gap: 6px;
padding: 12px 20px 16px;
padding: 12px;
}
/* FOOTER */
@ -539,7 +571,7 @@
.pr-footer__actions {
display: flex;
gap: 8px;
gap: 6px;
}
.draft-toggle__wrap {

View File

@ -9,6 +9,7 @@
import { getGitHostPrService } from '$lib/gitHost/interface/gitHostPrService';
import { getContext } from '$lib/utils/context';
import * as toasts from '$lib/utils/toasts';
import { openExternalUrl } from '$lib/utils/url';
import { VirtualBranchService } from '$lib/vbranches/virtualBranch';
import Button from '@gitbutler/ui/Button.svelte';
import { type ComponentColor } from '@gitbutler/ui/utils/colorTypes';
@ -202,7 +203,11 @@
style="ghost"
outline
icon="description-small"
onclick={() => {
onclick={(e: MouseEvent) => {
if (e.ctrlKey || e.metaKey) {
openExternalUrl($pr.htmlUrl);
return;
}
prDetailsModal?.show();
}}
>

View File

@ -9,6 +9,7 @@
import { getGitHostPrService } from '$lib/gitHost/interface/gitHostPrService';
import { getContext } from '$lib/utils/context';
import * as toasts from '$lib/utils/toasts';
import { openExternalUrl } from '$lib/utils/url';
import { VirtualBranchService } from '$lib/vbranches/virtualBranch';
import Button from '@gitbutler/ui/Button.svelte';
import { type ComponentColor } from '@gitbutler/ui/utils/colorTypes';
@ -205,7 +206,11 @@
style="ghost"
outline
icon="description-small"
onclick={() => {
onclick={(e: MouseEvent) => {
if (e.ctrlKey || e.metaKey) {
openExternalUrl(pr.htmlUrl);
return;
}
prDetailsModal?.show();
}}
>

View File

@ -11,6 +11,7 @@
readonly?: boolean;
fontSize?: number;
maxHeight?: string;
autofocus?: boolean;
padding?: {
top: number;
right: number;
@ -29,6 +30,7 @@
readonly,
fontSize = 14,
maxHeight = 'none',
autofocus = false,
padding = { top: 0, right: 0, bottom: 0, left: 0 },
oninput,
onfocus,
@ -37,12 +39,18 @@
onMount(() => {
setTimeout(() => {
if (ref) autoHeight(ref);
if (ref) {
autoHeight(ref);
if (autofocus) {
ref.focus();
}
}
}, 0);
});
</script>
<textarea
tabindex="0"
bind:this={ref}
bind:value
use:resizeObserver={(e) => {

View File

@ -7,10 +7,11 @@
id: string;
onselect?: (id: string) => void;
disabled?: boolean;
unfocusable?: boolean;
children: Snippet;
}
const { id, onselect, children, disabled = false }: SegmentProps = $props();
const { id, onselect, children, disabled = false, unfocusable = false }: SegmentProps = $props();
const context = getContext<SegmentContext>('SegmentControl');
const index = context.setIndex();
@ -39,7 +40,7 @@
class="segment-control-item"
role="tab"
{disabled}
tabindex={isSelected ? -1 : 0}
tabindex={isSelected || unfocusable ? -1 : 0}
aria-selected={isSelected}
onmousedown={() => {
if (index !== $selectedSegmentIndex) {