Skip to content

Commit e7752d1

Browse files
authored
Add support for GitHub projects (#108)
2 parents a40f81a + 1ff5c54 commit e7752d1

File tree

14 files changed

+1829
-22
lines changed

14 files changed

+1829
-22
lines changed

.claude/commands/corpus-loop.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ description: uses Playwright MCP and the `corpus` to parse page elements
3333
- this output means that this page is simulating the url `https://github.com/diffplug/selfie/issues/523`
3434
- every textarea on the page is represented
3535
- `NO_SPOT` means that the spot was not enhanced
36-
- `type: GH_ISSUE_ADD_COMMENT` means that it was enhanced by whichever implementation of `CommentEnhancer` returns the spot type `GH_ISSUE_ADD_COMMENT`
36+
- `type: GH_ISSUE_APPEND` means that it was enhanced by whichever implementation of `CommentEnhancer` returns the spot type `GH_ISSUE_APPEND`
3737
- if you search for that string in `src/lib/enhancers` you will find the correct one
3838
- the `tryToEnhance` method returned a `CommentSpot`, and that whole data is splatted out above
3939

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
55
Version must be kept in-sync between [`package.json`](package.json) and [`wxt.config.js`](wxt.config.ts).
66

77
## [Unreleased]
8+
### Added
9+
- Support for GitHub projects (draft and "real" issues). ([#108](https://github.com/diffplug/gitcasso/pull/108))
810
### Fixed
911
- Appending to GitHub issues was not being enhanced, now fixed. ([#105](https://github.com/diffplug/gitcasso/issues/105))
1012
- Reduced unnecessary permissions (no need for `host_permissions`)

src/entrypoints/content.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ function detectLocation(): StrippedLocation {
1515
const result = {
1616
host: window.location.host,
1717
pathname: window.location.pathname,
18+
search: window.location.search,
1819
}
1920
logger.debug("[gitcasso] detectLocation called, returning:", result)
2021
return result

src/lib/enhancer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface CommentEvent {
2626
export interface StrippedLocation {
2727
host: string
2828
pathname: string
29+
search: string
2930
}
3031

3132
/** Wraps the textareas of a given platform with Gitcasso's enhancements. */

src/lib/enhancers/github/GitHubEditEnhancer.tsx

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@ import type {
77
} from "@/lib/enhancer"
88
import { logger } from "@/lib/logger"
99
import { fixupOvertype, modifyDOM } from "../overtype-misc"
10-
import { commonGitHubOptions, prepareGitHubHighlighter } from "./github-common"
10+
import {
11+
commonGitHubOptions,
12+
isInProjectCommentBox,
13+
isProjectUrl,
14+
parseProjectIssueParam,
15+
prepareGitHubHighlighter,
16+
} from "./github-common"
1117

1218
const GH_EDIT = "GH_EDIT" as const
1319

@@ -29,6 +35,50 @@ export class GitHubEditEnhancer implements CommentEnhancer<GitHubEditSpot> {
2935
return null
3036
}
3137

38+
// Check for project draft edit first
39+
if (isProjectUrl(location.pathname)) {
40+
const params = new URLSearchParams(location.search)
41+
const itemId = params.get("itemId")
42+
43+
// Handle draft editing (itemId parameter)
44+
if (itemId) {
45+
// Exclude textareas within Shared-module__CommentBox (those are for adding new comments, not editing)
46+
if (!isInProjectCommentBox(textarea)) {
47+
const unique_key = `github.com:project-draft:${itemId}:edit-body`
48+
logger.debug(
49+
`${this.constructor.name} enhanced project draft body textarea`,
50+
unique_key
51+
)
52+
return {
53+
isIssue: true,
54+
type: GH_EDIT,
55+
unique_key,
56+
}
57+
}
58+
}
59+
60+
// Handle existing issue comment editing (issue parameter)
61+
const issueInfo = parseProjectIssueParam(params)
62+
if (issueInfo) {
63+
// Edit mode: empty placeholder
64+
// Add new comment mode: has placeholder "Add your comment here..." or similar
65+
if (!textarea.placeholder || textarea.placeholder.trim() === "") {
66+
const unique_key = `github.com:${issueInfo.slug}:${issueInfo.number}:edit-comment`
67+
logger.debug(
68+
`${this.constructor.name} enhanced project issue comment edit textarea`,
69+
unique_key
70+
)
71+
return {
72+
isIssue: true,
73+
type: GH_EDIT,
74+
unique_key,
75+
}
76+
}
77+
}
78+
79+
return null
80+
}
81+
3282
// Parse GitHub URL structure: /owner/repo/issues/123 or /owner/repo/pull/456
3383
const match = location.pathname.match(
3484
/^\/([^/]+)\/([^/]+)\/(?:issues|pull)\/(\d+)/
@@ -46,9 +96,8 @@ export class GitHubEditEnhancer implements CommentEnhancer<GitHubEditSpot> {
4696
"[data-wrapper-timeline-id]"
4797
)
4898
const isPRBodyEdit =
49-
textarea.name === "pull_request[body]" ||
50-
textarea.name === "issue_comment[body]"
51-
// ^this is the root pr comment ^this is the other pr comments (surprising!)
99+
textarea.name === "pull_request[body]" || // this is the root pr comment
100+
textarea.name === "issue_comment[body]" // this is the other pr comments (surprising!)
52101

53102
if (!isIssueBodyRootEdit && !isIssueBodyCommentEdit && !isPRBodyEdit) {
54103
return null

src/lib/enhancers/github/GitHubIssueAppendEnhancer.tsx

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@ import type {
99
} from "@/lib/enhancer"
1010
import { logger } from "@/lib/logger"
1111
import { fixupOvertype, modifyDOM } from "../overtype-misc"
12-
import { commonGitHubOptions, prepareGitHubHighlighter } from "./github-common"
12+
import {
13+
commonGitHubOptions,
14+
isInProjectCommentBox,
15+
isProjectUrl,
16+
parseProjectIssueParam,
17+
prepareGitHubHighlighter,
18+
} from "./github-common"
1319

1420
const GH_ISSUE_APPEND = "GH_ISSUE_APPEND" as const
1521

@@ -45,6 +51,32 @@ export class GitHubIssueAppendEnhancer
4551
return null
4652
}
4753

54+
// Check for project URLs with issue parameter first
55+
if (isProjectUrl(location.pathname)) {
56+
const params = new URLSearchParams(location.search)
57+
// Only match textareas within Shared-module__CommentBox (those are for adding new comments)
58+
if (isInProjectCommentBox(textarea)) {
59+
const issueInfo = parseProjectIssueParam(params)
60+
if (issueInfo) {
61+
const unique_key = `github.com:${issueInfo.slug}:${issueInfo.number}`
62+
// For project views, the title is in the side panel dialog
63+
const title =
64+
document
65+
.querySelector('[data-testid="issue-title"]')
66+
?.textContent?.trim() || ""
67+
return {
68+
domain: location.host,
69+
number: issueInfo.number,
70+
slug: issueInfo.slug,
71+
title,
72+
type: GH_ISSUE_APPEND,
73+
unique_key,
74+
}
75+
}
76+
}
77+
return null
78+
}
79+
4880
// Parse GitHub URL structure: /owner/repo/issues/123 or /owner/repo/pull/456
4981
logger.debug(`${this.constructor.name} examing url`, location.pathname)
5082

src/lib/enhancers/github/GitHubIssueCreateEnhancer.tsx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import type {
88
} from "../../enhancer"
99
import { logger } from "../../logger"
1010
import { fixupOvertype, modifyDOM } from "../overtype-misc"
11-
import { commonGitHubOptions, prepareGitHubHighlighter } from "./github-common"
11+
import {
12+
commonGitHubOptions,
13+
isProjectUrl,
14+
prepareGitHubHighlighter,
15+
} from "./github-common"
1216

1317
const GH_ISSUE_CREATE = "GH_ISSUE_CREATE" as const
1418

@@ -37,6 +41,32 @@ export class GitHubIssueCreateEnhancer
3741
return null
3842
}
3943

44+
// Check for project board URLs first
45+
if (isProjectUrl(location.pathname)) {
46+
// Check if we're in a "Create new issue" dialog
47+
const dialog = textarea.closest('[role="dialog"]')
48+
if (dialog) {
49+
const dialogHeading = dialog.querySelector("h1")?.textContent
50+
const slugMatch = dialogHeading?.match(/Create new issue in (.+)/)
51+
if (slugMatch) {
52+
const slug = slugMatch[1]!
53+
const unique_key = `github.com:${slug}:new`
54+
const titleInput = document.querySelector(
55+
'input[placeholder="Title"]'
56+
) as HTMLInputElement
57+
const title = titleInput?.value || ""
58+
return {
59+
domain: location.host,
60+
slug,
61+
title,
62+
type: GH_ISSUE_CREATE,
63+
unique_key,
64+
}
65+
}
66+
}
67+
return null
68+
}
69+
4070
// Parse GitHub URL structure: /owner/repo/issues/123 or /owner/repo/pull/456
4171
logger.debug(`${this.constructor.name} examing url`, location.pathname)
4272

src/lib/enhancers/github/github-common.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,49 @@ function githubHighlighter(code: string, language?: string) {
6161
return escapeHtml(code)
6262
}
6363
}
64+
65+
// Project-related helper functions
66+
67+
/**
68+
* Check if the pathname matches a GitHub project URL pattern.
69+
* Matches: /orgs/{org}/projects/{id} or /users/{user}/projects/{id}
70+
* Optional: /views/{viewId} suffix
71+
*/
72+
export function isProjectUrl(pathname: string): boolean {
73+
return /^\/(?:orgs|users)\/[^/]+\/projects\/\d+(?:\/views\/\d+)?/.test(
74+
pathname
75+
)
76+
}
77+
78+
/**
79+
* Parse the issue parameter from project URLs.
80+
* Format: ?issue=owner|repo|number
81+
* Returns: { slug: "owner/repo", number: 123 } or null if invalid
82+
*/
83+
export function parseProjectIssueParam(
84+
searchParams: URLSearchParams
85+
): { slug: string; number: number } | null {
86+
const issueParam = searchParams.get("issue")
87+
if (!issueParam) return null
88+
89+
const parts = issueParam.split("|")
90+
if (parts.length !== 3) return null
91+
92+
const [owner, repo, numberStr] = parts
93+
const number = parseInt(numberStr!, 10)
94+
95+
if (Number.isNaN(number)) return null
96+
97+
return {
98+
slug: `${owner}/${repo}`,
99+
number,
100+
}
101+
}
102+
103+
/**
104+
* Check if an element is within a project CommentBox container.
105+
* CommentBox containers are used for adding new comments (not editing).
106+
*/
107+
export function isInProjectCommentBox(element: HTMLElement): boolean {
108+
return !!element.closest('[class*="Shared-module__CommentBox"]')
109+
}

tests/corpus-fixture.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export function detectedSpots() {
6666
const location: StrippedLocation = {
6767
host: window.location.host,
6868
pathname: window.location.pathname,
69+
search: window.location.search,
6970
}
7071
const detectionResults = []
7172
for (const textarea of textareas) {
@@ -86,6 +87,7 @@ export function tableUI() {
8687
const location: StrippedLocation = {
8788
host: window.location.host,
8889
pathname: window.location.pathname,
90+
search: window.location.search,
8991
}
9092
const uiResults = []
9193
for (const textarea of textareas) {

tests/corpus-view.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ function getUrlParts(key: string) {
9797
hostname: url.hostname,
9898
href: originalUrl,
9999
pathname: url.pathname,
100+
search: url.search,
100101
}
101102
}
102103

@@ -562,11 +563,12 @@ function createGitcassoScript(
562563
): string {
563564
const contentScriptSetup = contentScriptCode
564565
? // Direct embedding (for HTML corpus)
565-
`
566+
`
566567
// Set up mocked location
567568
window.gitcassoMockLocation = {
568569
host: '${urlParts.host}',
569-
pathname: '${urlParts.pathname}'
570+
pathname: '${urlParts.pathname}',
571+
search: '${urlParts.search}'
570572
};
571573
572574
// Set up browser API mocks
@@ -589,7 +591,7 @@ function createGitcassoScript(
589591
}
590592
`
591593
: // Fetch-based loading (for HAR corpus)
592-
`
594+
`
593595
// Fetch and patch the content script to remove webextension-polyfill issues
594596
fetch('/chrome-mv3-dev/content-scripts/content.js')
595597
.then(response => response.text())
@@ -603,7 +605,8 @@ function createGitcassoScript(
603605
);
604606
window.gitcassoMockLocation = {
605607
host: '${urlParts.host}',
606-
pathname: '${urlParts.pathname}'
608+
pathname: '${urlParts.pathname}',
609+
search: '${urlParts.search}'
607610
};
608611
609612
// Execute the patched script with browser API mocks prepended

0 commit comments

Comments
 (0)