The GitHub Projects plugin integrates GitHub's Projects V2 API into Obsidian's plugin ecosystem, providing a Kanban board interface for project management. The architecture emphasizes:
┌─────────────────────────────────────────────────────────────┐
│ Obsidian Application │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ GitHub Projects Plugin │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ Settings │ │ Project │ │ │
│ │ │ Tab │◄────►│ Board View │ │ │
│ │ └──────────────┘ └──────┬───────┘ │ │
│ │ │ │ │
│ │ ┌──────▼───────┐ │ │
│ │ │ State │ │ │
│ │ │ Management │ │ │
│ │ │ (EventEmitter)│ │ │
│ │ └──────┬───────┘ │ │
│ │ │ │ │
│ │ ┌─────────────────────┼─────────────────┐ │ │
│ │ │ │ │ │ │
│ │ ┌────▼────┐ ┌──────▼──────┐ ┌─────▼────┐ │
│ │ │ GitHub │ │ Storage │ │ UI │ │
│ │ │ API │ │ (localStorage)│ │Components│ │
│ │ │ Client │ └─────────────┘ │ (Preact) │ │
│ │ └────┬────┘ └──────────┘ │
│ │ │ │ │
│ └─────────┼──────────────────────────────────────────────┘ │
│ │ │
└────────────┼────────────────────────────────────────────────┘
│
┌─────▼──────┐
│ GitHub │
│ Projects V2│
│ GraphQL API│
└────────────┘
main.tsx)The main plugin class extends Obsidian's Plugin base class.
Responsibilities:
Key Methods:
class GitHubProjectsPlugin extends Plugin {
async onload() {
// Load settings
// Register view
// Add ribbon icon
// Register commands
} async onunload() {
// Cleanup resources
// Detach views
}
}
settings.ts)Manages plugin configuration UI.
Responsibilities:
Settings Schema:
interface PluginSettings {
githubToken: string; // Stored in localStorage
organization: string; // GitHub org name
projectNumber: number; // Project number from URL
autoRefreshInterval: number; // Minutes
}
views/ProjectBoardView.tsx)Custom ItemView displaying the Kanban board.
Responsibilities:
Lifecycle:
class ProjectBoardView extends ItemView {
async onOpen() {
// Initialize state
// Fetch project data
// Render UI
} async onClose() {
// Cleanup event listeners
// Save state
}
}
state/ProjectState.ts)Centralized state container using event-driven pattern.
Responsibilities:
Events:
project-loaded: Project data fetcheditem-moved: Card moved between columnsproject-error: Error occurredproject-refreshing: Refresh startedapi/client.ts)GraphQL client for GitHub Projects V2 API.
Responsibilities:
Key Methods:
class GitHubAPIClient {
async fetchProject(org, projectNumber): Promise
async updateItemStatus(itemId, statusId): Promise
async addItemToProject(projectId, contentId): Promise
}
views/components/)Preact components for rendering the board.
Component Hierarchy:
ProjectBoard
├── FilterBar
├── Column (multiple)
│ └── Card (multiple)
│ ├── Avatar (multiple)
│ └── Badge
└── DetailModal
User Opens Board
↓
ProjectBoardView.onOpen()
↓
ProjectState.loadProject()
↓
GitHubAPIClient.fetchProject()
↓
GitHub GraphQL API
↓
ProjectState.setProject()
↓
Event: 'project-loaded'
↓
ProjectBoardView re-renders
User Drags Card
↓
onDrop handler
↓
ProjectState.moveItem() (optimistic)
↓
Event: 'item-moved'
↓
UI updates immediately
↓
GitHubAPIClient.updateItemStatus()
↓
GitHub GraphQL API
↓
Background verification on next refresh
Timer triggers (every N minutes)
↓
ProjectState.refresh()
↓
Event: 'project-refreshing'
↓
UI shows loading indicator
↓
GitHubAPIClient.fetchProject()
↓
ProjectState.setProject()
↓
Event: 'project-loaded'
↓
UI updates with fresh data
Using Obsidian's Events class for pub/sub pattern:
class ProjectState {
private events = new Events();
private projectData: Project | null = null; on(event: string, callback: (...args: any[]) => void) {
this.events.on(event, callback);
}
setProject(project: Project) {
this.projectData = project;
this.events.trigger('project-loaded', project);
}
}
State updates create new objects:
moveItem(itemId: string, newStatusId: string) {
const newItems = this.projectData.items.map(item =>
item.id === itemId
? { ...item, statusId: newStatusId }
: item
); this.setProject({
...this.projectData,
items: newItems
});
}
Fetch Project:
query GetProject($org: String!, $number: Int!) {
organization(login: $org) {
projectV2(number: $number) {
id
title
fields(first: 20) {
nodes {
... on ProjectV2SingleSelectField {
id
name
options {
id
name
}
}
}
}
items(first: 100) {
nodes {
id
content {
... on Issue {
title
number
state
}
... on PullRequest {
title
number
state
}
}
fieldValues(first: 20) {
nodes {
... on ProjectV2ItemFieldSingleSelectValue {
field { id }
optionId
}
}
}
}
}
}
}
}
Update Item Status:
mutation UpdateItemStatus($projectId: ID!, $itemId: ID!, $fieldId: ID!, $value: String!) {
updateProjectV2ItemFieldValue(
input: {
projectId: $projectId
itemId: $itemId
fieldId: $fieldId
value: { singleSelectOptionId: $value }
}
) {
projectV2Item {
id
}
}
}
GitHub GraphQL API limits:
Mitigation:
try {
const response = await this.fetchProject(org, number);
return response;
} catch (error) {
if (error.status === 401) {
throw new Error('Invalid token');
} else if (error.status === 404) {
throw new Error('Project not found');
} else {
throw new Error('Network error');
}
}
We use Preact (React-compatible, 3KB) for:
Using SortableJS for drag-and-drop:
import Sortable from 'sortablejs';const sortable = Sortable.create(columnElement, {
group: 'board',
animation: 150,
onEnd: (evt) => {
const itemId = evt.item.dataset.itemId;
const newStatusId = evt.to.dataset.statusId;
projectState.moveItem(itemId, newStatusId);
}
});
.github-projects-board {
--bg-color: var(--background-primary);
--text-color: var(--text-normal);
}.github-projects-board__column { }
.github-projects-board__card { }
Tokens stored in localStorage (NOT vault files):
// Save token
localStorage.setItem('github-projects-token', token);// Retrieve token
const token = localStorage.getItem('github-projects-token');
Security considerations:
Recommendations:
Non-sensitive settings stored in vault:
.obsidian/plugins/github-projects/data.json
Contains: organization, project number, refresh interval (NOT token)
For boards with 100+ cards, implement virtual scrolling:
import { VirtualScroller } from './VirtualScroller'; }
/>
Debounce expensive operations:
const debouncedRefresh = debounce(
() => projectState.refresh(),
1000
);
Memoize expensive calculations:
const columns = useMemo(() => {
return groupItemsByStatus(items);
}, [items]);
Current bundle size: ~120KB (minified)