From d3c1ed8ed49c55749768a5b3bb0d822769b5ae10 Mon Sep 17 00:00:00 2001 From: estib Date: Sun, 8 Sep 2024 12:16:56 +0200 Subject: [PATCH 01/43] Style: Checkbox indeterminate state Make it so that the indeterminate state of the checkbox matches the selected style --- .../src/lib/file/BranchFilesHeader.svelte | 1 + packages/ui/src/lib/Checkbox.svelte | 88 +++++++++++++++---- packages/ui/src/lib/data/design-tokens.json | 76 ++++++++++++---- packages/ui/src/styles/core/design-tokens.css | 44 ++++++---- 4 files changed, 159 insertions(+), 50 deletions(-) diff --git a/apps/desktop/src/lib/file/BranchFilesHeader.svelte b/apps/desktop/src/lib/file/BranchFilesHeader.svelte index 527e4b3e6..d589f80db 100644 --- a/apps/desktop/src/lib/file/BranchFilesHeader.svelte +++ b/apps/desktop/src/lib/file/BranchFilesHeader.svelte @@ -49,6 +49,7 @@ small {checked} {indeterminate} + style={indeterminate ? 'neutral' : 'default'} onchange={(e: Event & { currentTarget: EventTarget & HTMLInputElement; }) => { const isChecked = e.currentTarget.checked; if (isChecked) { diff --git a/packages/ui/src/lib/Checkbox.svelte b/packages/ui/src/lib/Checkbox.svelte index 07e5d7d4a..9ffcb7c36 100644 --- a/packages/ui/src/lib/Checkbox.svelte +++ b/packages/ui/src/lib/Checkbox.svelte @@ -1,4 +1,5 @@ {#if !$commit?.isMergeCommit()} @@ -60,12 +54,12 @@ GitHub, or run the following command in your project directory:

- +
From 8c22b6c0d1f9ce612ae237cb12dab74bd60b252b Mon Sep 17 00:00:00 2001 From: estib Date: Mon, 9 Sep 2024 13:53:57 +0200 Subject: [PATCH 06/43] Update Ownership class Rename the `Ownership` class to `SelectedOwnership`, as it is only used to determine the selected state of the files to add to a commit. Renamed the methods as well to convey their actual purpose. The `SelectedOwnership` class will be updated in a way that the selected is persisted across file/hunk ownership updates --- apps/desktop/src/lib/branch/BranchLane.svelte | 9 +- .../src/lib/commit/CommitDialog.svelte | 7 +- .../src/lib/commit/CommitMessageInput.svelte | 6 +- .../src/lib/file/BranchFilesHeader.svelte | 21 ++- apps/desktop/src/lib/file/FileListItem.svelte | 26 +-- apps/desktop/src/lib/hunk/HunkDiff.svelte | 7 +- apps/desktop/src/lib/hunk/HunkViewer.svelte | 9 +- apps/desktop/src/lib/vbranches/ownership.ts | 171 ++++++++++++++---- 8 files changed, 178 insertions(+), 78 deletions(-) diff --git a/apps/desktop/src/lib/branch/BranchLane.svelte b/apps/desktop/src/lib/branch/BranchLane.svelte index 33474882f..26916b414 100644 --- a/apps/desktop/src/lib/branch/BranchLane.svelte +++ b/apps/desktop/src/lib/branch/BranchLane.svelte @@ -19,7 +19,7 @@ createRemoteCommitsContextStore } from '$lib/vbranches/contexts'; import { FileIdSelection } from '$lib/vbranches/fileIdSelection'; - import { Ownership } from '$lib/vbranches/ownership'; + import { SelectedOwnership } from '$lib/vbranches/ownership'; import { RemoteFile, VirtualBranch } from '$lib/vbranches/types'; import lscache from 'lscache'; import { setContext } from 'svelte'; @@ -55,12 +55,15 @@ // BRANCH const branchStore = createContextStore(VirtualBranch, branch); - const ownershipStore = createContextStore(Ownership, Ownership.fromBranch(branch)); + const selectedOwnershipStore = createContextStore( + SelectedOwnership, + SelectedOwnership.fromBranch(branch) + ); const branchFiles = writable(branch.files); $effect(() => { branchStore.set(branch); - ownershipStore.set(Ownership.fromBranch(branch)); + selectedOwnershipStore.update((o) => o?.update(branch)); branchFiles.set(branch.files); }); diff --git a/apps/desktop/src/lib/commit/CommitDialog.svelte b/apps/desktop/src/lib/commit/CommitDialog.svelte index 9ec51fe38..913f024a9 100644 --- a/apps/desktop/src/lib/commit/CommitDialog.svelte +++ b/apps/desktop/src/lib/commit/CommitDialog.svelte @@ -4,7 +4,7 @@ import { getContext, getContextStore } from '$lib/utils/context'; import { intersectionObserver } from '$lib/utils/intersectionObserver'; import { BranchController } from '$lib/vbranches/branchController'; - import { Ownership } from '$lib/vbranches/ownership'; + import { SelectedOwnership } from '$lib/vbranches/ownership'; import { VirtualBranch } from '$lib/vbranches/types'; import Button from '@gitbutler/ui/Button.svelte'; import { slideFade } from '@gitbutler/ui/utils/transitions'; @@ -15,7 +15,7 @@ export let hasSectionsAfter: boolean; const branchController = getContext(BranchController); - const selectedOwnership = getContextStore(Ownership); + const selectedOwnership = getContextStore(SelectedOwnership); const branch = getContextStore(VirtualBranch); const runCommitHooks = projectRunCommitHooks(projectId); @@ -88,7 +88,8 @@ outline={!$expanded} grow loading={isCommitting} - disabled={(isCommitting || !commitMessageValid || $selectedOwnership.isEmpty()) && $expanded} + disabled={(isCommitting || !commitMessageValid || $selectedOwnership.nothingSelected()) && + $expanded} id="commit-to-branch" onclick={() => { if ($expanded) { diff --git a/apps/desktop/src/lib/commit/CommitMessageInput.svelte b/apps/desktop/src/lib/commit/CommitMessageInput.svelte index 31220c100..64409fb36 100644 --- a/apps/desktop/src/lib/commit/CommitMessageInput.svelte +++ b/apps/desktop/src/lib/commit/CommitMessageInput.svelte @@ -19,7 +19,7 @@ import { KeyName } from '$lib/utils/hotkeys'; import { resizeObserver } from '$lib/utils/resizeObserver'; import { isWhiteSpaceString } from '$lib/utils/string'; - import { Ownership } from '$lib/vbranches/ownership'; + import { SelectedOwnership } from '$lib/vbranches/ownership'; import { VirtualBranch, LocalFile } from '$lib/vbranches/types'; import Checkbox from '@gitbutler/ui/Checkbox.svelte'; import Icon from '@gitbutler/ui/Icon.svelte'; @@ -33,7 +33,7 @@ export let commit: (() => void) | undefined = undefined; const user = getContextStore(User); - const selectedOwnership = getContextStore(Ownership); + const selectedOwnership = getContextStore(SelectedOwnership); const aiService = getContext(AIService); const branch = getContextStore(VirtualBranch); const project = getContext(Project); @@ -71,7 +71,7 @@ async function generateCommitMessage(files: LocalFile[]) { const hunks = files.flatMap((f) => - f.hunks.filter((h) => $selectedOwnership.contains(f.id, h.id)) + f.hunks.filter((h) => $selectedOwnership.isSelected(f.id, h.id)) ); // Branches get their names generated only if there are at least 4 lines of code // If the change is a 'one-liner', the branch name is either left as "virtual branch" diff --git a/apps/desktop/src/lib/file/BranchFilesHeader.svelte b/apps/desktop/src/lib/file/BranchFilesHeader.svelte index d589f80db..6b7a32dc5 100644 --- a/apps/desktop/src/lib/file/BranchFilesHeader.svelte +++ b/apps/desktop/src/lib/file/BranchFilesHeader.svelte @@ -1,6 +1,6 @@ diff --git a/apps/desktop/src/lib/vbranches/ownership.ts b/apps/desktop/src/lib/vbranches/ownership.ts index cc547c09b..b3f8380e8 100644 --- a/apps/desktop/src/lib/vbranches/ownership.ts +++ b/apps/desktop/src/lib/vbranches/ownership.ts @@ -23,48 +23,149 @@ export type FilePath = string; export type HunkClaims = Map; export type FileClaims = Map; -export class Ownership { +function branchFilesToClaims(files: AnyFile[]): FileClaims { + const selection = new Map(); + for (const file of files) { + const existingFile = selection.get(file.id); + if (existingFile) { + file.hunks.forEach((hunk) => existingFile.set(hunk.id, hunk)); + continue; + } + + selection.set( + file.id, + file.hunks.reduce((acc, hunk) => { + return acc.set(hunk.id, hunk); + }, new Map()) + ); + } + + return selection; +} + +function selectAddedClaims( + branch: VirtualBranch, + previousState: SelectedOwnershipState, + selection: Map +) { + for (const file of branch.files) { + const existingFile = previousState.claims.get(file.id); + + if (!existingFile) { + // Select newly added files + selection.set( + file.id, + file.hunks.reduce((acc, hunk) => { + return acc.set(hunk.id, hunk); + }, new Map()) + ); + continue; + } + + for (const hunk of file.hunks) { + const existingHunk = existingFile.get(hunk.id); + if (!existingHunk) { + // Select newly added hunks + const existingFile = selection.get(file.id); + if (existingFile) { + existingFile.set(hunk.id, hunk); + } else { + selection.set(file.id, new Map([[hunk.id, hunk]])); + } + } + } + } +} + +function ignoreRemovedClaims( + previousState: SelectedOwnershipState, + branch: VirtualBranch, + selection: Map +) { + for (const [fileId, hunkClaims] of previousState.selection.entries()) { + const branchFile = branch.files.find((f) => f.id === fileId); + if (branchFile) { + for (const hunkId of hunkClaims.keys()) { + const branchHunk = branchFile.hunks.find((h) => h.id === hunkId); + if (branchHunk) { + // Re-select hunks that are still present in the branch + const existingFile = selection.get(fileId); + if (existingFile) { + existingFile.set(hunkId, branchHunk); + } else { + selection.set(fileId, new Map([[hunkId, branchHunk]])); + } + } + } + } + } +} + +interface SelectedOwnershipState { + claims: FileClaims; + selection: FileClaims; +} + +function getState( + branch: VirtualBranch, + previousState?: SelectedOwnershipState +): SelectedOwnershipState { + const claims = branchFilesToClaims(branch.files); + + if (previousState !== undefined) { + const selection = new Map(); + selectAddedClaims(branch, previousState, selection); + ignoreRemovedClaims(previousState, branch, selection); + + return { selection, claims }; + } + + return { selection: claims, claims }; +} + +export class SelectedOwnership { private claims: FileClaims; + private selection: FileClaims; + + constructor(state: SelectedOwnershipState) { + this.claims = state.claims; + this.selection = state.selection; + } static fromBranch(branch: VirtualBranch) { - const files = branch.files.reduce((acc, file) => { - const existing = acc.get(file.id); - if (existing) { - file.hunks.forEach((hunk) => existing.set(hunk.id, hunk)); - } else { - acc.set( - file.id, - file.hunks.reduce((acc2, hunk) => { - return acc2.set(hunk.id, hunk); - }, new Map()) - ); - } - return acc; - }, new Map>()); - const ownership = new Ownership(files); + const state = getState(branch); + const ownership = new SelectedOwnership(state); return ownership; } - constructor(files: FileClaims) { - this.claims = files; + update(branch: VirtualBranch) { + const { selection, claims } = getState(branch, { + claims: this.claims, + selection: this.selection + }); + + this.claims = claims; + this.selection = selection; + + return this; } - remove(fileId: string, ...hunkIds: string[]) { - const claims = this.claims; - if (!claims) return this; + ignore(fileId: string, ...hunkIds: string[]) { + const selection = this.selection; + if (!selection) return this; hunkIds.forEach((hunkId) => { - claims.get(fileId)?.delete(hunkId); - if (claims.get(fileId)?.size === 0) claims.delete(fileId); + selection.get(fileId)?.delete(hunkId); + if (selection.get(fileId)?.size === 0) selection.delete(fileId); }); return this; } - add(fileId: string, ...items: AnyHunk[]) { - const claim = this.claims.get(fileId); - if (claim) { - items.forEach((hunk) => claim.set(hunk.id, hunk)); + select(fileId: string, ...items: AnyHunk[]) { + const selectedFile = this.selection.get(fileId); + if (selectedFile) { + items.forEach((hunk) => selectedFile.set(hunk.id, hunk)); } else { - this.claims.set( + this.selection.set( fileId, items.reduce((acc, hunk) => { return acc.set(hunk.id, hunk); @@ -74,17 +175,17 @@ export class Ownership { return this; } - contains(fileId: string, ...hunkIds: string[]): boolean { - return hunkIds.every((hunkId) => !!this.claims.get(fileId)?.has(hunkId)); + isSelected(fileId: string, ...hunkIds: string[]): boolean { + return hunkIds.every((hunkId) => !!this.selection.get(fileId)?.has(hunkId)); } - clear() { - this.claims.clear(); + clearSelection() { + this.selection.clear(); return this; } toString() { - return Array.from(this.claims.entries()) + return Array.from(this.selection.entries()) .map( ([fileId, hunkMap]) => fileId + @@ -98,7 +199,7 @@ export class Ownership { .join('\n'); } - isEmpty() { - return this.claims.size === 0; + nothingSelected() { + return this.selection.size === 0; } } From aaae6e605c9ea944fb9b807005a66c119730fca4 Mon Sep 17 00:00:00 2001 From: estib Date: Tue, 10 Sep 2024 19:52:35 +0200 Subject: [PATCH 07/43] HunkDiff: Udpate the diff table style --- apps/desktop/src/lib/hunk/HunkDiff.svelte | 235 ++++++++++++++++++--- apps/desktop/src/lib/hunk/types.ts | 1 + apps/desktop/src/lib/utils/fileSections.ts | 5 + 3 files changed, 210 insertions(+), 31 deletions(-) diff --git a/apps/desktop/src/lib/hunk/HunkDiff.svelte b/apps/desktop/src/lib/hunk/HunkDiff.svelte index eba2c1d31..26d3d7cfc 100644 --- a/apps/desktop/src/lib/hunk/HunkDiff.svelte +++ b/apps/desktop/src/lib/hunk/HunkDiff.svelte @@ -4,9 +4,15 @@ import ScrollableContainer from '$lib/scroll/ScrollableContainer.svelte'; import { create } from '$lib/utils/codeHighlight'; import { maybeGetContextStore } from '$lib/utils/context'; - import { type ContentSection, SectionType, type Line } from '$lib/utils/fileSections'; + import { + type ContentSection, + SectionType, + type Line, + CountColumnSide + } from '$lib/utils/fileSections'; import { SelectedOwnership } from '$lib/vbranches/ownership'; import { type Hunk } from '$lib/vbranches/types'; + import Checkbox from '@gitbutler/ui/Checkbox.svelte'; import Icon from '@gitbutler/ui/Icon.svelte'; import diff_match_patch from 'diff-match-patch'; import type { Writable } from 'svelte/store'; @@ -55,6 +61,8 @@ const selectedOwnership: Writable | undefined = maybeGetContextStore(SelectedOwnership); + let tableWidth = $state(0); + const selected = $derived($selectedOwnership?.isSelected(hunk.filePath, hunk.id) ?? false); let isSelected = $derived(selectable && selected); @@ -88,7 +96,8 @@ afterLineNumber: line.afterLineNumber, tokens: toTokens(line.content), type: section.sectionType, - size: line.content.length + size: line.content.length, + isLast: false }; }); } @@ -130,14 +139,16 @@ afterLineNumber: oldLine.afterLineNumber, tokens: [] as string[], type: prevSection.sectionType, - size: oldLine.content.length + size: oldLine.content.length, + isLast: false }; const nextSectionRow = { beforeLineNumber: newLine.beforeLineNumber, afterLineNumber: newLine.afterLineNumber, tokens: [] as string[], type: nextSection.sectionType, - size: newLine.content.length + size: newLine.content.length, + isLast: false }; const diff = charDiff(oldLine.content, newLine.content); @@ -182,7 +193,8 @@ afterLineNumber: newLine.afterLineNumber, tokens: [] as string[], type: nextSection.sectionType, - size: newLine.content.length + size: newLine.content.length, + isLast: false }; const diff = charDiff(oldLine.content, newLine.content); @@ -210,7 +222,7 @@ } function generateRows(subsections: ContentSection[]) { - return subsections.reduce((acc, nextSection, i) => { + const rows = subsections.reduce((acc, nextSection, i) => { const prevSection = subsections[i - 1]; // Filter out section for which we don't need to compute word diffs @@ -255,59 +267,130 @@ return acc; } }, [] as Row[]); + + const last = rows.at(-1); + if (last) { + last.isLast = true; + } + + return rows; } const renderRows = $derived(generateRows(subsections)); + + interface DiffHunkLineInfo { + beforLineStart: number; + beforeLineCount: number; + afterLineStart: number; + afterLineCount: number; + } + + function getHunkLineInfo(subsections: ContentSection[]): DiffHunkLineInfo { + const firstSection = subsections[0]; + const lastSection = subsections.at(-1); + + const beforLineStart = firstSection?.lines[0]?.beforeLineNumber ?? 0; + const beforeLineEnd = lastSection?.lines?.at(-1)?.beforeLineNumber ?? 0; + const beforeLineCount = beforeLineEnd - beforLineStart + 1; + + const afterLineStart = firstSection?.lines[0]?.afterLineNumber ?? 0; + const afterLineEnd = lastSection?.lines?.at(-1)?.afterLineNumber ?? 0; + const afterLineCount = afterLineEnd - afterLineStart + 1; + + return { + beforLineStart, + beforeLineCount, + afterLineStart, + afterLineCount + }; + } + + const hunkLineInfo = $derived(getHunkLineInfo(subsections)); -{#snippet countColumn(count: number | undefined, lineType: SectionType)} +{#snippet countColumn(row: Row, side: CountColumnSide)} { selectable && handleSelected(hunk, !isSelected); }} > - {count} + {side === CountColumnSide.Before ? row.beforeLineNumber : row.afterLineNumber} {/snippet}
- {#if !draggingDisabled} -
- -
- {/if} + + + + + + - {#each renderRows as line} + {#each renderRows as row} - {@render countColumn(line.beforeLineNumber, line.type)} - {@render countColumn(line.afterLineNumber, line.type)} + {@render countColumn(row, CountColumnSide.Before)} + {@render countColumn(row, CountColumnSide.After)} {/each} @@ -318,7 +401,6 @@ diff --git a/apps/desktop/src/lib/hunk/types.ts b/apps/desktop/src/lib/hunk/types.ts index 30a5934d7..540bcfea9 100644 --- a/apps/desktop/src/lib/hunk/types.ts +++ b/apps/desktop/src/lib/hunk/types.ts @@ -6,6 +6,7 @@ export interface Row { tokens: string[]; type: SectionType; size: number; + isLast: boolean; } export enum Operation { diff --git a/apps/desktop/src/lib/utils/fileSections.ts b/apps/desktop/src/lib/utils/fileSections.ts index a431e2274..994a34d8f 100644 --- a/apps/desktop/src/lib/utils/fileSections.ts +++ b/apps/desktop/src/lib/utils/fileSections.ts @@ -21,6 +21,11 @@ export enum SectionType { Context } +export enum CountColumnSide { + Before, + After +} + export class HunkSection { hunk!: Hunk; header!: HunkHeader; From f5477a502b5afd460da5171c8293162743771b1e Mon Sep 17 00:00:00 2001 From: Caleb Owens Date: Tue, 10 Sep 2024 22:35:02 +0200 Subject: [PATCH 08/43] Issues --- apps/desktop/src/lib/config/uiFeatureFlags.ts | 5 + apps/desktop/src/lib/gitHost/azure/azure.ts | 4 + .../src/lib/gitHost/bitbucket/bitbucket.ts | 4 + apps/desktop/src/lib/gitHost/github/github.ts | 8 + .../src/lib/gitHost/github/issueService.ts | 29 ++++ apps/desktop/src/lib/gitHost/gitlab/gitlab.ts | 4 + .../src/lib/gitHost/interface/gitHost.ts | 3 + .../gitHost/interface/gitHostIssueService.ts | 4 + .../src/lib/navigation/Navigation.svelte | 8 + .../src/lib/navigation/TopicsButton.svelte | 34 ++++ .../src/lib/topics/CreateIssueModal.svelte | 149 ++++++++++++++++++ .../src/lib/topics/CreateTopicModal.svelte | 85 ++++++++++ apps/desktop/src/lib/topics/Topic.svelte | 116 ++++++++++++++ apps/desktop/src/lib/topics/service.ts | 51 ++++++ .../src/routes/[projectId]/+layout.svelte | 22 +++ .../routes/[projectId]/topics/+page.svelte | 59 +++++++ .../routes/settings/experimental/+page.svelte | 18 ++- 17 files changed, 602 insertions(+), 1 deletion(-) create mode 100644 apps/desktop/src/lib/gitHost/github/issueService.ts create mode 100644 apps/desktop/src/lib/gitHost/interface/gitHostIssueService.ts create mode 100644 apps/desktop/src/lib/navigation/TopicsButton.svelte create mode 100644 apps/desktop/src/lib/topics/CreateIssueModal.svelte create mode 100644 apps/desktop/src/lib/topics/CreateTopicModal.svelte create mode 100644 apps/desktop/src/lib/topics/Topic.svelte create mode 100644 apps/desktop/src/lib/topics/service.ts create mode 100644 apps/desktop/src/routes/[projectId]/topics/+page.svelte diff --git a/apps/desktop/src/lib/config/uiFeatureFlags.ts b/apps/desktop/src/lib/config/uiFeatureFlags.ts index f72b07f71..ba010686d 100644 --- a/apps/desktop/src/lib/config/uiFeatureFlags.ts +++ b/apps/desktop/src/lib/config/uiFeatureFlags.ts @@ -20,3 +20,8 @@ export function featureBranchStacking(): Persisted { const key = 'branchStacking'; return persisted(false, key); } + +export function featureTopics(): Persisted { + const key = 'feature--topics'; + return persisted(false, key); +} diff --git a/apps/desktop/src/lib/gitHost/azure/azure.ts b/apps/desktop/src/lib/gitHost/azure/azure.ts index 0e905402b..6e065f6d7 100644 --- a/apps/desktop/src/lib/gitHost/azure/azure.ts +++ b/apps/desktop/src/lib/gitHost/azure/azure.ts @@ -36,6 +36,10 @@ export class AzureDevOps implements GitHost { return undefined; } + issueService() { + return undefined; + } + prService() { return undefined; } diff --git a/apps/desktop/src/lib/gitHost/bitbucket/bitbucket.ts b/apps/desktop/src/lib/gitHost/bitbucket/bitbucket.ts index 88be72da5..4d0e39478 100644 --- a/apps/desktop/src/lib/gitHost/bitbucket/bitbucket.ts +++ b/apps/desktop/src/lib/gitHost/bitbucket/bitbucket.ts @@ -40,6 +40,10 @@ export class BitBucket implements GitHost { return undefined; } + issueService() { + return undefined; + } + prService() { return undefined; } diff --git a/apps/desktop/src/lib/gitHost/github/github.ts b/apps/desktop/src/lib/gitHost/github/github.ts index f35ed7e60..fbe525d3e 100644 --- a/apps/desktop/src/lib/gitHost/github/github.ts +++ b/apps/desktop/src/lib/gitHost/github/github.ts @@ -2,6 +2,7 @@ import { GitHubBranch } from './githubBranch'; import { GitHubChecksMonitor } from './githubChecksMonitor'; import { GitHubListingService } from './githubListingService'; import { GitHubPrService } from './githubPrService'; +import { GitHubIssueService } from '$lib/gitHost/github/issueService'; import { Octokit } from '@octokit/rest'; import type { ProjectMetrics } from '$lib/metrics/projectMetrics'; import type { Persisted } from '$lib/persisted/persisted'; @@ -64,6 +65,13 @@ export class GitHub implements GitHost { ); } + issueService() { + if (!this.octokit) { + return; + } + return new GitHubIssueService(this.octokit, this.repo); + } + checksMonitor(sourceBranch: string) { if (!this.octokit) { return; diff --git a/apps/desktop/src/lib/gitHost/github/issueService.ts b/apps/desktop/src/lib/gitHost/github/issueService.ts new file mode 100644 index 000000000..869be6ca3 --- /dev/null +++ b/apps/desktop/src/lib/gitHost/github/issueService.ts @@ -0,0 +1,29 @@ +import type { GitHostIssueService } from '$lib/gitHost/interface/gitHostIssueService'; +import type { RepoInfo } from '$lib/url/gitUrl'; +import type { Octokit } from '@octokit/rest'; + +export class GitHubIssueService implements GitHostIssueService { + constructor( + private octokit: Octokit, + private repository: RepoInfo + ) {} + + async create(title: string, body: string, labels: string[]): Promise { + await this.octokit.rest.issues.create({ + repo: this.repository.name, + owner: this.repository.owner, + title, + body, + labels + }); + } + + async listLabels(): Promise { + return ( + await this.octokit.rest.issues.listLabelsForRepo({ + repo: this.repository.name, + owner: this.repository.owner + }) + ).data.map((label) => label.name); + } +} diff --git a/apps/desktop/src/lib/gitHost/gitlab/gitlab.ts b/apps/desktop/src/lib/gitHost/gitlab/gitlab.ts index b078a7226..6b269a6b9 100644 --- a/apps/desktop/src/lib/gitHost/gitlab/gitlab.ts +++ b/apps/desktop/src/lib/gitHost/gitlab/gitlab.ts @@ -41,6 +41,10 @@ export class GitLab implements GitHost { return undefined; } + issueService() { + return undefined; + } + prService() { return undefined; } diff --git a/apps/desktop/src/lib/gitHost/interface/gitHost.ts b/apps/desktop/src/lib/gitHost/interface/gitHost.ts index 1e343ccec..fa892ca17 100644 --- a/apps/desktop/src/lib/gitHost/interface/gitHost.ts +++ b/apps/desktop/src/lib/gitHost/interface/gitHost.ts @@ -1,4 +1,5 @@ import { buildContextStore } from '$lib/utils/context'; +import type { GitHostIssueService } from '$lib/gitHost/interface/gitHostIssueService'; import type { GitHostBranch } from './gitHostBranch'; import type { GitHostChecksMonitor } from './gitHostChecksMonitor'; import type { GitHostListingService } from './gitHostListingService'; @@ -8,6 +9,8 @@ export interface GitHost { // Lists PRs for the repo. listService(): GitHostListingService | undefined; + issueService(): GitHostIssueService | undefined; + // Detailed information about a specific PR. prService(): GitHostPrService | undefined; diff --git a/apps/desktop/src/lib/gitHost/interface/gitHostIssueService.ts b/apps/desktop/src/lib/gitHost/interface/gitHostIssueService.ts new file mode 100644 index 000000000..299ec6c26 --- /dev/null +++ b/apps/desktop/src/lib/gitHost/interface/gitHostIssueService.ts @@ -0,0 +1,4 @@ +export interface GitHostIssueService { + create(title: string, body: string, labels: string[]): Promise; + listLabels(): Promise; +} diff --git a/apps/desktop/src/lib/navigation/Navigation.svelte b/apps/desktop/src/lib/navigation/Navigation.svelte index 74508be6c..950afec27 100644 --- a/apps/desktop/src/lib/navigation/Navigation.svelte +++ b/apps/desktop/src/lib/navigation/Navigation.svelte @@ -6,8 +6,10 @@ import WorkspaceButton from './WorkspaceButton.svelte'; import Resizer from '../shared/Resizer.svelte'; import { Project } from '$lib/backend/projects'; + import { featureTopics } from '$lib/config/uiFeatureFlags'; import { ModeService } from '$lib/modes/service'; import EditButton from '$lib/navigation/EditButton.svelte'; + import TopicsButton from '$lib/navigation/TopicsButton.svelte'; import { persisted } from '$lib/persisted/persisted'; import { platformName } from '$lib/platform/platform'; import { SETTINGS, type Settings } from '$lib/settings/userSettings'; @@ -43,6 +45,8 @@ const modeService = getContext(ModeService); const mode = modeService.mode; + + const topicsEnabled = featureTopics(); @@ -120,6 +124,10 @@ {:else if $mode?.type === 'Edit'} {/if} + + {#if $topicsEnabled} + + {/if} diff --git a/apps/desktop/src/lib/navigation/TopicsButton.svelte b/apps/desktop/src/lib/navigation/TopicsButton.svelte new file mode 100644 index 000000000..2aba62cf4 --- /dev/null +++ b/apps/desktop/src/lib/navigation/TopicsButton.svelte @@ -0,0 +1,34 @@ + + + await goto(href)} +> + + {#if !isNavCollapsed} + {label} + {/if} + + + diff --git a/apps/desktop/src/lib/topics/CreateIssueModal.svelte b/apps/desktop/src/lib/topics/CreateIssueModal.svelte new file mode 100644 index 000000000..5722c8ce0 --- /dev/null +++ b/apps/desktop/src/lib/topics/CreateIssueModal.svelte @@ -0,0 +1,149 @@ + + + + +{#if issueService} + +

Create an issue

+ +
+

Title

+ +
+ +
+

Body

+ {:else} -
+
{/if} diff --git a/apps/desktop/src/lib/components/BranchPreview.svelte b/apps/desktop/src/lib/components/BranchPreview.svelte index 71d6b032b..fbdcb3a82 100644 --- a/apps/desktop/src/lib/components/BranchPreview.svelte +++ b/apps/desktop/src/lib/components/BranchPreview.svelte @@ -108,7 +108,7 @@
{pr.title}
{#if pr.body} -
+
{/if} diff --git a/apps/desktop/src/lib/components/Markdown.svelte b/apps/desktop/src/lib/components/Markdown.svelte index 981341c3f..c91b7a512 100644 --- a/apps/desktop/src/lib/components/Markdown.svelte +++ b/apps/desktop/src/lib/components/Markdown.svelte @@ -15,14 +15,8 @@ }); -
+
{#if tokens} {/if}
- - diff --git a/apps/desktop/src/lib/components/PullRequestPreview.svelte b/apps/desktop/src/lib/components/PullRequestPreview.svelte index 83f6e5450..189111726 100644 --- a/apps/desktop/src/lib/components/PullRequestPreview.svelte +++ b/apps/desktop/src/lib/components/PullRequestPreview.svelte @@ -127,9 +127,7 @@
{#if pullrequest.body} -
- -
+ {/if}
-
-

Body

-
{ + selectable && handleSelected(hunk, !isSelected); + }} + > +
+ { + selectable && handleSelected(hunk, !isSelected); + }} + /> +
+
+

+ {`@@ -${hunkLineInfo.beforLineStart},${hunkLineInfo.beforeLineCount} +${hunkLineInfo.afterLineStart},${hunkLineInfo.afterLineCount} @@`} +

+ {#if !draggingDisabled} +
+ +
+ {/if} +
+
{ - const lineNumber = (line.beforeLineNumber - ? line.beforeLineNumber - : line.afterLineNumber) as number; + const lineNumber = (row.beforeLineNumber + ? row.beforeLineNumber + : row.afterLineNumber) as number; handleLineContextMenu({ event, hunk, lineNumber, subsection: subsections[0] as ContentSection }); }} > - {@html line.tokens.join('')} + {@html row.tokens.join('')}