-
Notifications
You must be signed in to change notification settings - Fork 570
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6208 from gitbutlerapp/BR-cards
BR-cards
- Loading branch information
Showing
12 changed files
with
412 additions
and
203 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
<script lang="ts"> | ||
import BranchReviewButRequest from '$components/BranchReviewButRequest.svelte'; | ||
import { BranchStack, type PatchSeries } from '$lib/branches/branch'; | ||
import { BranchController } from '$lib/branches/branchController'; | ||
import { cloudReviewFunctionality } from '$lib/config/uiFeatureFlags'; | ||
import { getForgePrService } from '$lib/forge/interface/forgePrService'; | ||
import { StackPublishingService } from '$lib/history/stackPublishingService'; | ||
import { getPr } from '$lib/pr/getPr.svelte'; | ||
import { getContextStore, inject } from '@gitbutler/shared/context'; | ||
import Button from '@gitbutler/ui/Button.svelte'; | ||
import ContextMenuItem from '@gitbutler/ui/ContextMenuItem.svelte'; | ||
import ContextMenuSection from '@gitbutler/ui/ContextMenuSection.svelte'; | ||
import DropDownButton from '@gitbutler/ui/DropDownButton.svelte'; | ||
import { isDefined } from '@gitbutler/ui/utils/typeguards'; | ||
import type { DetailedPullRequest } from '$lib/forge/interface/types'; | ||
import type { Snippet } from 'svelte'; | ||
// TODO: This and the SeriesHeader should have a wholistic refactor to | ||
// reduce the complexity of the forge related functionality. | ||
type Props = { | ||
pullRequestCard: Snippet<[DetailedPullRequest]>; | ||
branchStatus: Snippet; | ||
branchLine: Snippet; | ||
openForgePullRequest: () => void; | ||
branch: PatchSeries; | ||
}; | ||
const { pullRequestCard, branchStatus, branchLine, branch, openForgePullRequest }: Props = | ||
$props(); | ||
const stack = getContextStore(BranchStack); | ||
const [stackPublishingService, branchController] = inject( | ||
StackPublishingService, | ||
BranchController | ||
); | ||
async function publishReview() { | ||
await branchController.pushBranch($stack.id, true); | ||
await stackPublishingService.upsertStack($stack.id, branch.name); | ||
} | ||
const prService = getForgePrService(); | ||
const pr = getPr({ | ||
get current() { | ||
return branch; | ||
} | ||
}); | ||
const enum CreationAction { | ||
CreateBR, | ||
CreatePR | ||
} | ||
const creationActionsDisplay = { | ||
[CreationAction.CreateBR]: 'Create butler review', | ||
[CreationAction.CreatePR]: 'Create pull request' | ||
}; | ||
let selectedAction = $state<CreationAction>(); | ||
const actions = $derived.by(() => { | ||
const out: CreationAction[] = []; | ||
if ($prService && !pr.current) { | ||
out.push(CreationAction.CreatePR); | ||
} | ||
if (stackPublishingService.canPublish && !branch.reviewId && $cloudReviewFunctionality) { | ||
out.push(CreationAction.CreateBR); | ||
} | ||
return out; | ||
}); | ||
$effect(() => { | ||
selectedAction = actions.at(0); | ||
}); | ||
let loading = $state(false); | ||
async function create(action?: CreationAction) { | ||
if (!isDefined(action)) return; | ||
loading = true; | ||
try { | ||
switch (action) { | ||
case CreationAction.CreatePR: | ||
await openForgePullRequest(); | ||
break; | ||
case CreationAction.CreateBR: | ||
await publishReview(); | ||
break; | ||
} | ||
} finally { | ||
loading = false; | ||
} | ||
} | ||
const disabled = $derived(branch.patches.length === 0 || branch.conflicted); | ||
const tooltip = $derived( | ||
branch.conflicted ? 'Please resolve the conflicts before creating a PR' : undefined | ||
); | ||
let dropDownButton = $state<DropDownButton>(); | ||
</script> | ||
|
||
<div class="branch-action"> | ||
{@render branchLine()} | ||
<div class="branch-action__body"> | ||
{#if $prService && pr.current} | ||
{@render pullRequestCard(pr.current)} | ||
{/if} | ||
{#if branch.reviewId} | ||
<BranchReviewButRequest reviewId={branch.reviewId} /> | ||
{/if} | ||
|
||
{@render branchStatus()} | ||
|
||
{#if actions.length > 0 && isDefined(selectedAction)} | ||
{#if actions.length > 1} | ||
<DropDownButton | ||
style="neutral" | ||
kind="outline" | ||
onclick={() => create(selectedAction)} | ||
{loading} | ||
bind:this={dropDownButton} | ||
> | ||
{creationActionsDisplay[selectedAction]} | ||
{#snippet contextMenuSlot()} | ||
<ContextMenuSection> | ||
{#each actions as action} | ||
<ContextMenuItem | ||
label={creationActionsDisplay[action]} | ||
onclick={() => { | ||
selectedAction = action; | ||
dropDownButton?.close(); | ||
}} | ||
/> | ||
{/each} | ||
</ContextMenuSection> | ||
{/snippet} | ||
</DropDownButton> | ||
{:else} | ||
<Button | ||
onclick={() => create(selectedAction)} | ||
{disabled} | ||
{tooltip} | ||
{loading} | ||
style="neutral" | ||
kind="outline" | ||
> | ||
{creationActionsDisplay[selectedAction]} | ||
</Button> | ||
{/if} | ||
{/if} | ||
</div> | ||
</div> | ||
|
||
<style lang="postcss"> | ||
.branch-action { | ||
width: 100%; | ||
display: flex; | ||
justify-content: flex-start; | ||
align-items: stretch; | ||
.branch-action__body { | ||
width: 100%; | ||
padding: 0 14px 14px 0; | ||
display: flex; | ||
flex-direction: column; | ||
gap: 14px; | ||
} | ||
} | ||
/* | ||
The :empty selector does not work in svelte because undeterminate reasons. | ||
As such we have this beauty. | ||
All we want to do is to have this thing to not add extra whitespace if | ||
there is nothing interesting going on inside of the component. | ||
*/ | ||
.branch-action:not(:has(> .branch-action__body > *)) { | ||
display: none; | ||
} | ||
</style> |
120 changes: 120 additions & 0 deletions
120
apps/desktop/src/components/BranchReviewButRequest.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
<script lang="ts"> | ||
import { ProjectService } from '$lib/project/projectService'; | ||
import { sleep } from '$lib/utils/sleep'; | ||
import { BranchService as CloudBranchService } from '@gitbutler/shared/branches/branchService'; | ||
import { getBranchReview } from '@gitbutler/shared/branches/branchesPreview.svelte'; | ||
import { lookupLatestBranchUuid } from '@gitbutler/shared/branches/latestBranchLookup.svelte'; | ||
import { LatestBranchLookupService } from '@gitbutler/shared/branches/latestBranchLookupService'; | ||
import { inject } from '@gitbutler/shared/context'; | ||
import Loading from '@gitbutler/shared/network/Loading.svelte'; | ||
import { and, combine, isFound, isNotFound, map } from '@gitbutler/shared/network/loadable'; | ||
import { ProjectService as CloudProjectService } from '@gitbutler/shared/organizations/projectService'; | ||
import { getProjectByRepositoryId } from '@gitbutler/shared/organizations/projectsPreview.svelte'; | ||
import { AppState } from '@gitbutler/shared/redux/store.svelte'; | ||
import { WebRoutesService } from '@gitbutler/shared/routing/webRoutes.svelte'; | ||
import Link from '@gitbutler/ui/link/Link.svelte'; | ||
import { untrack } from 'svelte'; | ||
type Props = { | ||
reviewId: string; | ||
}; | ||
const { reviewId }: Props = $props(); | ||
const [ | ||
projectService, | ||
appState, | ||
cloudProjectService, | ||
latestBranchLookupService, | ||
cloudBranchService, | ||
webRoutes | ||
] = inject( | ||
ProjectService, | ||
AppState, | ||
CloudProjectService, | ||
LatestBranchLookupService, | ||
CloudBranchService, | ||
WebRoutesService | ||
); | ||
const project = projectService.project; | ||
const cloudProject = $derived( | ||
$project?.api?.repository_id | ||
? getProjectByRepositoryId(appState, cloudProjectService, $project.api.repository_id) | ||
: undefined | ||
); | ||
const cloudBranchUuid = $derived( | ||
map(cloudProject?.current, (cloudProject) => { | ||
return lookupLatestBranchUuid( | ||
appState, | ||
latestBranchLookupService, | ||
cloudProject.owner, | ||
cloudProject.slug, | ||
reviewId | ||
); | ||
}) | ||
); | ||
const cloudBranch = $derived( | ||
map(cloudBranchUuid?.current, (cloudBranchUuid) => { | ||
return getBranchReview(appState, cloudBranchService, cloudBranchUuid); | ||
}) | ||
); | ||
$effect(() => { | ||
const options = { keepPolling: true }; | ||
if (anyNotFound()) { | ||
pollWhileNotFound(reviewId, options); | ||
} | ||
return () => { | ||
options.keepPolling = false; | ||
}; | ||
}); | ||
async function pollWhileNotFound(reviewId: string, options: { keepPolling: boolean }) { | ||
let counter = 0; | ||
while (counter < 8 && options.keepPolling && untrack(() => anyNotFound())) { | ||
await sleep(100 * (2 ^ counter)); | ||
await invalidateAll(reviewId); | ||
} | ||
} | ||
function anyNotFound() { | ||
return isNotFound(cloudBranchUuid?.current) || isNotFound(cloudBranch?.current); | ||
} | ||
async function invalidateAll(reviewId: string) { | ||
await untrack(async () => { | ||
if (!isFound(cloudProject?.current)) return; | ||
if (isNotFound(cloudBranchUuid?.current)) { | ||
await latestBranchLookupService.refreshBranchUuid(reviewId); | ||
} | ||
if (isFound(cloudBranchUuid?.current) && isNotFound(cloudBranch?.current)) { | ||
await cloudBranchService.refreshBranch(cloudBranchUuid.current.value); | ||
} | ||
}); | ||
} | ||
</script> | ||
|
||
<Loading | ||
loadable={and([cloudBranchUuid?.current, combine([cloudBranch?.current, cloudProject?.current])])} | ||
> | ||
{#snippet children([cloudBranch, cloudProject])} | ||
<Link | ||
target="_blank" | ||
rel="noreferrer" | ||
href={webRoutes.projectReviewBranchUrl({ | ||
ownerSlug: cloudProject.owner, | ||
projectSlug: cloudProject.slug, | ||
branchId: cloudBranch.branchId | ||
})} | ||
> | ||
Open review</Link | ||
> | ||
{/snippet} | ||
</Loading> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.