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
135 changes: 135 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Common Development Commands

```bash
# Development
npm run watch # Watch mode for development
npm run build # Production build
npm run build:pack # Build and create dist.zip package
npm run release # Build and package for release

# Testing and Quality
npm test # Run Jest tests
npm run lint # Run ESLint
npm run lint:fix # Fix linting issues automatically
npm run prettier # Format code with Prettier

# Utility
npm run clean # Clean dist directory
npm run prepare # Setup husky hooks

# Chrome Extension Development
# Load the 'dist' directory in Chrome's extension developer mode after building
```

## Project Architecture

This is a Chrome extension (Manifest v3) that enhances GitHub and GitHub Enterprise UI with productivity features.

### Core Structure

- **Content Scripts**: Three separate content scripts inject functionality into different GitHub pages:
- `content_pr_page.tsx` - Individual PR pages
- `content_prs_page.tsx` - PRs listing page
- `content_compare_page.tsx` - Compare/diff pages

- **Background Service Worker**: `background.ts` handles JIRA API requests due to CORS restrictions

- **Extension Pages**:
- `popup.tsx` - Extension popup interface
- `options.tsx` - Options/settings page with tabbed interface

### Key Components

- **Settings Management**: Uses Chrome storage API with Yup schema validation (`src/services/getSettings.ts`)
- **Multi-Instance Support**: Supports multiple GitHub instances (Enterprise + GitHub.com) via instance configuration
- **Feature Toggles**: All features are individually toggleable via settings
- **JIRA Integration**: Fetches issue data through background script to bypass CORS

### Settings Architecture

Settings are validated using Yup schemas and organized into:

- **`instances[]`** - GitHub instance configurations
- `pat` - Personal Access Token (must start with `ghp_`, minimum 30 characters)
- `org` - GitHub organization name
- `repo` - Repository name
- `ghBaseUrl` - GitHub API base URL (defaults to `https://api.github.com`)
- `randomReviewers` - Comma-separated list of reviewer usernames

- **`features{}`** - Boolean toggles for extension features:
- `baseBranchLabels` - Show base branch labels on PRs
- `changedFiles` - Display changed files count
- `totalLinesPrs` - Show total lines changed on PR list page
- `totalLinesPr` - Show total lines changed on individual PR page
- `reOrderPrs` - Reorder PRs by custom logic
- `addUpdateBranchButton` - Add update branch button
- `autoFilter` - Enable automatic PR filtering
- `prTitleFromJira` - Auto-populate PR title from JIRA ticket
- `descriptionTemplate` - Use custom PR description template
- `randomReviewer` - Enable random reviewer assignment

- **`jira{}`** - JIRA integration settings (optional)
- `pat` - JIRA Personal Access Token (minimum 30 characters)
- `baseUrl` - JIRA instance base URL
- `issueKeyRegex` - Regex pattern for JIRA issue keys (e.g., "TEST-\\d+")

- **`autoFilter`** - Custom PR filtering query string (optional)
- **`descriptionTemplate`** - Custom PR description template text (optional)

### Options Page Structure

The Options page uses a tabbed interface with the following tabs:

- **Feature Toggles** - Individual toggles for all extension features
- **GH Instances** - GitHub instance configuration (PAT, org, repo, base URL)
- **Jira** - JIRA integration settings
- **Import/Export** - Settings backup and restore functionality

Each tab is implemented as a separate component in `src/pages/Tabs/` with dedicated styling and logic.

### Build System

- **Webpack**: Multi-entry build with code splitting (vendor chunk for all except background)
- **SASS Modules**: CSS modules with local scope and hash-based class names
- **TypeScript**: Full TypeScript with strict validation
- **Entry Points**: Separate bundles for popup, options, content scripts, and background

### Chrome Extension Permissions

- `storage` - Chrome storage for settings
- `host_permissions: ["<all_urls>"]` - Access to all GitHub instances
- Content scripts inject into `https://*/*` patterns

### Development Workflow

1. Run `npm run watch` for development
2. Load `dist` directory in Chrome extension developer mode
3. Changes auto-reload with webpack watch mode
4. Use `npm run clean` to clear dist before production builds
5. Run `npm test` to execute Jest unit tests
6. Use `npm run lint:fix` to automatically fix linting issues
7. Format code with `npm run prettier` before committing

## Coding Guidelines

- **Type Definitions**: Always use `type` instead of `interface`
- **React Component Props**: Always name component props type as `Props` and never export it
- **Exports**: Never use default exports - always use named exports
- **Component Structure**: Follow existing component folder structure with index.ts barrel exports
- **Type Safety**: Never use `any` type - use proper typing with `unknown` for uncertain types
- **Type Casting**: When type casting is necessary, always include safety checks before casting

## Important Implementation Notes

- Settings validation uses Yup schemas with strict type checking
- GitHub API integration uses Octokit with user-provided PATs
- JIRA API calls must go through background script due to CORS
- Features are conditionally loaded based on current page URL and user settings
- CSS modules prevent style conflicts with GitHub's native styles
- Pre-commit hooks with husky and lint-staged ensure code quality
- All settings are persisted to Chrome's local storage API
- The extension supports hot reloading during development via webpack watch mode
7 changes: 5 additions & 2 deletions src/components/FeatureInput/FeatureInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@ import { getSettingValue, Settings } from "../../services";
type Props = {
storageKey: keyof Settings;
placeholder: string;
ariaLabel?: string;
onError: (message: string) => void;
disabled?: boolean;
ariaLabel?: string;
};

export const FeatureInput: React.FC<Props> = ({
storageKey,
placeholder,
onError,
disabled = false,
ariaLabel,
placeholder,
}) => {
const [value, setValue] = useState<string>();

Expand Down Expand Up @@ -48,6 +50,7 @@ export const FeatureInput: React.FC<Props> = ({
value={value}
onChange={handleChange}
aria-label={ariaLabel}
disabled={disabled}
/>
);
};
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Text } from "@primer/react";
import React, { CSSProperties } from "react";
import React, { CSSProperties, ReactNode } from "react";

type Props = {
children: ReactNode;
sx?: CSSProperties;
children: React.ReactNode;
};
export const Paragraph: React.FC<Props> = ({ sx, children }) => {

export const Paragraph: React.FC<Props> = ({ children, sx }) => {
return (
<Text
as="p"
Expand Down
File renamed without changes.
9 changes: 9 additions & 0 deletions src/components/SectionTitle/SectionTitle.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@import "../../shared-component-styles.scss";

.sectionTitle {
color: $fontColor;
font-size: 20px;
font-weight: bold;
margin-bottom: 24px;
margin-top: 24px;
}
15 changes: 15 additions & 0 deletions src/components/SectionTitle/SectionTitle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Text } from "@primer/react";
import React, { ReactNode } from "react";
import styles from "./SectionTitle.module.scss";

type Props = {
children: ReactNode;
};

export const SectionTitle: React.FC<Props> = ({ children }) => {
return (
<Text as="h2" className={styles.sectionTitle}>
{children}
</Text>
);
};
1 change: 1 addition & 0 deletions src/components/SectionTitle/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { SectionTitle } from "./SectionTitle";
4 changes: 4 additions & 0 deletions src/components/Subtitle/Subtitle.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.subtitle {
font-size: 14px;
font-style: italic;
}
15 changes: 15 additions & 0 deletions src/components/Subtitle/Subtitle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Text } from "@primer/react";
import React, { ReactNode } from "react";
import styles from "./Subtitle.module.scss";

type Props = {
children: ReactNode;
};

export const Subtitle: React.FC<Props> = ({ children }) => {
return (
<Text as="p" className={styles.subtitle}>
{children}
</Text>
);
};
1 change: 1 addition & 0 deletions src/components/Subtitle/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Subtitle } from "./Subtitle";
11 changes: 3 additions & 8 deletions src/components/TabNavigation/TabNavigation.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
import { UnderlineNav } from "@primer/react";
import { UnderlineNavItem } from "@primer/react/lib-esm/UnderlineNav/UnderlineNavItem";
import React from "react";

export type Tab = "GH Instances" | "Auto filter" | "Jira";
import { Tab } from "../../constants";

type Props = {
tabs: Tab[];
tabs: readonly Tab[];
Comment thread
HansKre marked this conversation as resolved.
activeTab: Tab;
onTabClick: (tab: Tab) => void;
};

export const TabNavigation: React.FC<Props> = ({
tabs,
activeTab,
onTabClick,
}) => {
export const TabNavigation = ({ tabs, activeTab, onTabClick }: Props) => {
return (
<UnderlineNav aria-label="Tabs">
{tabs.map((tab) => (
Expand Down
3 changes: 3 additions & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ export * from "./ClosePopupButton";
export * from "./FeatureInput";
export * from "./FeatureItem";
export * from "./IconButton";
export * from "./Paragraph";
export * from "./RandomReviewerButton";
export * from "./SearchInput";
export * from "./SectionTitle";
export * from "./Subtitle";
export * from "./TabNavigation";
export * from "./TotalLines";
export * from "./UpdateBranchButton";
1 change: 1 addition & 0 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./tabs";
8 changes: 8 additions & 0 deletions src/constants/tabs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const TABS = [
"Feature Toggles",
"GH Instances",
"Jira",
"Import/Export",
] as const;

export type Tab = (typeof TABS)[number];
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import { Settings } from "../services";
import { TemplateDescriptionParameters } from "./types";
import { DescriptionTemplatePlaceholders } from "./types";
import { extractJiraIssueKeyFromBranch } from "./utils/comparePageUtils";

function resolveJiraLink(settings: Settings): string {
const description = settings.templateDescription;
const description = settings.descriptionTemplate;
const hasJiraTicket = description.includes(
TemplateDescriptionParameters.JIRA_TICKET,
DescriptionTemplatePlaceholders.JIRA_TICKET,
);
if (!hasJiraTicket) return description;

const issueKey = extractJiraIssueKeyFromBranch(settings);
if (!issueKey) return description;

return description.replace(
new RegExp(TemplateDescriptionParameters.JIRA_TICKET, "g"),
new RegExp(DescriptionTemplatePlaceholders.JIRA_TICKET, "g"),
`${settings.jira?.baseUrl}/browse/${issueKey}`,
);
}

export function addTemplateDescription(settings: Settings) {
if (!settings.templateDescription) return;
export function addDescriptionTemplate(settings: Settings) {
if (!settings.descriptionTemplate) return;
const textArea =
document.querySelector<HTMLTextAreaElement>("#pull_request_body");
if (!textArea) return;
Expand Down
10 changes: 3 additions & 7 deletions src/content/handlePrFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ let theInstanceConfig: InstanceConfig | undefined;
*/
export function handlePrFilter(
instanceConfig: InstanceConfig,
{ filter }: AutoFilter,
autoFilter: AutoFilter,
filterIntercepted?: string,
) {
theInstanceConfig = instanceConfig;
Expand All @@ -20,7 +20,7 @@ export function handlePrFilter(
}

document.addEventListener("click", onQuickFilterClick);
replaceFilter(filter, filterIntercepted);
replaceFilter(autoFilter, filterIntercepted);
}

function replaceFilter(filter: string | undefined, filterIntercepted?: string) {
Expand Down Expand Up @@ -66,11 +66,7 @@ function onQuickFilterClick(event: MouseEvent) {
if (targetUrl.href.includes("/issues")) {
event.preventDefault();
const decodedFilter = decodeQueryString(params.toString());
handlePrFilter(
theInstanceConfig,
{ filter: decodedFilter },
decodedFilter,
);
handlePrFilter(theInstanceConfig, decodedFilter, decodedFilter);
intercepted = true;
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/content/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,6 @@ export enum Messages {
FETCH_JIRA_ISSUE = "FETCH_JIRA_ISSUE",
}

export enum TemplateDescriptionParameters {
export enum DescriptionTemplatePlaceholders {
JIRA_TICKET = "{{jiraTicket}}",
}
8 changes: 1 addition & 7 deletions src/content/utils/comparePageUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,8 @@ export function isOnComparePage(instanceConfig: InstanceConfig): boolean {
}

export const extractJiraIssueKeyFromBranch = (settings: Settings) => {
const isJiraEnabled = settings.features?.jira;
const issueKeyRegex = settings.jira?.issueKeyRegex;
if (!isJiraEnabled || !issueKeyRegex) {
alert(
"Jira integration or Jira issue key regex is not set. Please configure it in the settings.",
);
return null;
}
if (!issueKeyRegex) return null;

const details = document.getElementById("head-ref-selector");
const span = details?.querySelector("span.css-truncate-target");
Expand Down
6 changes: 3 additions & 3 deletions src/content_compare_page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { addPrTitleFromJira } from "./content/addPrTitleFromJira";
import { addTemplateDescription } from "./content/addTemplateDescription";
import { addDescriptionTemplate } from "./content/addDescriptionTemplate";
import { isOnComparePage } from "./content/utils/comparePageUtils";
import { getInstanceConfig } from "./getInstanceConfig";
import { getSettings, InstanceConfig, Settings } from "./services";
Expand All @@ -22,8 +22,8 @@ async function executeScripts(
if (!isOnComparePage(instanceConfig)) return;

try {
if (settings.features.templateDescription) {
addTemplateDescription(settings);
if (settings.features.descriptionTemplate) {
addDescriptionTemplate(settings);
}
if (settings.features.prTitleFromJira) {
await addPrTitleFromJira(settings);
Expand Down
2 changes: 1 addition & 1 deletion src/content_pr_page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ async function executeScripts(

const octokit = getOctoInstance(instanceConfig);

if (features.totalLines) {
if (features.totalLinesPr) {
await handleTotalLines(octokit, instanceConfig);
}

Expand Down
Loading