mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-23 20:54:50 +03:00
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:
parent
108533777e
commit
29b37f5cab
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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">
|
||||
|
Loading…
Reference in New Issue
Block a user