Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/commands/corpus-loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ description: uses Playwright MCP and the `corpus` to parse page elements
- this output means that this page is simulating the url `https://github.com/diffplug/selfie/issues/523`
- every textarea on the page is represented
- `NO_SPOT` means that the spot was not enhanced
- `type: GH_ISSUE_ADD_COMMENT` means that it was enhanced by whichever implementation of `CommentEnhancer` returns the spot type `GH_ISSUE_ADD_COMMENT`
- `type: GH_ISSUE_APPEND` means that it was enhanced by whichever implementation of `CommentEnhancer` returns the spot type `GH_ISSUE_APPEND`
- if you search for that string in `src/lib/enhancers` you will find the correct one
- the `tryToEnhance` method returned a `CommentSpot`, and that whole data is splatted out above

Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Version must be kept in-sync between [`package.json`](package.json) and [`wxt.config.js`](wxt.config.ts).

## [Unreleased]
### Added
- Support for GitHub projects (draft and "real" issues). ([#108](https://github.com/diffplug/gitcasso/pull/108))
### Fixed
- Appending to GitHub issues was not being enhanced, now fixed. ([#105](https://github.com/diffplug/gitcasso/issues/105))
- Reduced unnecessary permissions (no need for `host_permissions`)
Expand Down
1 change: 1 addition & 0 deletions src/entrypoints/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ function detectLocation(): StrippedLocation {
const result = {
host: window.location.host,
pathname: window.location.pathname,
search: window.location.search,
}
logger.debug("[gitcasso] detectLocation called, returning:", result)
return result
Expand Down
1 change: 1 addition & 0 deletions src/lib/enhancer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface CommentEvent {
export interface StrippedLocation {
host: string
pathname: string
search: string
}

/** Wraps the textareas of a given platform with Gitcasso's enhancements. */
Expand Down
57 changes: 53 additions & 4 deletions src/lib/enhancers/github/GitHubEditEnhancer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ import type {
} from "@/lib/enhancer"
import { logger } from "@/lib/logger"
import { fixupOvertype, modifyDOM } from "../overtype-misc"
import { commonGitHubOptions, prepareGitHubHighlighter } from "./github-common"
import {
commonGitHubOptions,
isInProjectCommentBox,
isProjectUrl,
parseProjectIssueParam,
prepareGitHubHighlighter,
} from "./github-common"

const GH_EDIT = "GH_EDIT" as const

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

// Check for project draft edit first
if (isProjectUrl(location.pathname)) {
const params = new URLSearchParams(location.search)
const itemId = params.get("itemId")

// Handle draft editing (itemId parameter)
if (itemId) {
// Exclude textareas within Shared-module__CommentBox (those are for adding new comments, not editing)
if (!isInProjectCommentBox(textarea)) {
const unique_key = `github.com:project-draft:${itemId}:edit-body`
logger.debug(
`${this.constructor.name} enhanced project draft body textarea`,
unique_key
)
return {
isIssue: true,
type: GH_EDIT,
unique_key,
}
}
}

// Handle existing issue comment editing (issue parameter)
const issueInfo = parseProjectIssueParam(params)
if (issueInfo) {
// Edit mode: empty placeholder
// Add new comment mode: has placeholder "Add your comment here..." or similar
if (!textarea.placeholder || textarea.placeholder.trim() === "") {
const unique_key = `github.com:${issueInfo.slug}:${issueInfo.number}:edit-comment`
logger.debug(
`${this.constructor.name} enhanced project issue comment edit textarea`,
unique_key
)
return {
isIssue: true,
type: GH_EDIT,
unique_key,
}
}
}

return null
}

// Parse GitHub URL structure: /owner/repo/issues/123 or /owner/repo/pull/456
const match = location.pathname.match(
/^\/([^/]+)\/([^/]+)\/(?:issues|pull)\/(\d+)/
Expand All @@ -46,9 +96,8 @@ export class GitHubEditEnhancer implements CommentEnhancer<GitHubEditSpot> {
"[data-wrapper-timeline-id]"
)
const isPRBodyEdit =
textarea.name === "pull_request[body]" ||
textarea.name === "issue_comment[body]"
// ^this is the root pr comment ^this is the other pr comments (surprising!)
textarea.name === "pull_request[body]" || // this is the root pr comment
textarea.name === "issue_comment[body]" // this is the other pr comments (surprising!)

if (!isIssueBodyRootEdit && !isIssueBodyCommentEdit && !isPRBodyEdit) {
return null
Expand Down
34 changes: 33 additions & 1 deletion src/lib/enhancers/github/GitHubIssueAppendEnhancer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ import type {
} from "@/lib/enhancer"
import { logger } from "@/lib/logger"
import { fixupOvertype, modifyDOM } from "../overtype-misc"
import { commonGitHubOptions, prepareGitHubHighlighter } from "./github-common"
import {
commonGitHubOptions,
isInProjectCommentBox,
isProjectUrl,
parseProjectIssueParam,
prepareGitHubHighlighter,
} from "./github-common"

const GH_ISSUE_APPEND = "GH_ISSUE_APPEND" as const

Expand Down Expand Up @@ -45,6 +51,32 @@ export class GitHubIssueAppendEnhancer
return null
}

// Check for project URLs with issue parameter first
if (isProjectUrl(location.pathname)) {
const params = new URLSearchParams(location.search)
// Only match textareas within Shared-module__CommentBox (those are for adding new comments)
if (isInProjectCommentBox(textarea)) {
const issueInfo = parseProjectIssueParam(params)
if (issueInfo) {
const unique_key = `github.com:${issueInfo.slug}:${issueInfo.number}`
// For project views, the title is in the side panel dialog
const title =
document
.querySelector('[data-testid="issue-title"]')
?.textContent?.trim() || ""
return {
domain: location.host,
number: issueInfo.number,
slug: issueInfo.slug,
title,
type: GH_ISSUE_APPEND,
unique_key,
}
}
}
return null
}

// Parse GitHub URL structure: /owner/repo/issues/123 or /owner/repo/pull/456
logger.debug(`${this.constructor.name} examing url`, location.pathname)

Expand Down
32 changes: 31 additions & 1 deletion src/lib/enhancers/github/GitHubIssueCreateEnhancer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import type {
} from "../../enhancer"
import { logger } from "../../logger"
import { fixupOvertype, modifyDOM } from "../overtype-misc"
import { commonGitHubOptions, prepareGitHubHighlighter } from "./github-common"
import {
commonGitHubOptions,
isProjectUrl,
prepareGitHubHighlighter,
} from "./github-common"

const GH_ISSUE_CREATE = "GH_ISSUE_CREATE" as const

Expand Down Expand Up @@ -37,6 +41,32 @@ export class GitHubIssueCreateEnhancer
return null
}

// Check for project board URLs first
if (isProjectUrl(location.pathname)) {
// Check if we're in a "Create new issue" dialog
const dialog = textarea.closest('[role="dialog"]')
if (dialog) {
const dialogHeading = dialog.querySelector("h1")?.textContent
const slugMatch = dialogHeading?.match(/Create new issue in (.+)/)
if (slugMatch) {
const slug = slugMatch[1]!
const unique_key = `github.com:${slug}:new`
const titleInput = document.querySelector(
'input[placeholder="Title"]'
) as HTMLInputElement
const title = titleInput?.value || ""
return {
domain: location.host,
slug,
title,
type: GH_ISSUE_CREATE,
unique_key,
}
}
}
return null
}

// Parse GitHub URL structure: /owner/repo/issues/123 or /owner/repo/pull/456
logger.debug(`${this.constructor.name} examing url`, location.pathname)

Expand Down
46 changes: 46 additions & 0 deletions src/lib/enhancers/github/github-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,49 @@ function githubHighlighter(code: string, language?: string) {
return escapeHtml(code)
}
}

// Project-related helper functions

/**
* Check if the pathname matches a GitHub project URL pattern.
* Matches: /orgs/{org}/projects/{id} or /users/{user}/projects/{id}
* Optional: /views/{viewId} suffix
*/
export function isProjectUrl(pathname: string): boolean {
return /^\/(?:orgs|users)\/[^/]+\/projects\/\d+(?:\/views\/\d+)?/.test(
pathname
)
}

/**
* Parse the issue parameter from project URLs.
* Format: ?issue=owner|repo|number
* Returns: { slug: "owner/repo", number: 123 } or null if invalid
*/
export function parseProjectIssueParam(
searchParams: URLSearchParams
): { slug: string; number: number } | null {
const issueParam = searchParams.get("issue")
if (!issueParam) return null

const parts = issueParam.split("|")
if (parts.length !== 3) return null

const [owner, repo, numberStr] = parts
const number = parseInt(numberStr!, 10)

if (Number.isNaN(number)) return null

return {
slug: `${owner}/${repo}`,
number,
}
}

/**
* Check if an element is within a project CommentBox container.
* CommentBox containers are used for adding new comments (not editing).
*/
export function isInProjectCommentBox(element: HTMLElement): boolean {
return !!element.closest('[class*="Shared-module__CommentBox"]')
}
2 changes: 2 additions & 0 deletions tests/corpus-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export function detectedSpots() {
const location: StrippedLocation = {
host: window.location.host,
pathname: window.location.pathname,
search: window.location.search,
}
const detectionResults = []
for (const textarea of textareas) {
Expand All @@ -86,6 +87,7 @@ export function tableUI() {
const location: StrippedLocation = {
host: window.location.host,
pathname: window.location.pathname,
search: window.location.search,
}
const uiResults = []
for (const textarea of textareas) {
Expand Down
11 changes: 7 additions & 4 deletions tests/corpus-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ function getUrlParts(key: string) {
hostname: url.hostname,
href: originalUrl,
pathname: url.pathname,
search: url.search,
}
}

Expand Down Expand Up @@ -562,11 +563,12 @@ function createGitcassoScript(
): string {
const contentScriptSetup = contentScriptCode
? // Direct embedding (for HTML corpus)
`
`
// Set up mocked location
window.gitcassoMockLocation = {
host: '${urlParts.host}',
pathname: '${urlParts.pathname}'
pathname: '${urlParts.pathname}',
search: '${urlParts.search}'
};

// Set up browser API mocks
Expand All @@ -589,7 +591,7 @@ function createGitcassoScript(
}
`
: // Fetch-based loading (for HAR corpus)
`
`
// Fetch and patch the content script to remove webextension-polyfill issues
fetch('/chrome-mv3-dev/content-scripts/content.js')
.then(response => response.text())
Expand All @@ -603,7 +605,8 @@ function createGitcassoScript(
);
window.gitcassoMockLocation = {
host: '${urlParts.host}',
pathname: '${urlParts.pathname}'
pathname: '${urlParts.pathname}',
search: '${urlParts.search}'
};

// Execute the patched script with browser API mocks prepended
Expand Down
5 changes: 5 additions & 0 deletions tests/corpus/_corpus-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,9 @@ export const CORPUS: Record<string, CorpusEntry> = {
type: "html",
url: "https://github.com/orgs/diffplug/projects/12/views/1?pane=issue&itemId=129503239&issue=diffplug%7Cgitcasso%7C57",
},
gh_project_issue_new: {
description: "creating a new issue within a project",
type: "html",
url: "https://github.com/orgs/diffplug/projects/12",
},
} as const
1,503 changes: 1,503 additions & 0 deletions tests/corpus/gh_project_issue_new.html

Large diffs are not rendered by default.

53 changes: 45 additions & 8 deletions tests/lib/enhancers/__snapshots__/gh-detection.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -224,30 +224,67 @@ exports[`github detection > gh_project_draft:should detect correct spots 1`] = `
exports[`github detection > gh_project_draft_edit:should detect correct spots 1`] = `
[
{
"for": "id=:r5a: name=null className=prc-Textarea-TextArea-13q4j",
"spot": "NO_SPOT",
"for": "id=:r5a: name=null className=prc-Textarea-TextArea-13q4j overtype-input",
"spot": {
"isIssue": true,
"type": "GH_EDIT",
"unique_key": "github.com:project-draft:129503329:edit-body",
},
},
]
`;

exports[`github detection > gh_project_issue:should detect correct spots 1`] = `
[
{
"for": "id=:rbs: name=null className=prc-Textarea-TextArea-13q4j",
"spot": "NO_SPOT",
"for": "id=:rbs: name=null className=prc-Textarea-TextArea-13q4j overtype-input",
"spot": {
"domain": "github.com",
"number": 57,
"slug": "diffplug/gitcasso",
"title": "what about drafts?",
"type": "GH_ISSUE_APPEND",
"unique_key": "github.com:diffplug/gitcasso:57",
},
},
]
`;

exports[`github detection > gh_project_issue_edit:should detect correct spots 1`] = `
[
{
"for": "id=:rdh: name=null className=prc-Textarea-TextArea-13q4j",
"spot": "NO_SPOT",
"for": "id=:rdh: name=null className=prc-Textarea-TextArea-13q4j overtype-input",
"spot": {
"isIssue": true,
"type": "GH_EDIT",
"unique_key": "github.com:project-draft:129503239:edit-body",
},
},
{
"for": "id=:rbs: name=null className=prc-Textarea-TextArea-13q4j",
"spot": "NO_SPOT",
"for": "id=:rbs: name=null className=prc-Textarea-TextArea-13q4j overtype-input",
"spot": {
"domain": "github.com",
"number": 57,
"slug": "diffplug/gitcasso",
"title": "what about drafts?",
"type": "GH_ISSUE_APPEND",
"unique_key": "github.com:diffplug/gitcasso:57",
},
},
]
`;

exports[`github detection > gh_project_issue_new:should detect correct spots 1`] = `
[
{
"for": "id=:r4t: name=null className=prc-Textarea-TextArea-13q4j overtype-input",
"spot": {
"domain": "github.com",
"slug": "diffplug/gitcasso",
"title": "Draft project title",
"type": "GH_ISSUE_CREATE",
"unique_key": "github.com:diffplug/gitcasso:new",
},
},
]
`;
Loading