-
Notifications
You must be signed in to change notification settings - Fork 751
Feature: Label selection when creating merge request #11619
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| <script lang="ts"> | ||
| import ReduxResult from '$components/ReduxResult.svelte'; | ||
| import { Badge, Select, SelectItem, Button } from '@gitbutler/ui'; | ||
| import type { ForgePrService } from '$lib/forge/interface/forgePrService'; | ||
|
|
||
| interface Props { | ||
| projectId: string; | ||
| prService: ForgePrService | undefined; | ||
| selectedLabels: string[]; | ||
| disabled?: boolean; | ||
| } | ||
|
|
||
| let { projectId, prService, selectedLabels = $bindable(), disabled }: Props = $props(); | ||
|
|
||
| let labelsQuery = $state<ReturnType<NonNullable<typeof prService>['labels']>>(); | ||
|
|
||
| function fetchLabels() { | ||
| if (prService) { | ||
| if (!labelsQuery) { | ||
| labelsQuery = prService.labels(); | ||
| } else { | ||
| labelsQuery.result.refetch(); | ||
| } | ||
| } | ||
| } | ||
| </script> | ||
|
|
||
| <div class="label-section"> | ||
| <div class="label-section__header"> | ||
| <span class="label-section__title text-13 text-semibold">Labels</span> | ||
| <Select | ||
| options={(labelsQuery?.result?.data || []) | ||
| .filter((l) => !selectedLabels.includes(l.name)) | ||
| .map((l) => ({ label: l.name, value: l.name }))} | ||
| loading={labelsQuery?.result?.status === 'pending'} | ||
| searchable | ||
| autoWidth={true} | ||
| popupAlign="left" | ||
| closeOnSelect={false} | ||
| {disabled} | ||
| onselect={(value) => { | ||
| if (!selectedLabels.includes(value)) { | ||
| selectedLabels = [...selectedLabels, value]; | ||
| } | ||
| }} | ||
| ontoggle={(isOpen) => { | ||
| if (isOpen) fetchLabels(); | ||
| }} | ||
| > | ||
| {#snippet customSelectButton()} | ||
| <Button | ||
| kind="ghost" | ||
| class="text-13" | ||
| {disabled} | ||
| loading={labelsQuery?.result?.status === 'pending'} | ||
| > | ||
| Edit | ||
| </Button> | ||
| {/snippet} | ||
| {#snippet itemSnippet({ item, highlighted })} | ||
| <SelectItem selected={selectedLabels.includes(item.value)} {highlighted}> | ||
| {item.label} | ||
| </SelectItem> | ||
| {/snippet} | ||
| </Select> | ||
| </div> | ||
|
|
||
| <div class="label-section__content"> | ||
| {#if selectedLabels.length > 0} | ||
| <div class="labels-list"> | ||
| {#each selectedLabels as label} | ||
| <Badge | ||
| color="gray" | ||
| kind="soft" | ||
| size="tag" | ||
| icon="cross-small" | ||
| reversedDirection | ||
| onclick={() => (selectedLabels = selectedLabels.filter((l) => l !== label))} | ||
| > | ||
| {label} | ||
| </Badge> | ||
| {/each} | ||
| </div> | ||
| {:else} | ||
| <span class="text-13 text-secondary">None</span> | ||
| {/if} | ||
| </div> | ||
|
|
||
| {#if labelsQuery?.result?.error} | ||
| <ReduxResult {projectId} result={labelsQuery.result} /> | ||
| {/if} | ||
| </div> | ||
|
|
||
| <style lang="postcss"> | ||
| .label-section { | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 6px; | ||
| } | ||
|
|
||
| .label-section__header { | ||
| display: flex; | ||
| justify-content: space-between; | ||
| align-items: center; | ||
|
|
||
| & :global(.btn-label) { | ||
| font-size: var(--size-13); | ||
| } | ||
| } | ||
|
|
||
| .label-section__title { | ||
| color: var(--clr-text-1); | ||
| } | ||
|
|
||
| .label-section__content { | ||
| display: flex; | ||
| flex-wrap: wrap; | ||
| min-height: 20px; | ||
| } | ||
|
|
||
| .labels-list { | ||
| display: flex; | ||
| flex-wrap: wrap; | ||
| gap: 4px; | ||
| } | ||
|
|
||
| .text-secondary { | ||
| color: var(--clr-text-2); | ||
| } | ||
| </style> |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -10,7 +10,8 @@ import { | |||||
| MergeMethod, | ||||||
| type CreatePullRequestArgs, | ||||||
| type DetailedPullRequest, | ||||||
| type PullRequest | ||||||
| type PullRequest, | ||||||
| type Label | ||||||
| } from '$lib/forge/interface/types'; | ||||||
| import { eventualConsistencyCheck } from '$lib/forge/shared/progressivePolling'; | ||||||
| import { providesItem, invalidatesItem, ReduxTag, invalidatesList } from '$lib/state/tags'; | ||||||
|
|
@@ -39,7 +40,8 @@ export class GitHubPrService implements ForgePrService { | |||||
| body, | ||||||
| draft, | ||||||
| baseBranchName, | ||||||
| upstreamName | ||||||
| upstreamName, | ||||||
| labels | ||||||
| }: CreatePullRequestArgs): Promise<PullRequest> { | ||||||
| this.loading.set(true); | ||||||
| const request = async () => { | ||||||
|
|
@@ -49,7 +51,8 @@ export class GitHubPrService implements ForgePrService { | |||||
| base: baseBranchName, | ||||||
| title, | ||||||
| body, | ||||||
| draft | ||||||
| draft, | ||||||
| labels | ||||||
| }) | ||||||
| ); | ||||||
| }; | ||||||
|
|
@@ -102,6 +105,9 @@ export class GitHubPrService implements ForgePrService { | |||||
| ) { | ||||||
| await this.api.endpoints.updatePr.mutate({ number, update }); | ||||||
| } | ||||||
| labels(options?: StartQueryActionCreatorOptions) { | ||||||
| return this.api.endpoints.getLabels.useQuery(undefined, options); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| async function fetchRepoPermissions( | ||||||
|
|
@@ -183,17 +189,45 @@ function injectEndpoints(api: GitHubApi) { | |||||
| }), | ||||||
| createPr: build.mutation< | ||||||
| CreatePrResult, | ||||||
| { head: string; base: string; title: string; body: string; draft: boolean } | ||||||
| { | ||||||
| head: string; | ||||||
| base: string; | ||||||
| title: string; | ||||||
| body: string; | ||||||
| draft: boolean; | ||||||
| labels: string[]; | ||||||
| } | ||||||
| >({ | ||||||
| queryFn: async ({ head, base, title, body, draft }, api) => | ||||||
| queryFn: async ({ head, base, title, body, draft, labels }, api) => | ||||||
| await ghQuery({ | ||||||
| domain: 'pulls', | ||||||
| action: 'create', | ||||||
| parameters: { head, base, title, body, draft }, | ||||||
| parameters: { head, base, title, body, draft, labels }, | ||||||
| extra: api.extra | ||||||
| }), | ||||||
| invalidatesTags: (result) => [invalidatesItem(ReduxTag.PullRequests, result?.number)] | ||||||
| }), | ||||||
| getLabels: build.query<Label[], void>({ | ||||||
| queryFn: async (_args, api) => { | ||||||
| const result = await ghQuery({ | ||||||
| domain: 'issues', | ||||||
| action: 'listLabelsForRepo', | ||||||
| parameters: {}, | ||||||
| extra: api.extra | ||||||
| }); | ||||||
| if (result.error) { | ||||||
| return { error: result.error }; | ||||||
| } | ||||||
| return { | ||||||
| data: result.data.map((l: any) => ({ | ||||||
| name: l.name, | ||||||
| description: l.description || undefined, | ||||||
| color: l.color | ||||||
| })) | ||||||
| }; | ||||||
| }, | ||||||
| providesTags: [ReduxTag.PullRequests] | ||||||
|
||||||
| providesTags: [ReduxTag.PullRequests] | |
| providesTags: [ReduxTag.Labels] |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -10,7 +10,8 @@ import type { | |||||
| CreatePullRequestArgs, | ||||||
| DetailedPullRequest, | ||||||
| MergeMethod, | ||||||
| PullRequest | ||||||
| PullRequest, | ||||||
| Label | ||||||
| } from '$lib/forge/interface/types'; | ||||||
| import type { QueryOptions } from '$lib/state/butlerModule'; | ||||||
| import type { GitLabApi } from '$lib/state/clientState.svelte'; | ||||||
|
|
@@ -33,7 +34,8 @@ export class GitLabPrService implements ForgePrService { | |||||
| body, | ||||||
| draft, | ||||||
| baseBranchName, | ||||||
| upstreamName | ||||||
| upstreamName, | ||||||
| labels | ||||||
| }: CreatePullRequestArgs): Promise<PullRequest> { | ||||||
| this.loading.set(true); | ||||||
|
|
||||||
|
|
@@ -43,7 +45,8 @@ export class GitLabPrService implements ForgePrService { | |||||
| base: baseBranchName, | ||||||
| title, | ||||||
| body, | ||||||
| draft | ||||||
| draft, | ||||||
| labels | ||||||
| }); | ||||||
| }; | ||||||
|
|
||||||
|
|
@@ -95,6 +98,10 @@ export class GitLabPrService implements ForgePrService { | |||||
| ) { | ||||||
| await this.api.endpoints.updatePr.mutate({ number, update }); | ||||||
| } | ||||||
|
|
||||||
| labels(options?: StartQueryActionCreatorOptions) { | ||||||
| return this.api.endpoints.getLabels.useQuery(undefined, options); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| function injectEndpoints(api: GitLabApi) { | ||||||
|
|
@@ -123,9 +130,16 @@ function injectEndpoints(api: GitLabApi) { | |||||
| }), | ||||||
| createPr: build.mutation< | ||||||
| PullRequest, | ||||||
| { head: string; base: string; title: string; body: string; draft: boolean } | ||||||
| { | ||||||
| head: string; | ||||||
| base: string; | ||||||
| title: string; | ||||||
| body: string; | ||||||
| draft: boolean; | ||||||
| labels: string[]; | ||||||
| } | ||||||
| >({ | ||||||
| queryFn: async ({ head, base, title, body, draft }, query) => { | ||||||
| queryFn: async ({ head, base, title, body, draft, labels }, query) => { | ||||||
| try { | ||||||
| const { api, upstreamProjectId, forkProjectId } = gitlab(query.extra); | ||||||
| const upstreamProject = await api.Projects.show(upstreamProjectId); | ||||||
|
|
@@ -136,7 +150,8 @@ function injectEndpoints(api: GitLabApi) { | |||||
| const mr = await api.MergeRequests.create(forkProjectId, head, base, finalTitle, { | ||||||
| description: body, | ||||||
| targetProjectId: upstreamProject.id, | ||||||
| removeSourceBranch: true | ||||||
| removeSourceBranch: true, | ||||||
| labels: labels.join(',') | ||||||
| }); | ||||||
| return { data: mrToInstance(mr) }; | ||||||
| } catch (e: unknown) { | ||||||
|
|
@@ -145,6 +160,24 @@ function injectEndpoints(api: GitLabApi) { | |||||
| }, | ||||||
| invalidatesTags: (result) => [invalidatesItem(ReduxTag.GitLabPullRequests, result?.number)] | ||||||
| }), | ||||||
| getLabels: build.query<Label[], void>({ | ||||||
| queryFn: async (_args, query) => { | ||||||
| try { | ||||||
| const { api, upstreamProjectId } = gitlab(query.extra); | ||||||
| const labels = await api.ProjectLabels.all(upstreamProjectId); | ||||||
| return { | ||||||
| data: labels.map((l: any) => ({ | ||||||
|
||||||
| name: l.name, | ||||||
| description: l.description || undefined, | ||||||
| color: l.color | ||||||
| })) | ||||||
| }; | ||||||
| } catch (e: unknown) { | ||||||
| return { error: toSerializable(e) }; | ||||||
| } | ||||||
| }, | ||||||
| providesTags: [ReduxTag.GitLabPullRequests] | ||||||
|
||||||
| providesTags: [ReduxTag.GitLabPullRequests] | |
| providesTags: [ReduxTag.Labels] |
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -234,7 +234,7 @@ const FORGE_API_CONFIG = { | |||
| tagTypes: Object.values(ReduxTag), | ||||
| invalidationBehavior: 'immediately' as const, | ||||
| baseQuery: fakeBaseQuery, | ||||
| refetchOnFocus: true, | ||||
| refetchOnFocus: false, | ||||
|
||||
| refetchOnFocus: false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The parameter type is typed as
any. This should be typed properly to ensure type safety. Based on the GitHub API documentation, labels returned fromlistLabelsForReposhould have a specific structure that can be typed.