Create PR preview

When creating a PR, display a preview of the title and body to confirm with the user
This commit is contained in:
estib 2024-09-26 10:19:47 +02:00
parent 108533777e
commit 29b37f5cab
3 changed files with 151 additions and 68 deletions

View File

@ -4,7 +4,6 @@
import BranchLaneContextMenu from './BranchLaneContextMenu.svelte';
import DefaultTargetButton from './DefaultTargetButton.svelte';
import PullRequestButton from '../pr/PullRequestButton.svelte';
import { Project } from '$lib/backend/projects';
import { BaseBranch } from '$lib/baseBranch/baseBranch';
import { BaseBranchService } from '$lib/baseBranch/baseBranchService';
import ContextMenu from '$lib/components/contextmenu/ContextMenu.svelte';
@ -14,6 +13,7 @@
import { getGitHostPrMonitor } from '$lib/gitHost/interface/gitHostPrMonitor';
import { getGitHostPrService } from '$lib/gitHost/interface/gitHostPrService';
import { showError, showToast } from '$lib/notifications/toasts';
import PrDetailsModal, { type CreatePrParams } from '$lib/pr/PrDetailsModal.svelte';
import { getBranchNameFromRef } from '$lib/utils/branch';
import { getContext, getContextStore } from '$lib/utils/context';
import { sleep } from '$lib/utils/sleep';
@ -22,6 +22,7 @@
import { VirtualBranch } from '$lib/vbranches/types';
import Button from '@gitbutler/ui/Button.svelte';
import Icon from '@gitbutler/ui/Icon.svelte';
import { tick } from 'svelte';
import type { PullRequest } from '$lib/gitHost/interface/types';
import type { Persisted } from '$lib/persisted/persisted';
@ -41,13 +42,14 @@
const branchStore = getContextStore(VirtualBranch);
const prMonitor = getGitHostPrMonitor();
const gitHost = getGitHost();
const project = getContext(Project);
const baseBranchName = $derived($baseBranch.shortName);
const branch = $derived($branchStore);
const pr = $derived($prMonitor?.pr);
let contextMenu = $state<ReturnType<typeof ContextMenu>>();
let useDraftPr = $state<boolean>(false);
let prDetailsModal = $state<ReturnType<typeof PrDetailsModal>>();
let meatballButtonEl = $state<HTMLDivElement>();
let isLoading = $state(false);
let isTargetBranchAnimated = $state(false);
@ -70,50 +72,12 @@
let headerInfoHeight = $state(0);
interface CreatePrOpts {
draft: boolean;
}
const defaultPrOpts: CreatePrOpts = {
draft: true
};
async function createPr(createPrOpts: CreatePrOpts): Promise<PullRequest | undefined> {
const opts = { ...defaultPrOpts, ...createPrOpts };
async function createPr(params: CreatePrParams): Promise<PullRequest | undefined> {
if (!$gitHost) {
error('Pull request service not available');
return;
}
let title: string;
let body: string;
let pullRequestTemplateBody: string | undefined;
const prTemplatePath = project.git_host.pullRequestTemplatePath;
if (prTemplatePath) {
pullRequestTemplateBody = await $prService?.pullRequestTemplateContent(
prTemplatePath,
project.id
);
}
if (pullRequestTemplateBody) {
title = branch.name;
body = pullRequestTemplateBody;
} else {
// In case of a single commit, use the commit summary and description for the title and
// description of the PR.
if (branch.commits.length === 1) {
const commit = branch.commits[0];
title = commit?.descriptionTitle ?? '';
body = commit?.descriptionBody ?? '';
} else {
title = branch.name;
body = '';
}
}
isLoading = true;
try {
let upstreamBranchName = branch.upstreamName;
@ -148,9 +112,9 @@
}
await $prService.createPr({
title,
body,
draft: opts.draft,
title: params.title,
body: params.body,
draft: params.draft,
baseBranchName,
upstreamName: upstreamBranchName
});
@ -165,6 +129,12 @@
await $gitListService?.refresh();
baseBranchService.fetchFromRemotes();
}
async function handleCreatePR(draft: boolean) {
useDraftPr = draft;
await tick();
prDetailsModal?.show();
}
</script>
{#if $isLaneCollapsed}
@ -270,7 +240,7 @@
<div class="header__buttons">
{#if !$pr}
<PullRequestButton
click={async ({ draft }) => await createPr({ draft })}
click={async ({ draft }) => await handleCreatePR(draft)}
disabled={branch.commits.length === 0 || !$gitHost || !$prService}
tooltip={!$gitHost || !$prService
? 'You can enable git host integration in the settings'
@ -301,6 +271,13 @@
</div>
{/if}
<PrDetailsModal
bind:this={prDetailsModal}
type="preview"
onCreatePr={createPr}
draft={useDraftPr}
/>
<style>
.header__wrapper {
z-index: var(--z-lifted);

View File

@ -1,17 +1,86 @@
<script lang="ts" module>
export interface CreatePrParams {
title: string;
body: string;
draft: boolean;
}
</script>
<script lang="ts">
import { Project } from '$lib/backend/projects';
import Markdown from '$lib/components/Markdown.svelte';
import { getGitHostPrService } from '$lib/gitHost/interface/gitHostPrService';
import ScrollableContainer from '$lib/scroll/ScrollableContainer.svelte';
import { getContext, getContextStore } from '$lib/utils/context';
import { VirtualBranch } from '$lib/vbranches/types';
import Button from '@gitbutler/ui/Button.svelte';
import Modal from '@gitbutler/ui/Modal.svelte';
import type { DetailedPullRequest } from '$lib/gitHost/interface/types';
interface Props {
interface BaseProps {
type: 'display' | 'preview';
}
interface DisplayProps extends BaseProps {
type: 'display';
pr: DetailedPullRequest;
}
let { pr }: Props = $props();
interface PreviewProps extends BaseProps {
type: 'preview';
draft: boolean;
onCreatePr: (p: CreatePrParams) => void;
}
type Props = DisplayProps | PreviewProps;
let props: Props = $props();
const project = getContext(Project);
const branchStore = getContextStore(VirtualBranch);
const prService = getGitHostPrService();
let modal = $state<Modal>();
const branch = $derived($branchStore);
const prTemplatePath = $derived(project.git_host.pullRequestTemplatePath);
let pullRequestTemplateBody = $state<string | undefined>(undefined);
const previewTitle: string | undefined = $derived.by(() => {
if (props.type !== 'preview') return undefined;
// In case of a single commit, use the commit summary for the title
if (branch.commits.length === 1) {
const commit = branch.commits[0];
return commit?.descriptionTitle ?? '';
} else {
return branch.name;
}
});
const previewBody: string | undefined = $derived.by(() => {
if (props.type !== 'preview') return undefined;
if (pullRequestTemplateBody) return pullRequestTemplateBody;
// In case of a single commit, use the commit description for the body
if (branch.commits.length === 1) {
const commit = branch.commits[0];
return commit?.descriptionBody ?? '';
} else {
return '';
}
});
$effect(() => {
if ($prService && pullRequestTemplateBody === undefined) {
$prService.pullRequestTemplateContent(prTemplatePath, project.id).then((template) => {
pullRequestTemplateBody = template;
});
}
});
function handleCreatePR(close: () => void) {
if (props.type !== 'preview') return;
props.onCreatePr({ title: previewTitle ?? '', body: previewBody ?? '', draft: props.draft });
close();
}
export function show() {
modal?.show();
@ -24,29 +93,66 @@
};
</script>
<Modal bind:this={modal} width="large" noPadding>
{#snippet children(_, close)}
<ScrollableContainer maxHeight="70vh">
<div class="pr-modal__content">
<div class="card">
<div class="card__header text-14 text-body text-semibold pr-modal__header">
{pr.title}
</div>
{#if pr.body}
<div class="card__content text-13 text-body">
<Markdown content={pr.body} />
<!-- PREVIEW MODAL -->
{#if props.type === 'preview'}
<Modal bind:this={modal} width="large" noPadding onSubmit={handleCreatePR}>
{#snippet children(_, close)}
<ScrollableContainer maxHeight="70vh">
<div class="pr-modal__content">
<div class="card">
<div class="card__header text-14 text-body text-semibold pr-modal__header">
{#if previewTitle}
{previewTitle}
{:else}
<span class="text-clr2"> No title provided.</span>
{/if}
</div>
{:else}
<div class="card__content text-13 text-body text-clr2">No PR description.</div>
{/if}
{#if previewBody}
<div class="card__content text-13 text-body">
<Markdown content={previewBody} />
</div>
{:else}
<div class="card__content text-13 text-body text-clr2">No PR description.</div>
{/if}
</div>
</div>
</ScrollableContainer>
<div class="pr-modal__footer">
<Button style="ghost" outline onclick={close}>Cancel</Button>
<Button style="pop" type="submit" kind="solid"
>{props.draft ? 'Create Draft PR' : 'Create PR'}</Button
>
</div>
</ScrollableContainer>
<div class="pr-modal__footer">
<Button style="ghost" outline onclick={close}>Done</Button>
</div>
{/snippet}
</Modal>
{/snippet}
</Modal>
{/if}
<!-- DISPLAY -->
{#if props.type === 'display'}
<Modal bind:this={modal} width="large" noPadding>
{#snippet children(_, close)}
<ScrollableContainer maxHeight="70vh">
<div class="pr-modal__content">
<div class="card">
<div class="card__header text-14 text-body text-semibold pr-modal__header">
{props.pr.title}
</div>
{#if props.pr.body}
<div class="card__content text-13 text-body">
<Markdown content={props.pr.body} />
</div>
{:else}
<div class="card__content text-13 text-body text-clr2">No PR description.</div>
{/if}
</div>
</div>
</ScrollableContainer>
<div class="pr-modal__footer">
<Button style="ghost" outline onclick={close}>Done</Button>
</div>
{/snippet}
</Modal>
{/if}
<style>
.pr-modal__content {

View File

@ -284,7 +284,7 @@
{/if}
{#if $pr}
<PrDetailsModal bind:this={prDetailsModal} pr={$pr} />
<PrDetailsModal bind:this={prDetailsModal} type="display" pr={$pr} />
{/if}
<style lang="postcss">