mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-23 11:45:06 +03:00
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:
parent
0658920abc
commit
72e981f8cc
@ -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,23 +329,12 @@
|
||||
const isPreviewOnly = props.type === 'display';
|
||||
</script>
|
||||
|
||||
<Modal bind:this={modal} width="medium-large" noPadding {onClose} onKeyDown={handleModalKeydown}>
|
||||
<div class="pr-content">
|
||||
<!-- MAIN FIELDS -->
|
||||
<Modal bind:this={modal} width="default" noPadding {onClose} onKeyDown={handleModalKeydown}>
|
||||
<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>
|
||||
|
||||
{#if !isPreviewOnly}
|
||||
<h3 class="text-14 text-semibold pr-title">
|
||||
{!isEditing ? actualTitle : 'Create a pull request'}
|
||||
</h3>
|
||||
<SegmentControl
|
||||
defaultIndex={isPreviewOnly ? 1 : 0}
|
||||
onselect={(id) => {
|
||||
@ -362,35 +345,54 @@
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Segment id="write">Edit</Segment>
|
||||
<Segment id="preview">Preview</Segment>
|
||||
<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>
|
||||
|
||||
<!-- HEADER -->
|
||||
|
||||
<!-- 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}
|
||||
<div class="pr-fields">
|
||||
<TextBox
|
||||
placeholder="PR title"
|
||||
value={actualTitle}
|
||||
readonly={!isEditing || isPreviewOnly}
|
||||
on:change={(e) => {
|
||||
inputTitle = e.detail;
|
||||
}}
|
||||
/>
|
||||
|
||||
<!-- DESCRIPTION FIELD -->
|
||||
<div class="pr-description-field text-input">
|
||||
<BorderlessTextarea
|
||||
value={actualBody}
|
||||
padding={{ top: 0, right: 16, bottom: 16, left: 20 }}
|
||||
autofocus
|
||||
padding={{ top: 12, right: 12, bottom: 0, left: 12 }}
|
||||
placeholder="Add description…"
|
||||
oninput={(e) => {
|
||||
inputBody = e.currentTarget.value;
|
||||
}}
|
||||
/>
|
||||
{/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: 16, right: 16, bottom: 0, left: 20 }}
|
||||
padding={{ top: 12, right: 12, bottom: 0, left: 12 }}
|
||||
placeholder={aiService.prSummaryMainDirective}
|
||||
onkeydown={onMetaEnter(handleAIButtonPressed)}
|
||||
oninput={(e) => {
|
||||
@ -398,7 +400,14 @@
|
||||
}}
|
||||
/>
|
||||
<div class="pr-ai__actions">
|
||||
<Button style="ghost" outline onclick={() => (showAiBox = false)}>Hide</Button>
|
||||
<Button
|
||||
style="ghost"
|
||||
outline
|
||||
onclick={() => {
|
||||
showAiBox = false;
|
||||
aiDescriptionDirective = undefined;
|
||||
}}>Hide</Button
|
||||
>
|
||||
<Button
|
||||
style="neutral"
|
||||
kind="solid"
|
||||
@ -438,8 +447,11 @@
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</ScrollableContainer>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</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 {
|
||||
|
@ -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();
|
||||
}}
|
||||
>
|
||||
|
@ -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();
|
||||
}}
|
||||
>
|
||||
|
@ -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) => {
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user