diff --git a/.claude/commands/corpus-loop.md b/.claude/commands/corpus-loop.md index b669dcd..32adc72 100644 --- a/.claude/commands/corpus-loop.md +++ b/.claude/commands/corpus-loop.md @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1776c9e..77eb705 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`) diff --git a/src/entrypoints/content.ts b/src/entrypoints/content.ts index 4e1e04d..3f8ef02 100644 --- a/src/entrypoints/content.ts +++ b/src/entrypoints/content.ts @@ -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 diff --git a/src/lib/enhancer.ts b/src/lib/enhancer.ts index 5f596da..c50dc01 100644 --- a/src/lib/enhancer.ts +++ b/src/lib/enhancer.ts @@ -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. */ diff --git a/src/lib/enhancers/github/GitHubEditEnhancer.tsx b/src/lib/enhancers/github/GitHubEditEnhancer.tsx index 17bb08b..2bc8b61 100644 --- a/src/lib/enhancers/github/GitHubEditEnhancer.tsx +++ b/src/lib/enhancers/github/GitHubEditEnhancer.tsx @@ -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 @@ -29,6 +35,50 @@ export class GitHubEditEnhancer implements CommentEnhancer { 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+)/ @@ -46,9 +96,8 @@ export class GitHubEditEnhancer implements CommentEnhancer { "[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 diff --git a/src/lib/enhancers/github/GitHubIssueAppendEnhancer.tsx b/src/lib/enhancers/github/GitHubIssueAppendEnhancer.tsx index 5c05d45..24eadf8 100644 --- a/src/lib/enhancers/github/GitHubIssueAppendEnhancer.tsx +++ b/src/lib/enhancers/github/GitHubIssueAppendEnhancer.tsx @@ -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 @@ -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) diff --git a/src/lib/enhancers/github/GitHubIssueCreateEnhancer.tsx b/src/lib/enhancers/github/GitHubIssueCreateEnhancer.tsx index a9ee917..fbcb019 100644 --- a/src/lib/enhancers/github/GitHubIssueCreateEnhancer.tsx +++ b/src/lib/enhancers/github/GitHubIssueCreateEnhancer.tsx @@ -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 @@ -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) diff --git a/src/lib/enhancers/github/github-common.ts b/src/lib/enhancers/github/github-common.ts index 09f57ff..26808fd 100644 --- a/src/lib/enhancers/github/github-common.ts +++ b/src/lib/enhancers/github/github-common.ts @@ -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"]') +} diff --git a/tests/corpus-fixture.ts b/tests/corpus-fixture.ts index 649ae4b..e8a3181 100644 --- a/tests/corpus-fixture.ts +++ b/tests/corpus-fixture.ts @@ -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) { @@ -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) { diff --git a/tests/corpus-view.ts b/tests/corpus-view.ts index 4280918..60688e8 100644 --- a/tests/corpus-view.ts +++ b/tests/corpus-view.ts @@ -97,6 +97,7 @@ function getUrlParts(key: string) { hostname: url.hostname, href: originalUrl, pathname: url.pathname, + search: url.search, } } @@ -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 @@ -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()) @@ -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 diff --git a/tests/corpus/_corpus-index.ts b/tests/corpus/_corpus-index.ts index aba7860..25bee0c 100644 --- a/tests/corpus/_corpus-index.ts +++ b/tests/corpus/_corpus-index.ts @@ -76,4 +76,9 @@ export const CORPUS: Record = { 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 diff --git a/tests/corpus/gh_project_issue_new.html b/tests/corpus/gh_project_issue_new.html new file mode 100644 index 0000000..3b45d04 --- /dev/null +++ b/tests/corpus/gh_project_issue_new.html @@ -0,0 +1,1503 @@ + + + + + + + + + + + + +New issue · Backlog · Gitcasso + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ + + + + +
+ +
+ +
+
+ + Select an owner + + + + + + Loading + + + +
+
+
+ +
+
+ +
+
+
+ +
+ Skip to content + + + + + + + + +
+
+ + + + + + +
+
+
+ +
+ + + +
+
+ +
+ +
+

    Unrefined

     (8 / 999)
    Estimate: 0
      Click a value to filter the view
        Click a value to filter the view
          Click a value to filter the view
          gitcasso #30

          Snapshots for Reddit, GitLab, etc.

            Click a value to filter the view
              Click a value to filter the view
                Click a value to filter the view
                  Click a value to filter the view

                  1.0 Backlog

                   (1)
                  Estimate: 0

                  In Progress

                   (2 / 5)
                  Estimate: 0
                  This is actively being worked on
                  Draft

                  figure out publishing

                    Click a value to filter the view
                      Click a value to filter the view

                      In PR

                       (0)
                      Estimate: 0

                      Done

                       (45)
                      Estimate: 0
                      This has been completed
                      + + + + +
                        +
                        + + + + + + + +
                        +
                        New issue · Backlog · Gitcasso
                        +
                        + + \ No newline at end of file diff --git a/tests/lib/enhancers/__snapshots__/gh-detection.test.ts.snap b/tests/lib/enhancers/__snapshots__/gh-detection.test.ts.snap index 67f29e6..401542e 100644 --- a/tests/lib/enhancers/__snapshots__/gh-detection.test.ts.snap +++ b/tests/lib/enhancers/__snapshots__/gh-detection.test.ts.snap @@ -224,8 +224,12 @@ 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", + }, }, ] `; @@ -233,8 +237,15 @@ exports[`github detection > gh_project_draft_edit:should detect correct spots 1` 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", + }, }, ] `; @@ -242,12 +253,38 @@ exports[`github detection > gh_project_issue:should detect correct spots 1`] = ` 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", + }, }, ] `; diff --git a/tests/lib/enhancers/__snapshots__/gh-ui.test.ts.snap b/tests/lib/enhancers/__snapshots__/gh-ui.test.ts.snap index 33bcd78..15c6ae1 100644 --- a/tests/lib/enhancers/__snapshots__/gh-ui.test.ts.snap +++ b/tests/lib/enhancers/__snapshots__/gh-ui.test.ts.snap @@ -274,8 +274,104 @@ exports[`github ui > gh_project:should render correct UI elements 1`] = `[]`; exports[`github ui > gh_project_draft:should render correct UI elements 1`] = `[]`; -exports[`github ui > gh_project_draft_edit:should render correct UI elements 1`] = `[]`; +exports[`github ui > gh_project_draft_edit:should render correct UI elements 1`] = ` +[ + { + "for": "id=:r5a: name=null className=prc-Textarea-TextArea-13q4j overtype-input", + "title": "N/A", + "upperDecoration": + N/A + , + }, +] +`; -exports[`github ui > gh_project_issue:should render correct UI elements 1`] = `[]`; +exports[`github ui > gh_project_issue:should render correct UI elements 1`] = ` +[ + { + "for": "id=:rbs: name=null className=prc-Textarea-TextArea-13q4j overtype-input", + "title": "what about drafts?", + "upperDecoration": + + + + + # + 57 + + + diffplug/gitcasso + + + , + }, +] +`; -exports[`github ui > gh_project_issue_edit:should render correct UI elements 1`] = `[]`; +exports[`github ui > gh_project_issue_edit:should render correct UI elements 1`] = ` +[ + { + "for": "id=:rdh: name=null className=prc-Textarea-TextArea-13q4j overtype-input", + "title": "N/A", + "upperDecoration": + N/A + , + }, + { + "for": "id=:rbs: name=null className=prc-Textarea-TextArea-13q4j overtype-input", + "title": "what about drafts?", + "upperDecoration": + + + + + # + 57 + + + diffplug/gitcasso + + + , + }, +] +`; + +exports[`github ui > gh_project_issue_new:should render correct UI elements 1`] = ` +[ + { + "for": "id=:r4t: name=null className=prc-Textarea-TextArea-13q4j overtype-input", + "title": "Draft project title", + "upperDecoration": + + + + + <draft> + + + diffplug/gitcasso + + + , + }, +] +`;