From 2511d0fa30f122a940191084657203eff1d1a42a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Platte?= Date: Thu, 15 Jan 2026 18:52:52 +0100 Subject: [PATCH 1/7] bugfix: use global Template #36 --- src/types.ts | 4 ++++ src/util/settingsUtils.ts | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/types.ts b/src/types.ts index 755a5bb..91419dc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -125,12 +125,14 @@ export interface GlobalDefaults { issueFolder: string; issueNoteTemplate: string; issueContentTemplate: string; + useCustomIssueContentTemplate: boolean; includeIssueComments: boolean; pullRequestUpdateMode: "none" | "update" | "append"; allowDeletePullRequest: boolean; pullRequestFolder: string; pullRequestNoteTemplate: string; pullRequestContentTemplate: string; + useCustomPullRequestContentTemplate: boolean; includePullRequestComments: boolean; includeClosedIssues: boolean; includeClosedPullRequests: boolean; @@ -160,12 +162,14 @@ export const DEFAULT_GLOBAL_DEFAULTS: GlobalDefaults = { issueFolder: "GitHub", issueNoteTemplate: "Issue - {number}", issueContentTemplate: "", + useCustomIssueContentTemplate: false, includeIssueComments: true, pullRequestUpdateMode: "none", allowDeletePullRequest: true, pullRequestFolder: "GitHub Pull Requests", pullRequestNoteTemplate: "PR - {number}", pullRequestContentTemplate: "", + useCustomPullRequestContentTemplate: false, includePullRequestComments: true, includeClosedIssues: false, includeClosedPullRequests: false, diff --git a/src/util/settingsUtils.ts b/src/util/settingsUtils.ts index e5cc4b9..1aa1ac6 100644 --- a/src/util/settingsUtils.ts +++ b/src/util/settingsUtils.ts @@ -22,13 +22,15 @@ export function getEffectiveRepoSettings( allowDeleteIssue: globalDefaults.allowDeleteIssue, issueFolder: repo.useCustomIssueFolder ? repo.customIssueFolder : globalDefaults.issueFolder, issueNoteTemplate: globalDefaults.issueNoteTemplate, - issueContentTemplate: repo.useCustomIssueContentTemplate ? repo.issueContentTemplate : globalDefaults.issueContentTemplate, + issueContentTemplate: (repo.useCustomIssueContentTemplate && repo.issueContentTemplate) ? repo.issueContentTemplate : globalDefaults.issueContentTemplate, + useCustomIssueContentTemplate: (repo.useCustomIssueContentTemplate && repo.issueContentTemplate) ? repo.useCustomIssueContentTemplate : globalDefaults.useCustomIssueContentTemplate, includeIssueComments: globalDefaults.includeIssueComments, pullRequestUpdateMode: globalDefaults.pullRequestUpdateMode, allowDeletePullRequest: globalDefaults.allowDeletePullRequest, pullRequestFolder: repo.useCustomPullRequestFolder ? repo.customPullRequestFolder : globalDefaults.pullRequestFolder, pullRequestNoteTemplate: globalDefaults.pullRequestNoteTemplate, - pullRequestContentTemplate: repo.useCustomPullRequestContentTemplate ? repo.pullRequestContentTemplate : globalDefaults.pullRequestContentTemplate, + pullRequestContentTemplate: (repo.useCustomPullRequestContentTemplate && repo.pullRequestContentTemplate) ? repo.pullRequestContentTemplate : globalDefaults.pullRequestContentTemplate, + useCustomPullRequestContentTemplate: (repo.useCustomPullRequestContentTemplate && repo.pullRequestContentTemplate) ? repo.useCustomPullRequestContentTemplate : globalDefaults.useCustomPullRequestContentTemplate, includePullRequestComments: globalDefaults.includePullRequestComments, }; } From 7e6dacac7c957c1d0fdddd1fb02bd190ead5b03f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Platte?= Date: Thu, 15 Jan 2026 20:13:20 +0100 Subject: [PATCH 2/7] feat: integrate Obsidian Keychain --- package.json | 8 +- src/github-client.ts | 7 +- src/main.ts | 103 +++++++++++++++++++++++- src/settings-tab.ts | 185 ++++++++++++++++++++++++++++++++++--------- src/types.ts | 4 + styles.css | 88 ++++---------------- 6 files changed, 276 insertions(+), 119 deletions(-) diff --git a/package.json b/package.json index 917d3a4..2afc647 100644 --- a/package.json +++ b/package.json @@ -19,13 +19,13 @@ "author": "LonoxX", "license": "MIT", "devDependencies": { - "@types/node": "^25.0.3", - "@typescript-eslint/eslint-plugin": "^8.52.0", - "@typescript-eslint/parser": "^8.52.0", + "@types/node": "^25.0.8", + "@typescript-eslint/eslint-plugin": "^8.53.0", + "@typescript-eslint/parser": "^8.53.0", "builtin-modules": "^5.0.0", "esbuild": "^0.27.2", "eslint": "^9.39.2", - "obsidian": "latest", + "obsidian": "^1.11.4", "tslib": "^2.8.1", "typescript": "^5.9.3" }, diff --git a/src/github-client.ts b/src/github-client.ts index dec4632..ef1f955 100644 --- a/src/github-client.ts +++ b/src/github-client.ts @@ -16,19 +16,22 @@ import { export class GitHubClient { private octokit: Octokit | null = null; private currentUser: string = ""; + private tokenGetter: () => string; constructor( private settings: GitHubTrackerSettings, private noticeManager: NoticeManager, + tokenGetter: () => string, ) { + this.tokenGetter = tokenGetter; this.initializeClient(); } /** * Initialize GitHub client with the current token */ - public initializeClient(token?: string): void { - const authToken = token || this.settings.githubToken; + public initializeClient(): void { + const authToken = this.tokenGetter(); if (!authToken) { this.noticeManager.error( diff --git a/src/main.ts b/src/main.ts index dce2db9..24b93c0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -19,6 +19,99 @@ export default class GitHubTrackerPlugin extends Plugin { currentUser: string = ""; private backgroundSyncIntervalId: number | null = null; + /** + * Get the GitHub token, either from SecretStorage or from settings + * @returns The GitHub token or empty string if not available + */ + getGitHubToken(): string { + if (this.settings.useSecretStorage && this.settings.secretTokenName) { + try { + const secret = this.app.secretStorage?.getSecret(this.settings.secretTokenName); + if (secret) { + return secret; + } + // If secret not found but useSecretStorage is enabled, warn user + console.warn(`Secret "${this.settings.secretTokenName}" not found in SecretStorage`); + } catch (error) { + console.error("Error retrieving secret from SecretStorage:", error); + } + } + // Fallback to legacy token in settings + return this.settings.githubToken || ""; + } + + /** + * Check if SecretStorage is available (Obsidian 1.11) + */ + isSecretStorageAvailable(): boolean { + return !!this.app.secretStorage; + } + + /** + * Validate that the configured secret exists and has a value + * @returns true if secret is valid or not using SecretStorage, false if secret is missing/invalid + */ + validateSecretStorage(): boolean { + if (!this.settings.useSecretStorage) { + return true; + } + + if (!this.settings.secretTokenName) { + return false; + } + + try { + const secret = this.app.secretStorage?.getSecret(this.settings.secretTokenName); + return !!secret; + } catch (error) { + console.error("Error validating secret:", error); + return false; + } + } + + /** + * Migrate token from settings to SecretStorage + * @param secretName The name to use for the secret + * @returns true if migration was successful + */ + async migrateTokenToSecretStorage(secretName: string): Promise { + if (!this.isSecretStorageAvailable()) { + new Notice("SecretStorage is not available. Please update Obsidian to version 1.11 or later."); + return false; + } + + if (!this.app.secretStorage) { + new Notice("SecretStorage is not initialized."); + return false; + } + + if (!this.settings.githubToken) { + new Notice("No token to migrate. Please enter a token first."); + return false; + } + + try { + // Store the token in SecretStorage + this.app.secretStorage.setSecret(secretName, this.settings.githubToken); + + // Update settings + this.settings.useSecretStorage = true; + this.settings.secretTokenName = secretName; + + // Clear the plaintext token from settings + this.settings.githubToken = ""; + + await this.saveSettings(); + + new Notice("Token successfully migrated to SecretStorage!"); + return true; + } catch (error) { + console.error("Failed to migrate token to SecretStorage:", error); + new Notice("Failed to migrate token. See console for details."); + return false; + } + } + async sync() { if (this.isSyncing) { this.noticeManager.warning("Already syncing..."); @@ -293,7 +386,7 @@ export default class GitHubTrackerPlugin extends Plugin { await this.loadSettings(); this.noticeManager = new NoticeManager(this.settings); - this.gitHubClient = new GitHubClient(this.settings, this.noticeManager); + this.gitHubClient = new GitHubClient(this.settings, this.noticeManager, () => this.getGitHubToken()); if (this.gitHubClient.isReady()) { this.currentUser = await this.gitHubClient.fetchAuthenticatedUser(); } @@ -426,7 +519,8 @@ export default class GitHubTrackerPlugin extends Plugin { async saveSettings() { await this.saveData(this.settings); - if (this.settings.githubToken) { + const token = this.getGitHubToken(); + if (token) { this.gitHubClient?.initializeClient(); } if (this.noticeManager) { @@ -444,7 +538,8 @@ export default class GitHubTrackerPlugin extends Plugin { return []; } - if (!this.settings.githubToken) { + const token = this.getGitHubToken(); + if (!token) { this.noticeManager.error( "No GitHub token provided. Please add your GitHub token in the settings.", ); @@ -452,7 +547,7 @@ export default class GitHubTrackerPlugin extends Plugin { } try { - await this.gitHubClient.initializeClient(this.settings.githubToken); + this.gitHubClient.initializeClient(); if (!this.currentUser) { this.currentUser = diff --git a/src/settings-tab.ts b/src/settings-tab.ts index 7f7719a..12ad094 100644 --- a/src/settings-tab.ts +++ b/src/settings-tab.ts @@ -9,6 +9,7 @@ import { TFolder, AbstractInputSuggest, TAbstractFile, + SecretComponent, } from "obsidian"; import { RepositoryTracking, DEFAULT_REPOSITORY_TRACKING, TrackedProject } from "./types"; import GitHubTrackerPlugin from "./main"; @@ -28,6 +29,7 @@ export class GitHubTrackerSettingTab extends PluginSettingTab { private modalManager: ModalManager; private projectListManager: ProjectListManager; private projectRenderer: ProjectRenderer; + private isValidatingToken: boolean = false; constructor( app: App, @@ -107,37 +109,119 @@ export class GitHubTrackerSettingTab extends PluginSettingTab { const authContainer = containerEl.createDiv("github-issues-settings-group github-issues-settings-group-compact"); new Setting(authContainer).setName("Authentication").setHeading(); - const tokenSetting = new Setting(authContainer) - .setName("GitHub token") - .setDesc("Your GitHub personal access token"); - - let isTokenVisible = false; - const tokenInput = tokenSetting.addText((text) => { - text - .setPlaceholder("Enter your GitHub token") - .setValue(this.plugin.settings.githubToken) - .onChange(async (value) => { - this.plugin.settings.githubToken = value; - await this.plugin.saveSettings(); - this.updateTokenBadge(); // Update badge when token changes - }); - text.inputEl.type = "password"; - return text; - }); + // Check if SecretStorage is available and show appropriate UI + const isSecretStorageAvailable = this.plugin.isSecretStorageAvailable(); + const isUsingSecretStorage = this.plugin.settings.useSecretStorage; + + if (isSecretStorageAvailable) { + // Show SecretStorage toggle option + new Setting(authContainer) + .setName("Use SecretStorage") + .setDesc("Store your GitHub token securely using Obsidian's Keychain (recommended).") + .addToggle((toggle) => + toggle + .setValue(isUsingSecretStorage) + .onChange(async (value) => { + this.plugin.settings.useSecretStorage = value; + if (value) { + // clear the legacy token from data.json + this.plugin.settings.githubToken = ""; + } else { + // Disable SecretStorage - clear the secret name + this.plugin.settings.secretTokenName = ""; + } + await this.plugin.saveSettings(); + this.display(); + }), + ); + } + + if (isUsingSecretStorage && isSecretStorageAvailable) { + new Setting(authContainer) + .setName("GitHub Token Secret") + .setDesc("Select an existing secret or create a new one") + .addComponent((containerEl) => { + const secretComponent = new SecretComponent(this.app, containerEl); - tokenSetting.addButton((button) => { - button - .setIcon("eye") - .setTooltip("Show/hide token") - .onClick(() => { - isTokenVisible = !isTokenVisible; - const inputEl = tokenSetting.controlEl.querySelector("input"); - if (inputEl) { - inputEl.type = isTokenVisible ? "text" : "password"; + // Set initial value if exists + if (this.plugin.settings.secretTokenName) { + secretComponent.setValue(this.plugin.settings.secretTokenName); } - button.setIcon(isTokenVisible ? "eye-off" : "eye"); + + secretComponent.onChange(async (value) => { + this.plugin.settings.secretTokenName = value; + await this.plugin.saveSettings(); + + // Re-initialize the GitHub client with the new token + if (this.plugin.getGitHubToken() && this.plugin.gitHubClient) { + this.plugin.gitHubClient.initializeClient(); + } + + // Update the badge + await this.updateTokenBadge(); + }); + + return secretComponent; }); - }); + } else { + // Show traditional token input + const tokenSetting = new Setting(authContainer) + .setName("GitHub token") + .setDesc("Your GitHub personal access token"); + + let isTokenVisible = false; + tokenSetting.addText((text) => { + text + .setPlaceholder("Enter your GitHub token") + .setValue(this.plugin.settings.githubToken) + .onChange(async (value) => { + this.plugin.settings.githubToken = value; + await this.plugin.saveSettings(); + + // Re-initialize the GitHub client with the new token + if (value && this.plugin.gitHubClient) { + this.plugin.gitHubClient.initializeClient(); + } + + // Update the badge + await this.updateTokenBadge(); + }); + text.inputEl.type = "password"; + return text; + }); + + tokenSetting.addButton((button) => { + button + .setIcon("eye") + .setTooltip("Show/hide token") + .onClick(() => { + isTokenVisible = !isTokenVisible; + const inputEl = tokenSetting.controlEl.querySelector("input"); + if (inputEl) { + inputEl.type = isTokenVisible ? "text" : "password"; + } + button.setIcon(isTokenVisible ? "eye-off" : "eye"); + }); + }); + + // Show migration button if SecretStorage is available and token exists + if (isSecretStorageAvailable && this.plugin.settings.githubToken) { + new Setting(authContainer) + .setName("Migrate to Keychain") + .setDesc("Move your existing token to Obsidian's Keychain") + .addButton((button) => + button + .setButtonText("Migrate now") + .setCta() + .onClick(async () => { + const success = await this.plugin.migrateTokenToSecretStorage("github-issues-token"); + if (success) { + this.display(); + } + }) + ); + } + } // Add token status badge const tokenBadgeContainer = authContainer.createDiv("github-issues-token-badge-container"); @@ -355,7 +439,7 @@ export class GitHubTrackerSettingTab extends PluginSettingTab { const escapingContent = escapingDetails.createDiv("github-issues-escaping-content"); const warningP = escapingContent.createEl("p"); - warningP.textContent = "⚠️ CAUTION: Disabling escaping may allow malicious scripts to execute"; + warningP.textContent = "CAUTION: Disabling escaping may allow malicious scripts to execute"; warningP.addClass("github-issues-warning-text"); escapingContent.createEl("p").textContent = "• Normal: Escapes template syntax like '`', '{{', '}}', '<%', '%>'"; @@ -1749,15 +1833,40 @@ export class GitHubTrackerSettingTab extends PluginSettingTab { } /** - * Fetch and display available labels for a repository + * Update token validation badge */ private async updateTokenBadge(container?: HTMLElement): Promise { + // Prevent multiple simultaneous validation attempts + if (this.isValidatingToken) { + return; + } + const badgeContainer = container || this.containerEl.querySelector(".github-issues-token-badge-container") as HTMLElement; if (!badgeContainer) return; badgeContainer.empty(); - if (!this.plugin.settings.githubToken) { + // Check if using SecretStorage and if secret exists + if (this.plugin.settings.useSecretStorage) { + if (!this.plugin.settings.secretTokenName) { + const badge = badgeContainer.createDiv("github-issues-token-badge github-issues-token-badge-invalid"); + badge.setText("No secret selected"); + return; + } + + // Check if secret actually has a value + const secretValue = this.app.secretStorage?.getSecret(this.plugin.settings.secretTokenName); + if (!secretValue) { + const badge = badgeContainer.createDiv("github-issues-token-badge github-issues-token-badge-invalid"); + badge.setText("Secret not found or empty"); + return; + } + } + + // Get token using the plugin's getGitHubToken method (supports both SecretStorage and settings) + const currentToken = this.plugin.getGitHubToken(); + + if (!currentToken) { const badge = badgeContainer.createDiv("github-issues-token-badge github-issues-token-badge-invalid"); badge.setText("No token"); return; @@ -1773,9 +1882,11 @@ export class GitHubTrackerSettingTab extends PluginSettingTab { const loadingBadge = badgeContainer.createDiv("github-issues-token-badge github-issues-token-badge-loading"); loadingBadge.setText("Validating token..."); + this.isValidatingToken = true; + try { // Initialize client with current token - this.plugin.gitHubClient.initializeClient(this.plugin.settings.githubToken); + this.plugin.gitHubClient.initializeClient(); // Validate token and get information const [tokenInfo, rateLimit] = await Promise.all([ @@ -1783,13 +1894,13 @@ export class GitHubTrackerSettingTab extends PluginSettingTab { this.plugin.gitHubClient.getRateLimit() ]); - // Clear loading state + // Clear loading badge first badgeContainer.empty(); if (tokenInfo.valid) { // Valid token badge const validBadge = badgeContainer.createDiv("github-issues-token-badge github-issues-token-badge-valid"); - validBadge.setText("✓ Valid token"); + validBadge.setText("Valid token"); // Scopes badge if (tokenInfo.scopes.length > 0) { @@ -1804,13 +1915,15 @@ export class GitHubTrackerSettingTab extends PluginSettingTab { } } else { const invalidBadge = badgeContainer.createDiv("github-issues-token-badge github-issues-token-badge-invalid"); - invalidBadge.setText("✗ Invalid token"); + invalidBadge.setText("Invalid token"); } } catch (error) { - // Clear loading state and show error + // Clear loading badge and show error badgeContainer.empty(); const errorBadge = badgeContainer.createDiv("github-issues-token-badge github-issues-token-badge-error"); errorBadge.setText("Error validating token"); + } finally { + this.isValidatingToken = false; } } diff --git a/src/types.ts b/src/types.ts index 91419dc..f20ed40 100644 --- a/src/types.ts +++ b/src/types.ts @@ -140,6 +140,8 @@ export interface GlobalDefaults { export interface GitHubTrackerSettings { githubToken: string; + useSecretStorage: boolean; + secretTokenName: string; repositories: RepositoryTracking[]; dateFormat: string; syncOnStartup: boolean; @@ -177,6 +179,8 @@ export const DEFAULT_GLOBAL_DEFAULTS: GlobalDefaults = { export const DEFAULT_SETTINGS: GitHubTrackerSettings = { githubToken: "", + useSecretStorage: false, + secretTokenName: "", repositories: [], dateFormat: "", syncOnStartup: true, diff --git a/styles.css b/styles.css index 7ba7597..8083673 100644 --- a/styles.css +++ b/styles.css @@ -247,10 +247,7 @@ github-issues-confirmation-modal .warning-text { margin-bottom: 15px; } -.github-issues-hidden { - display: none; -} - +.github-issues-hidden, .github-issues-settings-hidden { display: none; } @@ -511,6 +508,11 @@ github-issues-confirmation-modal .warning-text { } .github-issues-tracked-container { + display: flex; + align-items: center; + gap: 5px; + color: var(--text-accent); + font-weight: 500; animation: pulse 1s ease-in-out; } @@ -668,7 +670,7 @@ github-issues-confirmation-modal .warning-text { display: flex; align-items: center; flex-grow: 1; - gap: 6px; + gap: 8px; } .github-issues-settings-group { @@ -818,10 +820,6 @@ github-issues-confirmation-modal .warning-text { transition: all 0.2s ease; } -.github-issues-config-button { - position: relative; -} - .github-issues-sync-button { background-color: var(--interactive-accent); color: var(--text-on-accent); @@ -878,18 +876,6 @@ github-issues-confirmation-modal .warning-text { margin-right: 5px; } -.github-issues-repo-header-container { - position: relative; -} - -@keyframes loading { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} .github-issues-loading::before { content: ""; @@ -899,7 +885,7 @@ github-issues-confirmation-modal .warning-text { border: 2px solid rgba(255, 255, 255, 0.3); border-radius: 50%; border-top-color: white; - animation: loading 1s linear infinite; + animation: spin 1s linear infinite; margin-right: 8px; } @@ -932,6 +918,7 @@ button:disabled { width: 100%; box-sizing: border-box; cursor: pointer; + position: relative; } .github-issues-card { @@ -1008,7 +995,7 @@ button:disabled { } .github-issues-spinner { - animation: rotate 2s linear infinite; + animation: spin 2s linear infinite; width: 40px; height: 40px; } @@ -1019,12 +1006,6 @@ button:disabled { animation: dash 1.5s ease-in-out infinite; } -@keyframes rotate { - 100% { - transform: rotate(360deg); - } -} - @keyframes dash { 0% { stroke-dasharray: 1, 150; @@ -1142,14 +1123,6 @@ button:disabled { transform: translateY(-1px); } -.github-issues-tracked-container { - display: flex; - align-items: center; - gap: 5px; - color: var(--text-accent); - font-weight: 500; -} - .github-issues-empty-state { text-align: center; padding: 40px 20px; @@ -1236,7 +1209,10 @@ button:disabled { } .github-issues-repo-checkbox { - margin-right: 12px; + cursor: pointer; + width: 18px; + height: 18px; + margin-right: 8px; flex-shrink: 0; } @@ -1247,13 +1223,6 @@ button:disabled { accent-color: var(--interactive-accent); } -.github-issues-repo-info { - display: flex; - align-items: center; - gap: 8px; - flex-grow: 1; -} - .github-issues-item .github-issues-repo-info { padding-left: 4px; } @@ -1731,15 +1700,6 @@ button:disabled { cursor: not-allowed; } -/* Repository Checkbox */ -.github-issues-repo-checkbox { - cursor: pointer; - width: 18px; - height: 18px; - margin-right: 8px; - flex-shrink: 0; -} - /* Delete Modal Repository List */ .github-issues-delete-repo-list { margin: 12px 0; @@ -1991,6 +1951,7 @@ button:disabled { flex-wrap: wrap; gap: 5px; margin-top: 10px; + margin-bottom: 6px; } .github-kanban-label { @@ -2238,25 +2199,6 @@ button:disabled { color: var(--text-muted); } -.github-kanban-item-title { - font-weight: 600; - margin-bottom: 6px; -} - -.github-kanban-item-labels { - display: flex; - flex-wrap: wrap; - gap: 4px; - margin-bottom: 6px; -} - -.github-kanban-label { - padding: 2px 6px; - border-radius: 10px; - font-size: 0.75em; - font-weight: 500; -} - .github-kanban-label-more { font-size: 0.75em; color: var(--text-muted); From e6fe48400e6d0d046b9e9c52ad5b797fd903bedf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Platte?= Date: Fri, 16 Jan 2026 04:42:37 +0100 Subject: [PATCH 3/7] feat: Support for GitHub Sub-Issues #35 --- src/content-generator.ts | 6 +- src/file-manager.ts | 43 ++++++- src/github-client.ts | 90 +++++++++++++++ src/issue-file-manager.ts | 34 +++++- src/settings/project-renderer.ts | 13 +++ src/settings/repository-renderer.ts | 12 ++ src/types.ts | 3 + src/util/file-helpers.ts | 54 +++++++++ src/util/templateUtils.ts | 135 +++++++++++++++++++++- styles.css | 8 ++ templates/Template Variables Reference.md | 28 +++++ 11 files changed, 418 insertions(+), 8 deletions(-) diff --git a/src/content-generator.ts b/src/content-generator.ts index 46a39d7..62e17a2 100644 --- a/src/content-generator.ts +++ b/src/content-generator.ts @@ -22,6 +22,8 @@ export class ContentGenerator { comments: any[], settings: GitHubTrackerSettings, projectData?: ProjectData[], + subIssues?: any[], + parentIssue?: any, ): Promise { // Determine whether to escape hash tags (repo setting takes precedence if ignoreGlobalSettings is true) const shouldEscapeHashTags = repo.ignoreGlobalSettings ? repo.escapeHashTags : settings.escapeHashTags; @@ -37,7 +39,9 @@ export class ContentGenerator { settings.dateFormat, settings.escapeMode, shouldEscapeHashTags, - projectData + projectData, + subIssues, + parentIssue ); return processContentTemplate(templateContent, templateData, settings.dateFormat); } diff --git a/src/file-manager.ts b/src/file-manager.ts index efdfc5c..3d8041f 100644 --- a/src/file-manager.ts +++ b/src/file-manager.ts @@ -29,7 +29,7 @@ export class FileManager { private app: App, private settings: GitHubTrackerSettings, private noticeManager: NoticeManager, - gitHubClient: GitHubClient, + private gitHubClient: GitHubClient, ) { this.issueFileManager = new IssueFileManager(app, settings, noticeManager, gitHubClient); this.prFileManager = new PullRequestFileManager(app, settings, noticeManager, gitHubClient); @@ -159,6 +159,29 @@ export class FileManager { const repository = this.extractRepositoryFromUrl(content.url) || `${project.owner}/unknown`; const projectData = this.convertFieldValuesToProjectData(project, status, item.fieldValues?.nodes || []); + // Fetch sub-issues and parent issue for template support (only if enabled for project) + let subIssues: any[] = []; + let parentIssue: any = null; + + if (isIssue && project.includeSubIssues) { + const [owner, repoName] = repository.split("/"); + if (owner && repoName) { + subIssues = await this.gitHubClient.fetchSubIssues(owner, repoName, content.number); + parentIssue = await this.gitHubClient.fetchParentIssue(owner, repoName, content.number); + + // Enrich sub-issues with vault paths if they exist + const noteTemplate = project.issueNoteTemplate || "Issue - {number} - {title}"; + subIssues = await this.fileHelpers.enrichSubIssuesWithVaultPaths( + subIssues, + folderPath, + noteTemplate, + repository, + this.settings.dateFormat, + this.settings.escapeMode + ); + } + } + const templateData = isIssue ? createIssueTemplateData( this.convertToIssueFormat(content), @@ -167,7 +190,9 @@ export class FileManager { this.settings.dateFormat, this.settings.escapeMode, this.settings.escapeHashTags, - [projectData] + [projectData], + subIssues, + parentIssue ) : createPullRequestTemplateData( this.convertToPullRequestFormat(content), @@ -193,7 +218,9 @@ export class FileManager { project, status, isIssue, - item.fieldValues?.nodes || [] + item.fieldValues?.nodes || [], + subIssues, + parentIssue ); if (existingFile && existingFile instanceof TFile) { @@ -226,6 +253,8 @@ export class FileManager { status: string, isIssue: boolean, fieldValues: any[], + subIssues?: any[], + parentIssue?: any, ): Promise { const shouldEscapeHashTags = this.settings.escapeHashTags; @@ -255,7 +284,9 @@ export class FileManager { this.settings.dateFormat, this.settings.escapeMode, shouldEscapeHashTags, - [projectData] + [projectData], + subIssues, + parentIssue ) : createPullRequestTemplateData( this.convertToPullRequestFormat(content), @@ -272,7 +303,7 @@ export class FileManager { } // Fallback to default format - return this.generateDefaultProjectItemContent(content, project, status, isIssue, fieldValues); + return this.generateDefaultProjectItemContent(content, project, status, isIssue, fieldValues, subIssues, parentIssue); } /** @@ -284,6 +315,8 @@ export class FileManager { status: string, isIssue: boolean, fieldValues: any[], + subIssues?: any[], + parentIssue?: any, ): string { const shouldEscapeHashTags = this.settings.escapeHashTags; const dateFormat = this.settings.dateFormat; diff --git a/src/github-client.ts b/src/github-client.ts index ef1f955..584f44e 100644 --- a/src/github-client.ts +++ b/src/github-client.ts @@ -1041,6 +1041,96 @@ export class GitHubClient { } } + /** + * Fetch sub-issues for an issue + * Uses the GitHub Sub-Issues API: GET /repos/{owner}/{repo}/issues/{issue_number}/sub_issues + */ + public async fetchSubIssues( + owner: string, + repo: string, + issueNumber: number, + ): Promise { + if (!this.octokit) { + return []; + } + + try { + let allSubIssues: any[] = []; + let page = 1; + let hasMorePages = true; + + while (hasMorePages) { + const response = await this.octokit.request( + 'GET /repos/{owner}/{repo}/issues/{issue_number}/sub_issues', + { + owner, + repo, + issue_number: issueNumber, + per_page: 100, + page, + } + ); + + allSubIssues = [...allSubIssues, ...response.data]; + hasMorePages = response.data.length === 100; + page++; + } + + this.noticeManager.debug( + `Fetched ${allSubIssues.length} sub-issues for issue #${issueNumber}`, + ); + return allSubIssues; + } catch (error: any) { + // 404 means no sub-issues or feature not available + if (error.status === 404) { + return []; + } + this.noticeManager.debug( + `Error fetching sub-issues for issue #${issueNumber}: ${error.message}`, + ); + return []; + } + } + + /** + * Fetch parent issue for a sub-issue + * Uses the GitHub Sub-Issues API: GET /repos/{owner}/{repo}/issues/{issue_number}/parent + */ + public async fetchParentIssue( + owner: string, + repo: string, + issueNumber: number, + ): Promise { + if (!this.octokit) { + return null; + } + + try { + const response = await this.octokit.request( + 'GET /repos/{owner}/{repo}/issues/{issue_number}/parent', + { + owner, + repo, + issue_number: issueNumber, + } + ); + + this.noticeManager.debug( + `Found parent issue #${response.data.number} for issue #${issueNumber}`, + ); + return response.data; + } catch (error: any) { + // 404 means no parent issue + if (error.status === 404) { + return null; + } + this.noticeManager.debug( + `Error fetching parent issue for #${issueNumber}: ${error.message}`, + ); + return null; + } + } + public dispose(): void { this.octokit = null; this.currentUser = ""; diff --git a/src/issue-file-manager.ts b/src/issue-file-manager.ts index 40fb7a9..63972d8 100644 --- a/src/issue-file-manager.ts +++ b/src/issue-file-manager.ts @@ -113,7 +113,36 @@ export class IssueFileManager { ); } - let content = await this.contentGenerator.createIssueContent(issue, repo, comments, this.settings); + // Fetch sub-issues and parent issue for template support (only if enabled) + let subIssues: any[] = []; + let parentIssue: any = null; + + if (repo.includeSubIssues) { + subIssues = await this.gitHubClient.fetchSubIssues(owner, repoName, issue.number); + parentIssue = await this.gitHubClient.fetchParentIssue(owner, repoName, issue.number); + + // Enrich sub-issues with vault paths if they exist + const issueFolder = this.folderPathManager.getIssueFolderPath(repo, owner, repoName); + const noteTemplate = repo.issueNoteTemplate || "Issue - {number}"; + subIssues = await this.fileHelpers.enrichSubIssuesWithVaultPaths( + subIssues, + issueFolder, + noteTemplate, + repo.repository, + this.settings.dateFormat, + this.settings.escapeMode + ); + } + + let content = await this.contentGenerator.createIssueContent( + issue, + repo, + comments, + this.settings, + undefined, // projectData + subIssues, + parentIssue + ); if (file) { if (file instanceof TFile) { @@ -146,6 +175,9 @@ export class IssueFileManager { repo, comments, this.settings, + undefined, // projectData + subIssues, + parentIssue ); // Merge persist blocks back into new content diff --git a/src/settings/project-renderer.ts b/src/settings/project-renderer.ts index 875fb13..80e4e3d 100644 --- a/src/settings/project-renderer.ts +++ b/src/settings/project-renderer.ts @@ -156,6 +156,19 @@ export class ProjectRenderer { }); }); + new Setting(issuesSettingsContainer) + .setName("Include sub-issues") + .setDesc("If enabled, sub-issues will be included in the generated files") + .addToggle((toggle) => + toggle + .setValue(project.includeSubIssues ?? false) + .onChange(async (value) => { + project.includeSubIssues = value; + await this.plugin.saveSettings(); + }), + ); + } + // ===== PULL REQUESTS STORAGE SECTION ===== new Setting(container).setName("Pull Requests Storage").setHeading(); diff --git a/src/settings/repository-renderer.ts b/src/settings/repository-renderer.ts index 460ff72..2523824 100644 --- a/src/settings/repository-renderer.ts +++ b/src/settings/repository-renderer.ts @@ -294,6 +294,18 @@ export class RepositoryRenderer { await this.plugin.saveSettings(); }), ); + + new Setting(issuesSettingsContainer) + .setName("Include sub-issues") + .setDesc("If enabled, sub-issues will be included in the generated files") + .addToggle((toggle) => + toggle + .setValue(repo.includeSubIssues ?? false) + .onChange(async (value) => { + repo.includeSubIssues = value; + await this.plugin.saveSettings(); + }), + ); } renderPullRequestSettings( diff --git a/src/types.ts b/src/types.ts index f20ed40..38f62d5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -36,6 +36,7 @@ export interface RepositoryTracking { includeClosedIssues: boolean; includeClosedPullRequests: boolean; escapeHashTags: boolean; + includeSubIssues: boolean; } // Basic project info for selection UI @@ -81,6 +82,7 @@ export interface TrackedProject { showEmptyColumns?: boolean; hiddenStatuses?: string[]; skipHiddenStatusesOnSync?: boolean; + includeSubIssues?: boolean; } // GitHub Projects v2 types @@ -235,4 +237,5 @@ export const DEFAULT_REPOSITORY_TRACKING: RepositoryTracking = { includeClosedIssues: false, includeClosedPullRequests: false, escapeHashTags: false, + includeSubIssues: false, }; diff --git a/src/util/file-helpers.ts b/src/util/file-helpers.ts index 517cd61..0fe50a5 100644 --- a/src/util/file-helpers.ts +++ b/src/util/file-helpers.ts @@ -2,6 +2,7 @@ import { App, TFile } from "obsidian"; import { format } from "date-fns"; import { escapeBody } from "./escapeUtils"; import { NoticeManager } from "../notice-manager"; +import { createIssueTemplateData, processFilenameTemplate } from "./templateUtils"; export class FileHelpers { constructor( @@ -94,4 +95,57 @@ export class FileHelpers { return commentSection; } + + /** + * Enrich sub-issues with vault paths if the corresponding files exist + * This allows templates to use internal Obsidian links instead of GitHub URLs + */ + public async enrichSubIssuesWithVaultPaths( + subIssues: any[], + issueFolder: string, + noteTemplate: string, + repository: string, + dateFormat: string, + escapeMode: "disabled" | "normal" | "strict" | "veryStrict" + ): Promise { + if (!subIssues || subIssues.length === 0) { + return subIssues; + } + + return Promise.all(subIssues.map(async (subIssue) => { + const templateData = createIssueTemplateData( + { + title: subIssue.title || "Untitled", + number: subIssue.number, + state: subIssue.state || "open", + user: { login: subIssue.user?.login || "unknown" }, + created_at: subIssue.created_at || new Date().toISOString(), + updated_at: subIssue.updated_at || new Date().toISOString(), + html_url: subIssue.html_url || subIssue.url || "", + body: subIssue.body || "", + comments: 0, + locked: false, + }, + repository, + [], + dateFormat, + escapeMode, + false + ); + + const expectedFilename = processFilenameTemplate(noteTemplate, templateData, dateFormat); + const expectedPath = `${issueFolder}/${expectedFilename}.md`; + + // Check if the file exists in the vault + const file = this.app.vault.getAbstractFileByPath(expectedPath); + if (file instanceof TFile) { + return { + ...subIssue, + vaultPath: expectedFilename, + }; + } + + return subIssue; + })); + } } diff --git a/src/util/templateUtils.ts b/src/util/templateUtils.ts index 1f79c71..6e85274 100644 --- a/src/util/templateUtils.ts +++ b/src/util/templateUtils.ts @@ -9,6 +9,27 @@ import { ProjectData } from "../types"; /** * Represents the data available for template replacement */ +/** + * Sub-issue data for template replacement + */ +interface SubIssueData { + number: number; + title: string; + state: string; + url: string; + vaultPath?: string; // Path to the sub-issue file in the vault (if it exists) +} + +/** + * Parent issue data for template replacement + */ +interface ParentIssueData { + number: number; + title: string; + state: string; + url: string; +} + interface TemplateData { title: string; title_yaml: string; @@ -42,6 +63,9 @@ interface TemplateData { comments?: string; // Formatted comments section // GitHub Projects fields projectData?: ProjectData[]; + // Sub-issues fields + subIssues?: SubIssueData[]; + parentIssue?: ParentIssueData; } /** @@ -245,6 +269,87 @@ export function processTemplate( replacements["{project_fields}"] = ""; } + // Add Sub-Issues variables + if (data.subIssues && data.subIssues.length > 0) { + // Count open and closed sub-issues + const closedCount = data.subIssues.filter(si => si.state === "closed").length; + const openCount = data.subIssues.length - closedCount; + const totalCount = data.subIssues.length; + + // Sub-issues counts + replacements["{sub_issues_count}"] = totalCount.toString(); + replacements["{sub_issues_open}"] = openCount.toString(); + replacements["{sub_issues_closed}"] = closedCount.toString(); + + // Progress indicator (e.g., "2 of 5") + replacements["{sub_issues_progress}"] = `${closedCount} of ${totalCount}`; + + // Sub-issues as comma-separated links + replacements["{sub_issues}"] = data.subIssues + .map(si => `[#${si.number}](${si.url})`) + .join(", "); + + // Sub-issues as markdown list with status indicators + replacements["{sub_issues_list}"] = data.subIssues + .map(si => { + const isClosed = si.state === "closed"; + const cssClass = isClosed + ? "github-issues-sub-issue-closed" + : "github-issues-sub-issue-open"; + const statusIcon = ``; + const link = si.vaultPath + ? `[[${si.vaultPath}|#${si.number} ${si.title}]]` + : `[#${si.number} ${si.title}](${si.url})`; + return `- ${statusIcon} ${link}`; + }) + .join("\n"); + + // Sub-issues as simple list (without status icons) + replacements["{sub_issues_simple_list}"] = data.subIssues + .map(si => { + const link = si.vaultPath + ? `[[${si.vaultPath}|#${si.number} ${si.title}]]` + : `[#${si.number} ${si.title}](${si.url})`; + return `- ${link}`; + }) + .join("\n"); + + // Sub-issues for YAML frontmatter + replacements["{sub_issues_yaml}"] = `[${data.subIssues + .map(si => si.number) + .join(", ")}]`; + + // Sub-issues numbers only + replacements["{sub_issues_numbers}"] = data.subIssues + .map(si => `#${si.number}`) + .join(", "); + } else { + replacements["{sub_issues_count}"] = "0"; + replacements["{sub_issues_open}"] = "0"; + replacements["{sub_issues_closed}"] = "0"; + replacements["{sub_issues_progress}"] = "0 of 0"; + replacements["{sub_issues}"] = ""; + replacements["{sub_issues_list}"] = ""; + replacements["{sub_issues_simple_list}"] = ""; + replacements["{sub_issues_yaml}"] = "[]"; + replacements["{sub_issues_numbers}"] = ""; + } + + // Add Parent Issue variables + if (data.parentIssue) { + replacements["{parent_issue}"] = data.parentIssue.title; + replacements["{parent_issue_number}"] = data.parentIssue.number.toString(); + replacements["{parent_issue_url}"] = data.parentIssue.url; + replacements["{parent_issue_link}"] = `[#${data.parentIssue.number} ${data.parentIssue.title}](${data.parentIssue.url})`; + replacements["{parent_issue_state}"] = data.parentIssue.state; + } else { + replacements["{parent_issue}"] = ""; + replacements["{parent_issue_number}"] = ""; + replacements["{parent_issue_url}"] = ""; + replacements["{parent_issue_link}"] = ""; + replacements["{parent_issue_state}"] = ""; + } + // Replace all variables for (const [placeholder, value] of Object.entries(replacements)) { result = result.replace(new RegExp(escapeRegExp(placeholder), "g"), value); @@ -362,6 +467,11 @@ function getVariableValue(variableName: string, data: TemplateData): string | un case "project_priority": return data.projectData && data.projectData.length > 0 ? data.projectData[0].priority : undefined; case "project_iteration": return data.projectData && data.projectData.length > 0 && data.projectData[0].iteration ? data.projectData[0].iteration.title : undefined; case "projects": return data.projectData && data.projectData.length > 0 ? "true" : undefined; + // Sub-issues conditionals + case "sub_issues": return data.subIssues && data.subIssues.length > 0 ? "true" : undefined; + case "sub_issues_count": return data.subIssues && data.subIssues.length > 0 ? data.subIssues.length.toString() : undefined; + case "parent_issue": return data.parentIssue ? data.parentIssue.title : undefined; + case "parent_issue_number": return data.parentIssue ? data.parentIssue.number.toString() : undefined; default: return undefined; } } @@ -375,6 +485,8 @@ function getVariableValue(variableName: string, data: TemplateData): string | un * @param escapeMode Escape mode for text * @param escapeHashTags Whether to escape hash tags * @param projectData Optional project data from GitHub Projects + * @param subIssues Optional array of sub-issues + * @param parentIssue Optional parent issue data * @returns TemplateData object */ export function createIssueTemplateData( @@ -384,13 +496,32 @@ export function createIssueTemplateData( dateFormat: string = "", escapeMode: "disabled" | "normal" | "strict" | "veryStrict" = "normal", escapeHashTags: boolean = false, - projectData?: ProjectData[] + projectData?: ProjectData[], + subIssues?: any[], + parentIssue?: any ): TemplateData { const [owner, repoName] = repository.split("/"); // Ensure milestone data is properly extracted const milestoneTitle = issue.milestone?.title || issue.milestone?.name || ""; + // Convert raw sub-issues to SubIssueData format + const subIssueData: SubIssueData[] | undefined = subIssues?.map(si => ({ + number: si.number, + title: si.title, + state: si.state || "open", + url: si.html_url || si.url || "", + vaultPath: si.vaultPath, // Path to the sub-issue file in the vault (if it exists) + })); + + // Convert raw parent issue to ParentIssueData format + const parentIssueData: ParentIssueData | undefined = parentIssue ? { + number: parentIssue.number, + title: parentIssue.title, + state: parentIssue.state || "open", + url: parentIssue.html_url || parentIssue.url || "", + } : undefined; + return { title: issue.title || "Untitled", title_yaml: escapeYamlString(issue.title || "Untitled"), @@ -416,6 +547,8 @@ export function createIssueTemplateData( lockReason: issue.active_lock_reason || "", comments: formatComments(comments, dateFormat, escapeMode, escapeHashTags), projectData: projectData, + subIssues: subIssueData, + parentIssue: parentIssueData, }; } diff --git a/styles.css b/styles.css index 8083673..b6a7c62 100644 --- a/styles.css +++ b/styles.css @@ -2258,3 +2258,11 @@ button:disabled { background-color: var(--background-modifier-border); border-radius: 4px; } + +.github-issues-sub-issue-open { + color: #22c55e; +} + +.github-issues-sub-issue-closed { + color: #ef4444; +} diff --git a/templates/Template Variables Reference.md b/templates/Template Variables Reference.md index d86a843..f11dcbb 100644 --- a/templates/Template Variables Reference.md +++ b/templates/Template Variables Reference.md @@ -104,6 +104,34 @@ These variables are available when the issue/PR is part of a GitHub Project (Pro | `{project_fields}` | All custom fields as YAML | ` Effort: "5"` (with newlines) | | `{project_field:FieldName}` | Access specific custom field by name | `{project_field:Effort}` → "5" | +## Sub-Issues + +These variables are available when sub-issues are enabled and the issue has sub-issues. + +| Variable | Description | Example | +|----------|-------------|---------| +| `{sub_issues_count}` | Total number of sub-issues | "5" | +| `{sub_issues_open}` | Number of open sub-issues | "3" | +| `{sub_issues_closed}` | Number of closed sub-issues | "2" | +| `{sub_issues_progress}` | Progress indicator (closed/total) | "2 of 5" | +| `{sub_issues}` | Sub-issues as comma-separated links | "[#123](url), [#124](url)" | +| `{sub_issues_list}` | Sub-issues as markdown list with status | "- ● [#123 Title](url)
- ● [#124 Title](url)" | +| `{sub_issues_simple_list}` | Sub-issues as simple markdown list | "- [#123 Title](url)
- [#124 Title](url)" | +| `{sub_issues_yaml}` | Sub-issue numbers as YAML array | "[123, 124, 125]" | +| `{sub_issues_numbers}` | Sub-issue numbers only | "#123, #124, #125" | + +## Parent Issues + +These variables are available when the issue is a sub-issue itself. + +| Variable | Description | Example | +|----------|-------------|---------| +| `{parent_issue}` | Parent issue title | "Main Feature Implementation" | +| `{parent_issue_number}` | Parent issue number | "456" | +| `{parent_issue_url}` | Parent issue URL | "https://github.com/owner/repo/issues/456" | +| `{parent_issue_link}` | Parent issue as markdown link | "[#456 Main Feature](url)" | +| `{parent_issue_state}` | Parent issue status | "open" or "closed" | + ## Conditional Blocks | Syntax | Description | Example | From 7327f4ad9f6c77d35f82752578f4dd7e3f4e3a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Platte?= Date: Fri, 16 Jan 2026 06:13:23 +0100 Subject: [PATCH 4/7] docs: create Wiki and remove inline template help - Simplify settings descriptions --- README.md | 6 + src/settings-tab.ts | 29 +-- src/settings/project-renderer.ts | 25 +-- src/settings/repository-renderer.ts | 34 ++-- src/settings/ui-helpers.ts | 227 ---------------------- styles.css | 12 +- templates/Template Variables Reference.md | 146 -------------- templates/default-issue-template.md | 39 ---- templates/default-pr-template.md | 51 ----- templates/detailed-template.md | 48 ----- templates/minimal-template.md | 42 ---- templates/project-template.md | 59 ------ 12 files changed, 51 insertions(+), 667 deletions(-) delete mode 100644 templates/Template Variables Reference.md delete mode 100644 templates/default-issue-template.md delete mode 100644 templates/default-pr-template.md delete mode 100644 templates/detailed-template.md delete mode 100644 templates/minimal-template.md delete mode 100644 templates/project-template.md diff --git a/README.md b/README.md index f8dc7d0..73eb024 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,12 @@ An Obsidian plugin that integrates with GitHub to track issues and pull requests - Custom field support (status, priority, iteration) - Project-specific filtering and organization +### 🔗 Sub-Issues Support +- Track GitHub sub-issues (parent/child relationships) +- Display sub-issues list with status indicators +- Navigate between parent and child issues +- Progress tracking with completion percentage + ### 📝 Markdown Notes - Create markdown notes for each issue or PR - Customizable filename templates with variables diff --git a/src/settings-tab.ts b/src/settings-tab.ts index 12ad094..ab50f44 100644 --- a/src/settings-tab.ts +++ b/src/settings-tab.ts @@ -63,9 +63,20 @@ export class GitHubTrackerSettingTab extends PluginSettingTab { const linksContainer = subtitleContainer.createDiv({ cls: "github-issues-subtitle-links" }); + const wikiLink = linksContainer.createEl("a", { + href: "https://github.com/LonoxX/obsidian-github-issues/wiki", + cls: "github-issues-header-link", + }); + wikiLink.setAttribute("target", "_blank"); + const wikiIcon = wikiLink.createSpan({ cls: "github-issues-link-icon" }); + setIcon(wikiIcon, "book-open"); + wikiLink.createSpan({ text: "Wiki" }); + + linksContainer.createSpan({ text: " • " }); + const bugLink = linksContainer.createEl("a", { href: "https://github.com/LonoxX/obsidian-github-issues/issues/new", - cls: "github-issues-bug-link", + cls: "github-issues-header-link", }); bugLink.setAttribute("target", "_blank"); const bugIcon = bugLink.createSpan({ cls: "github-issues-link-icon" }); @@ -76,7 +87,7 @@ export class GitHubTrackerSettingTab extends PluginSettingTab { const sponsorLink = linksContainer.createEl("a", { href: "https://github.com/sponsors/LonoxX", - cls: "github-issues-sponsor-link", + cls: "github-issues-header-link", }); sponsorLink.setAttribute("target", "_blank"); const sponsorIcon = sponsorLink.createSpan({ cls: "github-issues-link-icon" }); @@ -87,7 +98,7 @@ export class GitHubTrackerSettingTab extends PluginSettingTab { const kofiLink = linksContainer.createEl("a", { href: "https://ko-fi.com/lonoxx", - cls: "github-issues-kofi-link", + cls: "github-issues-header-link", }); kofiLink.setAttribute("target", "_blank"); const kofiIcon = kofiLink.createSpan({ cls: "github-issues-link-icon" }); @@ -98,7 +109,7 @@ export class GitHubTrackerSettingTab extends PluginSettingTab { const bmcLink = linksContainer.createEl("a", { href: "https://buymeacoffee.com/lonoxx", - cls: "github-issues-bmc-link", + cls: "github-issues-header-link", }); bmcLink.setAttribute("target", "_blank"); const bmcIcon = bmcLink.createSpan({ cls: "github-issues-link-icon" }); @@ -298,7 +309,7 @@ export class GitHubTrackerSettingTab extends PluginSettingTab { new Setting(syncContainer) .setName("Cleanup closed items after (days)") - .setDesc("Delete local files for items closed longer than this many days") + .setDesc("Delete files for items closed longer than X days") .addText((text) => text .setPlaceholder("30") @@ -343,12 +354,6 @@ export class GitHubTrackerSettingTab extends PluginSettingTab { const advancedContainer = containerEl.createDiv("github-issues-settings-group"); new Setting(advancedContainer).setName("Advanced Settings").setHeading(); - // Template variables help - UIHelpers.addTemplateVariablesHelp(advancedContainer, 'issue'); - - // Persist blocks help - UIHelpers.addPersistBlocksHelp(advancedContainer); - new Setting(advancedContainer) .setName("Date format") .setDesc("Format for dates in issue files (e.g., yyyy-MM-dd HH:mm:ss)") @@ -448,7 +453,7 @@ export class GitHubTrackerSettingTab extends PluginSettingTab { new Setting(advancedContainer) .setName("Escape hash tags") - .setDesc("Escape # characters that are not valid Markdown headers to prevent unintended Obsidian tags (e.g., #134 becomes \\#134)") + .setDesc("Escape # to prevent unintended Obsidian tags") .addToggle((toggle) => toggle .setValue(this.plugin.settings.escapeHashTags) diff --git a/src/settings/project-renderer.ts b/src/settings/project-renderer.ts index 80e4e3d..ee71cea 100644 --- a/src/settings/project-renderer.ts +++ b/src/settings/project-renderer.ts @@ -35,7 +35,7 @@ export class ProjectRenderer { const issueFolderSetting = new Setting(standardIssueFolderContainer) .setName("Issues folder template") - .setDesc("Folder path template. Variables: {project}, {owner}, {project_number}") + .setDesc("Folder path template for issue storage") .addText((text) => { text .setPlaceholder("GitHub/{project}") @@ -89,12 +89,10 @@ export class ProjectRenderer { new FolderSuggest(this.app, text.inputEl); }); - // Issue filename template with FULL variable list + // Issue filename template new Setting(issueStorageContainer) .setName("Issue filename template") - .setDesc( - "Variables: {number}, {title}, {author}, {status}, {project}, {type}, {labels}, {assignees}, {owner}, {repoName}, {labels_hash}, {created}, {updated}" - ) + .setDesc("Template for issue filenames") .addText((text) => text .setPlaceholder("Issue - {number}") @@ -108,7 +106,7 @@ export class ProjectRenderer { // Issue Content Template Settings new Setting(issueStorageContainer) .setName("Use custom issue content template") - .setDesc("Enable custom template file for issue content instead of the default format") + .setDesc("Use custom template for issue content") .addToggle((toggle) => { toggle .setValue(project.useCustomIssueContentTemplate ?? false) @@ -156,9 +154,9 @@ export class ProjectRenderer { }); }); - new Setting(issuesSettingsContainer) + new Setting(issueStorageContainer) .setName("Include sub-issues") - .setDesc("If enabled, sub-issues will be included in the generated files") + .setDesc("Include sub-issues in generated files") .addToggle((toggle) => toggle .setValue(project.includeSubIssues ?? false) @@ -167,7 +165,6 @@ export class ProjectRenderer { await this.plugin.saveSettings(); }), ); - } // ===== PULL REQUESTS STORAGE SECTION ===== new Setting(container).setName("Pull Requests Storage").setHeading(); @@ -185,7 +182,7 @@ export class ProjectRenderer { new Setting(standardPrFolderContainer) .setName("Pull requests folder template") - .setDesc("Folder path template. Variables: {project}, {owner}, {project_number}") + .setDesc("Folder path template for PR storage") .addText((text) => { text .setPlaceholder("GitHub/{project}") @@ -239,12 +236,10 @@ export class ProjectRenderer { new FolderSuggest(this.app, text.inputEl); }); - // PR filename template with FULL variable list + // PR filename template new Setting(prStorageContainer) .setName("PR filename template") - .setDesc( - "Variables: {number}, {title}, {author}, {status}, {project}, {type}, {labels}, {assignees}, {owner}, {repoName}, {labels_hash}, {created}, {updated}" - ) + .setDesc("Template for pull request filenames") .addText((text) => text .setPlaceholder("PR - {number}") @@ -258,7 +253,7 @@ export class ProjectRenderer { // PR Content Template Settings new Setting(prStorageContainer) .setName("Use custom PR content template") - .setDesc("Enable custom template file for PR content instead of the default format") + .setDesc("Use custom template for PR content") .addToggle((toggle) => { toggle .setValue(project.useCustomPullRequestContentTemplate ?? false) diff --git a/src/settings/repository-renderer.ts b/src/settings/repository-renderer.ts index 2523824..c4709c6 100644 --- a/src/settings/repository-renderer.ts +++ b/src/settings/repository-renderer.ts @@ -94,7 +94,7 @@ export class RepositoryRenderer { new Setting(issuesSettingsContainer) .setName("Use custom folder") - .setDesc("Instead of organizing issues by Owner/Repository, place all issues in a custom folder") + .setDesc("Use custom folder instead of Owner/Repository structure") .addToggle((toggle) => { toggle .setValue(repo.useCustomIssueFolder) @@ -116,7 +116,7 @@ export class RepositoryRenderer { new Setting(customIssuesFolderContainer) .setName("Custom issues folder") - .setDesc("Specific folder path where all issues will be placed (overrides the folder structure)") + .setDesc("Custom folder path for all issues") .addText((text) => { text .setPlaceholder("e.g., Issues, GitHub/All Issues") @@ -174,7 +174,7 @@ export class RepositoryRenderer { new Setting(issuesSettingsContainer) .setName("Default: Allow issue deletion") .setDesc( - "If enabled, issue files will be set to be deleted from your vault when the issue is closed or no longer matches your filter criteria", + "Delete issue files when closed or filtered out", ) .addToggle((toggle) => { allowDeleteIssueToggle = toggle; @@ -189,9 +189,7 @@ export class RepositoryRenderer { new Setting(issuesSettingsContainer) .setName("Issue note template") - .setDesc( - "Template for issue note filenames. Available variables: {title}, {number}, {status}, {author}, {assignee}, {labels}, {repository}, {owner}, {repoName}, {type}, {created}, {updated}. Example: \"{title} - Issue {number}\"" - ) + .setDesc("Template for issue filenames") .addText((text) => text .setPlaceholder("Issue - {number}") @@ -204,7 +202,7 @@ export class RepositoryRenderer { new Setting(issuesSettingsContainer) .setName("Use custom issue content template") - .setDesc("Enable custom template file for issue content instead of the default format") + .setDesc("Use custom template for issue content") .addToggle((toggle) => { toggle .setValue(repo.useCustomIssueContentTemplate) @@ -258,7 +256,7 @@ export class RepositoryRenderer { new Setting(issuesSettingsContainer) .setName("Include issue comments") .setDesc( - "If enabled, comments from issues will be included in the generated files", + "Include comments in generated files", ) .addToggle((toggle) => toggle @@ -272,7 +270,7 @@ export class RepositoryRenderer { new Setting(issuesSettingsContainer) .setName("Include closed issues") .setDesc( - "If enabled, closed issues will also be created and updated (not just deleted after the cleanup period).", + "Include closed issues in tracking", ) .addToggle((toggle) => toggle @@ -297,7 +295,7 @@ export class RepositoryRenderer { new Setting(issuesSettingsContainer) .setName("Include sub-issues") - .setDesc("If enabled, sub-issues will be included in the generated files") + .setDesc("Include sub-issues in generated files") .addToggle((toggle) => toggle .setValue(repo.includeSubIssues ?? false) @@ -394,7 +392,7 @@ export class RepositoryRenderer { new Setting(pullRequestsSettingsContainer) .setName("Use custom folder") - .setDesc("Instead of organizing pull requests by Owner/Repository, place all pull requests in a custom folder") + .setDesc("Use custom folder instead of Owner/Repository structure") .addToggle((toggle) => { toggle .setValue(repo.useCustomPullRequestFolder) @@ -407,7 +405,7 @@ export class RepositoryRenderer { new Setting(customPRFolderContainer) .setName("Custom pull requests folder") - .setDesc("Specific folder path where all pull requests will be placed (overrides the folder structure)") + .setDesc("Custom folder path for all pull requests") .addText((text) => { text .setPlaceholder("e.g., Pull Requests, GitHub/All PRs") @@ -462,9 +460,7 @@ export class RepositoryRenderer { new Setting(pullRequestsSettingsContainer) .setName("Pull request note template") - .setDesc( - "Template for pull request note filenames. Available variables: {title}, {number}, {status}, {author}, {assignee}, {labels}, {repository}, {owner}, {repoName}, {type}, {created}, {updated}. Example: \"{title} - PR {number}\"" - ) + .setDesc("Template for pull request filenames") .addText((text) => text .setPlaceholder("PR - {number}") @@ -477,7 +473,7 @@ export class RepositoryRenderer { new Setting(pullRequestsSettingsContainer) .setName("Use custom pull request content template") - .setDesc("Enable custom template file for pull request content instead of the default format") + .setDesc("Use custom template for PR content") .addToggle((toggle) => { toggle .setValue(repo.useCustomPullRequestContentTemplate) @@ -533,7 +529,7 @@ export class RepositoryRenderer { new Setting(pullRequestsSettingsContainer) .setName("Default: Allow pull request deletion") .setDesc( - "If enabled, pull request files will be set to be deleted from your vault when the pull request is closed or no longer matches your filter criteria. Automatically disabled when 'Include closed pull requests' is enabled.", + "Delete PR files when closed or filtered out", ) .addToggle((toggle) => { allowDeletePRToggle = toggle; @@ -549,7 +545,7 @@ export class RepositoryRenderer { new Setting(pullRequestsSettingsContainer) .setName("Include pull request comments") .setDesc( - "If enabled, comments from pull requests will be included in the generated files", + "Include comments in generated files", ) .addToggle((toggle) => toggle @@ -563,7 +559,7 @@ export class RepositoryRenderer { new Setting(pullRequestsSettingsContainer) .setName("Include closed pull requests") .setDesc( - "If enabled, closed pull requests will also be created and updated (not just deleted after the cleanup period). This is useful for building a knowledge base.", + "Include closed PRs in tracking", ) .addToggle((toggle) => toggle diff --git a/src/settings/ui-helpers.ts b/src/settings/ui-helpers.ts index e909492..d1ac8b6 100644 --- a/src/settings/ui-helpers.ts +++ b/src/settings/ui-helpers.ts @@ -1,179 +1,4 @@ -import { setIcon } from "obsidian"; - export class UIHelpers { - static addTemplateVariablesHelp(container: HTMLElement, type: 'issue' | 'pr'): void { - const helpContainer = container.createDiv("github-issues-template-help"); - - const details = helpContainer.createEl("details"); - const summary = details.createEl("summary"); - summary.textContent = "Available template variables"; - summary.addClass("github-issues-template-help-summary"); - - const variablesContainer = details.createDiv("github-issues-template-variables"); - - // Basic Information section - const basicTitle = variablesContainer.createEl("h4"); - basicTitle.textContent = "Basic Information"; - - const basicList = variablesContainer.createEl("ul"); - basicList.innerHTML = ` -
  • {title} - Issue/PR title
  • -
  • {title_yaml} - Issue/PR title (YAML-escaped for use in frontmatter)
  • -
  • {number} - Issue/PR number
  • -
  • {status} / {state} - Current status (open, closed, etc.)
  • -
  • {author} - Username who created the issue/PR
  • -
  • {body} - Issue/PR description/body
  • -
  • {url} - Web URL
  • -
  • {repository} - Full repository name (owner/repo)
  • -
  • {owner} - Repository owner
  • -
  • {repoName} - Repository name only
  • -
  • {type} - "issue" or "pr"
  • - `; - - // Assignees section - const assigneesTitle = variablesContainer.createEl("h4"); - assigneesTitle.textContent = "Assignees"; - - const assigneesList = variablesContainer.createEl("ul"); - assigneesList.innerHTML = ` -
  • {assignee} - Primary assignee (first one if multiple)
  • -
  • {assignees} - All assignees as comma-separated list
  • -
  • {assignees_list} - All assignees as bulleted list
  • -
  • {assignees_yaml} - All assignees as YAML inline array
  • - `; - - // Labels section - const labelsTitle = variablesContainer.createEl("h4"); - labelsTitle.textContent = "Labels"; - - const labelsList = variablesContainer.createEl("ul"); - labelsList.innerHTML = ` -
  • {labels} - All labels as comma-separated list
  • -
  • {labels_list} - All labels as bulleted list
  • -
  • {labels_hash} - All labels as hashtags (#label1 #label2)
  • -
  • {labels_yaml} - All labels as YAML inline array
  • - `; - - // Dates section - const datesTitle = variablesContainer.createEl("h4"); - datesTitle.textContent = "Dates"; - - const datesList = variablesContainer.createEl("ul"); - datesList.innerHTML = ` -
  • {created} - Creation date
  • -
  • {updated} - Last update date
  • -
  • {closed} - Closed date (if closed)
  • - `; - - // Pull Request Specific section (only if type is 'pr') - if (type === 'pr') { - const prTitle = variablesContainer.createEl("h4"); - prTitle.textContent = "Pull Request Specific"; - - const prList = variablesContainer.createEl("ul"); - prList.innerHTML = ` -
  • {mergedAt} - Merge date (if merged)
  • -
  • {mergeable} - Whether PR can be merged
  • -
  • {merged} - Whether PR is merged
  • -
  • {baseBranch} - Target branch
  • -
  • {headBranch} - Source branch
  • - `; - } - - // Additional Info section - const additionalTitle = variablesContainer.createEl("h4"); - additionalTitle.textContent = "Additional Info"; - - const additionalList = variablesContainer.createEl("ul"); - additionalList.innerHTML = ` -
  • {milestone} - Milestone title
  • -
  • {commentsCount} - Number of comments
  • -
  • {isLocked} - Whether issue/PR is locked
  • -
  • {lockReason} - Lock reason (if locked)
  • -
  • {comments} - Formatted comments section (available only in content templates)
  • - `; - - // Conditional Blocks section - const conditionalTitle = variablesContainer.createEl("h4"); - conditionalTitle.textContent = "Conditional Blocks"; - - const conditionalDesc = variablesContainer.createEl("p"); - conditionalDesc.innerHTML = `Use {variable:content} to show content only if the variable has a value.`; - - const conditionalExamples = variablesContainer.createEl("ul"); - conditionalExamples.innerHTML = ` -
  • {closed:- **Closed:** {closed}} - Shows "- Closed: [date]" only if closed
  • -
  • {milestone: Milestone: {milestone}} - Shows milestone info only if set
  • - `; - - // Examples section - const examplesTitle = variablesContainer.createEl("h4"); - examplesTitle.textContent = "Examples"; - - const examplesList = variablesContainer.createEl("ul"); - examplesList.innerHTML = ` -
  • "{title} - Issue {number}" → "Bug fix - Issue 123"
  • -
  • "{type} {number} - {title}" → "issue 123 - Bug fix"
  • -
  • "[{status}] {title} ({assignee})" → "[open] Bug fix (username)"
  • -
  • "{repoName}-{number} {title}" → "myproject-123 Bug fix"
  • -
  • "{closed:Closed on {closed}}" → "Closed on 2024-01-15" (only if closed)
  • - `; - } - - static addPersistBlocksHelp(container: HTMLElement): void { - const helpContainer = container.createDiv("github-issues-template-help"); - - const details = helpContainer.createEl("details"); - const summary = details.createEl("summary"); - summary.textContent = "Protect your custom notes with Persist Blocks"; - summary.addClass("github-issues-template-help-summary"); - - const contentContainer = details.createDiv("github-issues-template-variables"); - - // Introduction - const intro = contentContainer.createEl("p"); - intro.innerHTML = `Persist blocks allow you to add your own custom notes to GitHub issue and PR files without them being overwritten during sync.`; - - // Basic usage - const usageTitle = contentContainer.createEl("h4"); - usageTitle.textContent = "Basic Usage"; - - const usageExample = contentContainer.createEl("pre"); - usageExample.textContent = `{% persist "notes" %} -## My Notes -- Your custom content here -- Will never be overwritten! -{% endpersist %}`; - - // How it works - const howTitle = contentContainer.createEl("h4"); - howTitle.textContent = "How It Works"; - - const howList = contentContainer.createEl("ul"); - howList.innerHTML = ` -
  • Smart Updates: Files are only updated if GitHub data has changed (checks the updated field)
  • -
  • Content Protection: Everything inside persist blocks is preserved during sync
  • -
  • Position Preservation: Blocks stay exactly where you placed them using surrounding text as anchors
  • - `; - - // Multiple blocks - const multipleTitle = contentContainer.createEl("h4"); - multipleTitle.textContent = "Multiple Blocks"; - - const multipleDesc = contentContainer.createEl("p"); - multipleDesc.textContent = "You can have multiple persist blocks in one file. Each needs a unique name:"; - - const multipleExample = contentContainer.createEl("pre"); - multipleExample.textContent = `{% persist "notes" %} -Your notes here... -{% endpersist %} - -{% persist "todos" %} -- [ ] Task 1 -- [ ] Task 2 -{% endpersist %}`; - } - static getContrastColor(hexColor: string): string { const r = parseInt(hexColor.substr(0, 2), 16); const g = parseInt(hexColor.substr(2, 2), 16); @@ -181,56 +6,4 @@ Your notes here... const brightness = (r * 299 + g * 587 + b * 114) / 1000; return brightness > 128 ? "#000000" : "#ffffff"; } - - static createSettingsHeader(container: HTMLElement): void { - const headerEl = container.createEl("div", { cls: "github-issues-settings-header" }); - headerEl.createEl("h2", { text: "GitHub Issues & Pull Requests" }); - - const subtitleContainer = headerEl.createDiv({ cls: "github-issues-settings-subtitle" }); - subtitleContainer.createSpan({ text: "Sync your GitHub issues and pull requests in Obsidian" }); - - const linksContainer = subtitleContainer.createDiv({ cls: "github-issues-subtitle-links" }); - - const bugLink = linksContainer.createEl("a", { - href: "https://github.com/LonoxX/obsidian-github-issues/issues/new", - cls: "github-issues-bug-link", - }); - bugLink.setAttribute("target", "_blank"); - const bugIcon = bugLink.createSpan({ cls: "github-issues-link-icon" }); - setIcon(bugIcon, "bug"); - bugLink.createSpan({ text: "Report Bug" }); - - linksContainer.createSpan({ text: " • " }); - - const sponsorLink = linksContainer.createEl("a", { - href: "https://github.com/sponsors/LonoxX", - cls: "github-issues-sponsor-link", - }); - sponsorLink.setAttribute("target", "_blank"); - const sponsorIcon = sponsorLink.createSpan({ cls: "github-issues-link-icon" }); - setIcon(sponsorIcon, "heart"); - sponsorLink.createSpan({ text: "Support me" }); - - linksContainer.createSpan({ text: " • " }); - - const kofiLink = linksContainer.createEl("a", { - href: "https://ko-fi.com/lonoxx", - cls: "github-issues-kofi-link", - }); - kofiLink.setAttribute("target", "_blank"); - const kofiIcon = kofiLink.createSpan({ cls: "github-issues-link-icon" }); - setIcon(kofiIcon, "coffee"); - kofiLink.createSpan({ text: "Ko-fi" }); - - linksContainer.createSpan({ text: " • " }); - - const bmcLink = linksContainer.createEl("a", { - href: "https://buymeacoffee.com/lonoxx", - cls: "github-issues-bmc-link", - }); - bmcLink.setAttribute("target", "_blank"); - const bmcIcon = bmcLink.createSpan({ cls: "github-issues-link-icon" }); - setIcon(bmcIcon, "pizza"); - bmcLink.createSpan({ text: "Buy me a Pizza" }); - } } diff --git a/styles.css b/styles.css index b6a7c62..a138632 100644 --- a/styles.css +++ b/styles.css @@ -36,11 +36,8 @@ gap: 4px; } -/* All subtitle links - shared button style */ -.github-issues-bug-link, -.github-issues-sponsor-link, -.github-issues-kofi-link, -.github-issues-bmc-link { +/* Header links - shared style */ +.github-issues-header-link { color: var(--text-muted); padding: 4px 10px; font-weight: 500; @@ -51,10 +48,7 @@ gap: 6px; } -.github-issues-bug-link:hover, -.github-issues-sponsor-link:hover, -.github-issues-kofi-link:hover, -.github-issues-bmc-link:hover { +.github-issues-header-link:hover { color: var(--text-muted) !important; text-decoration: none !important; transform: translateY(-1px); diff --git a/templates/Template Variables Reference.md b/templates/Template Variables Reference.md deleted file mode 100644 index f11dcbb..0000000 --- a/templates/Template Variables Reference.md +++ /dev/null @@ -1,146 +0,0 @@ -# Template Variables Reference - -## Basic Information - -| Variable | Description | Example | -|----------|-------------|---------| -| `{title}` | Issue/PR title | "Fix login bug" | -| `{title_yaml}` | Issue/PR title (YAML-escaped) | "Fix \"login\" bug" | -| `{number}` | Issue/PR number | "123" | -| `{status}` | Current status | "open", "closed" | -| `{state}` | Synonym for status | "open", "closed" | -| `{author}` | Username of creator | "octocat" | -| `{body}` | Issue/PR description | Full description text | -| `{repository}` | Full repository name | "owner/repo-name" | -| `{owner}` | Repository owner | "octocat" | -| `{repoName}` | Repository name only | "repo-name" | -| `{type}` | Type of element | "issue" or "pr" | - -## URLs and Links - -| Variable | Description | Example | -|----------|-------------|---------| -| `{url}` | GitHub Web URL | "https://github.com/owner/repo/issues/123" | - -## Assignees - -| Variable | Description | Example | -|----------|-------------|---------| -| `{assignee}` | Primary assignee (first one) | "octocat" | -| `{assignees}` | All assignees as comma-separated list | "octocat, user2, user3" | -| `{assignees_list}` | All assignees as bullet list | "- octocat
    - user2
    - user3" | -| `{assignees_yaml}` | All assignees as YAML array | `["octocat", "user2", "user3"]` | - -## Labels - -| Variable | Description | Example | -|----------|-------------|---------| -| `{labels}` | All labels as comma-separated list | "bug, enhancement, priority-high" | -| `{labels_list}` | All labels as bullet list | "- bug
    - enhancement
    - priority-high" | -| `{labels_hash}` | All labels as hashtags | "#bug #enhancement #priority-high" | -| `{labels_yaml}` | All labels as YAML array | `["bug", "enhancement", "priority-high"]` | - -## Dates - -| Variable | Description | Example | -|----------|-------------|---------| -| `{created}` | Creation date | "9/5/2025" | -| `{updated}` | Last update date | "1/20/2024" | -| `{closed}` | Closing date (if closed) | "1/25/2024" | - -## Pull Request Specific Variables - -| Variable | Description | Example | -|----------|-------------|---------| -| `{mergedAt}` | Merge date (if merged) | "9/21/2025" | -| `{mergeable}` | Whether PR can be merged | "true" or "false" | -| `{merged}` | Whether PR was merged | "true" or "false" | -| `{baseBranch}` | Target branch | "main" | -| `{headBranch}` | Source branch | "feature/new-login" | - -## Additional Information - -| Variable | Description | Example | -|----------|-------------|---------| -| `{milestone}` | Milestone title | "v1.2.0 Release" | -| `{commentsCount}` | Number of comments | "5" | -| `{isLocked}` | Whether issue/PR is locked | "true" or "false" | -| `{lockReason}` | Reason for locking | "resolved", "spam", "off-topic" | -| `{comments}` | Formatted comments section | Complete comments with formatting | - -## GitHub Projects - -These variables are available when the issue/PR is part of a GitHub Project (Projects V2). - -### Basic Project Information - -| Variable | Description | Example | -|----------|-------------|---------| -| `{project}` | Project title (first project if in multiple) | "Sprint Board" | -| `{project_url}` | Project URL | "https://github.com/orgs/owner/projects/1" | -| `{project_number}` | Project number | "1" | -| `{project_status}` | Status field value | "In Progress", "Done" | -| `{project_priority}` | Priority field value | "High", "Medium", "Low" | - -### Iteration Information - -| Variable | Description | Example | -|----------|-------------|---------| -| `{project_iteration}` | Current iteration title | "Sprint 5" | -| `{project_iteration_start}` | Iteration start date | "2025-01-15" | -| `{project_iteration_duration}` | Iteration duration in days | "14" | - -### Multiple Projects - -| Variable | Description | Example | -|----------|-------------|---------| -| `{projects}` | All project names as comma-separated list | "Sprint Board, Backlog" | -| `{projects_yaml}` | All project names as YAML array | `["Sprint Board", "Backlog"]` | - -### Custom Fields - -| Variable | Description | Example | -|----------|-------------|---------| -| `{project_fields}` | All custom fields as YAML | ` Effort: "5"` (with newlines) | -| `{project_field:FieldName}` | Access specific custom field by name | `{project_field:Effort}` → "5" | - -## Sub-Issues - -These variables are available when sub-issues are enabled and the issue has sub-issues. - -| Variable | Description | Example | -|----------|-------------|---------| -| `{sub_issues_count}` | Total number of sub-issues | "5" | -| `{sub_issues_open}` | Number of open sub-issues | "3" | -| `{sub_issues_closed}` | Number of closed sub-issues | "2" | -| `{sub_issues_progress}` | Progress indicator (closed/total) | "2 of 5" | -| `{sub_issues}` | Sub-issues as comma-separated links | "[#123](url), [#124](url)" | -| `{sub_issues_list}` | Sub-issues as markdown list with status | "- ● [#123 Title](url)
    - ● [#124 Title](url)" | -| `{sub_issues_simple_list}` | Sub-issues as simple markdown list | "- [#123 Title](url)
    - [#124 Title](url)" | -| `{sub_issues_yaml}` | Sub-issue numbers as YAML array | "[123, 124, 125]" | -| `{sub_issues_numbers}` | Sub-issue numbers only | "#123, #124, #125" | - -## Parent Issues - -These variables are available when the issue is a sub-issue itself. - -| Variable | Description | Example | -|----------|-------------|---------| -| `{parent_issue}` | Parent issue title | "Main Feature Implementation" | -| `{parent_issue_number}` | Parent issue number | "456" | -| `{parent_issue_url}` | Parent issue URL | "https://github.com/owner/repo/issues/456" | -| `{parent_issue_link}` | Parent issue as markdown link | "[#456 Main Feature](url)" | -| `{parent_issue_state}` | Parent issue status | "open" or "closed" | - -## Conditional Blocks - -| Syntax | Description | Example | -|--------|-------------|---------| -| `{variable:content}` | Shows content only if variable has a value | `{milestone:Milestone: {milestone}}` | - -### Project-related Conditionals - -| Syntax | Description | -|--------|-------------| -| `{project:content}` | Shows content only if item is in a project | -| `{projects:content}` | Shows content if item is in any project | diff --git a/templates/default-issue-template.md b/templates/default-issue-template.md deleted file mode 100644 index 7d676c0..0000000 --- a/templates/default-issue-template.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: "{title_yaml}" -number: {number} -status: "{status}" -created: "{created}" -url: "{url}" -opened_by: "{author}" -assignees: {assignees_yaml} -labels: {labels_yaml} -milestone: "{milestone}" -comments: {commentsCount} -locked: {isLocked} -updateMode: "none" -allowDelete: true ---- - -# {title} - -**Issue #{number}** opened by **{author}** on {created} - -{body} - -## Metadata - -- **Status:** {status} -- **Repository:** {repository} -- **Assignees:** {assignees} -- **Labels:** {labels} -- **Comments:** {commentsCount} -- **Created:** {created} -- **Updated:** {updated} -{closed:- **Closed:** {closed}} -{milestone:- **Milestone:** {milestone}} - -{labels_hash} - ---- - -**[View on GitHub]({url})** diff --git a/templates/default-pr-template.md b/templates/default-pr-template.md deleted file mode 100644 index e2a6f29..0000000 --- a/templates/default-pr-template.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -title: "{title_yaml}" -number: {number} -status: "{status}" -created: "{created}" -url: "{url}" -opened_by: "{author}" -assignees: {assignees_yaml} -labels: {labels_yaml} -milestone: "{milestone}" -comments: {commentsCount} -locked: {isLocked} -merged: {merged} -mergeable: {mergeable} -base_branch: "{baseBranch}" -head_branch: "{headBranch}" -updateMode: "none" -allowDelete: true ---- - -# {title} - -**Pull Request #{number}** opened by **{author}** on {created} - -{body} - -## Pull Request Details - -- **Status:** {status} -- **Repository:** {repository} -- **Base Branch:** `{baseBranch}` -- **Head Branch:** `{headBranch}` -- **Merged:** {merged} -- **Mergeable:** {mergeable} -{mergedAt:- **Merged At:** {mergedAt}} - -## Metadata - -- **Assignees:** {assignees} -- **Labels:** {labels} -- **Comments:** {commentsCount} -- **Created:** {created} -- **Updated:** {updated} -{closed:- **Closed:** {closed}} -{milestone:- **Milestone:** {milestone}} - -{labels_hash} - ---- - -**[View on GitHub]({url})** diff --git a/templates/detailed-template.md b/templates/detailed-template.md deleted file mode 100644 index dae4afb..0000000 --- a/templates/detailed-template.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: "{title_yaml}" -number: {number} -status: "{status}" -type: "{type}" -repository: "{repository}" -created: "{created}" -author: "{author}" -assignees: {assignees_yaml} -labels: {labels_yaml} -updateMode: "none" -allowDelete: true ---- - -# {title} - -**{type} #{number}** in **{repository}** - -## Summary - -{body} - -## People - -- **Author:** @{author} -- **Assignees:** {assignees} - -## Classification - -- **Status:** `{status}` -- **Labels:** {labels} -- **Type:** {type} - -## Stats - -- **Comments:** {commentsCount} -- **Created:** {created} -- **Updated:** {updated} - -## Links - -[View on GitHub]({url}) - -{comments} - ---- - -*Last updated: {updated}* diff --git a/templates/minimal-template.md b/templates/minimal-template.md deleted file mode 100644 index 5fcd90a..0000000 --- a/templates/minimal-template.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -title: "{title_yaml}" -number: {number} -status: "{status}" -created: "{created}" -url: "{url}" -opened_by: "{author}" -assignees: {assignees_yaml} -labels: {labels_yaml} -updateMode: "none" -allowDelete: true ---- - -# {title} #{number} - -> **{status}** | Created by [{author}]({url}) on {created} - -## Description - -{body} - -## Details - -| Field | Value | -|-------|-------| -| Status | {status} | -| Repository | {repository} | -| Assignees | {assignees} | -| Labels | {labels} | -| Comments | {commentsCount} | -| Created | {created} | -| Updated | {updated} | - -## Quick Links - -- [GitHub]({url}) -- {commentsCount} comments -- {labels_hash} - ---- - -*Last updated: {updated}* diff --git a/templates/project-template.md b/templates/project-template.md deleted file mode 100644 index 3a2b11b..0000000 --- a/templates/project-template.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -title: "{title_yaml}" -number: {number} -status: "{status}" -type: "{type}" -repository: "{repository}" -created: "{created}" -author: "{author}" -assignees: {assignees_yaml} -labels: {labels_yaml} -project: "{project}" -project_status: "{project_status}" -project_priority: "{project_priority}" -project_iteration: "{project_iteration}" -updateMode: "none" -allowDelete: true ---- - -# {title} - -**{type} #{number}** in **{repository}** - -{project:## Project - -| Field | Value | -|-------|-------| -| **Project** | [{project}]({project_url}) | -| **Status** | {project_status} | -| **Priority** | {project_priority} | -| **Iteration** | {project_iteration} | -} -## Summary - -{body} - -## People - -- **Author:** @{author} -- **Assignees:** {assignees} - -## Classification - -- **Status:** `{status}` -- **Labels:** {labels} -- **Milestone:** {milestone} -## Dates - -- **Created:** {created} -- **Updated:** {updated} -- **Closed:** {closed} -## Links - -[View on GitHub]({url}){project: | [View in Project]({project_url})} - -{comments} - ---- - -*Last updated: {updated}* From 6297ed06407a4e9d09e20452eee547cc2778408f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Platte?= Date: Fri, 16 Jan 2026 08:25:16 +0100 Subject: [PATCH 5/7] docs: update README --- README.md | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 73eb024..942188c 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,12 @@ An Obsidian plugin that integrates with GitHub to track issues and pull requests >The configurations are heavily inspired by https://github.com/schaier-io, including some specific settings. However, I had already started working on my prototype before I discovered the plugin, and had initially even given it a similar name. -## ✨ Features +# Documentation +Check out the [documentation](https://github.com/LonoxX/obsidian-github-issues/wiki) for detailed information on setup, configuration, and usage. -### 🔄 Issue & Pull Request Tracking +## Features + +### Issue & Pull Request Tracking - Track issues and pull requests from multiple GitHub repositories - Automatically sync GitHub data on startup (configurable) - Background sync at configurable intervals @@ -15,19 +18,19 @@ An Obsidian plugin that integrates with GitHub to track issues and pull requests - Include or exclude closed issues/PRs - Automatic cleanup of old closed items -### 📊 GitHub Projects v2 Integration +### GitHub Projects v2 Integration - Track GitHub Projects across repositories - Kanban board view for project visualization - Custom field support (status, priority, iteration) - Project-specific filtering and organization -### 🔗 Sub-Issues Support +### Sub-Issues Support - Track GitHub sub-issues (parent/child relationships) - Display sub-issues list with status indicators - Navigate between parent and child issues - Progress tracking with completion percentage -### 📝 Markdown Notes +### Markdown Notes - Create markdown notes for each issue or PR - Customizable filename templates with variables - Custom content templates @@ -35,23 +38,24 @@ An Obsidian plugin that integrates with GitHub to track issues and pull requests - Preserve user content with persist blocks - Include comments in notes -## 🚀 Installation +## Installation + +### Via Community Plugins (Recommended) -### Via Obsidian Community Plugins -1. Open Obsidian settings +1. Open Obsidian Settings 2. Navigate to **Community Plugins** 3. Click **Browse** and search for "GitHub Issues" 4. Click **Install** and then **Enable** ### Manual Installation -1. Download the latest release from the [GitHub Releases page](https://github.com/LonoxX/obsidian-github-issues/releases). -2. Extract the contents into your Obsidian plugins folder: - `/.obsidian/plugins/github-issues/` -3. Enable the plugin in Obsidian under **Community Plugins** -4. Reload or restart Obsidian +1. Download the latest release from [GitHub Releases](https://github.com/LonoxX/obsidian-github-issues/releases) +2. Extract to `/.obsidian/plugins/github-issues/` +3. Enable the plugin in **Community Plugins** +4. Reload Obsidian -## ⚙️ Configuration + +## Configuration 1. Create a new GitHub token with the `repo` and `read:org` permissions → [GitHub Settings > Developer Settings > Personal access tokens](https://github.com/settings/tokens) @@ -59,7 +63,7 @@ An Obsidian plugin that integrates with GitHub to track issues and pull requests - Paste your GitHub token in the **GitHub Token** field - Adjust additional settings as needed -## 📦 Adding Repositories +## Adding Repositories 1. Open the plugin settings in Obsidian 2. Add repositories by entering the full GitHub repository path (e.g., `lonoxx/obsidian-github-issues`), @@ -67,9 +71,8 @@ An Obsidian plugin that integrates with GitHub to track issues and pull requests 3. Click **Add Repository** or **Add Selected Repositories** 4. The plugin will automatically fetch issues from the configured repositories -### ⭐ This repository if you like this project! - +## Support -## 📄 License +If you find this plugin useful and would like to support its development, you can star the repository or support me on Ko-fi or [GitHub Sponsors](https://github.com/sponsors/LonoxX): -This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. +[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/LonoxX) From 3a01248e0fabae7f046ffbe50842e2123367da4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Platte?= Date: Fri, 16 Jan 2026 09:18:35 +0100 Subject: [PATCH 6/7] fix: use replaceAll in processTemplate to prevent ReDoS vulnerabilities --- src/util/templateUtils.ts | 4 ++-- tsconfig.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/util/templateUtils.ts b/src/util/templateUtils.ts index 6e85274..637b300 100644 --- a/src/util/templateUtils.ts +++ b/src/util/templateUtils.ts @@ -350,9 +350,9 @@ export function processTemplate( replacements["{parent_issue_state}"] = ""; } - // Replace all variables + // Replace all variables (using replaceAll to avoid ReDoS vulnerabilities) for (const [placeholder, value] of Object.entries(replacements)) { - result = result.replace(new RegExp(escapeRegExp(placeholder), "g"), value); + result = result.replaceAll(placeholder, value); } // Process dynamic project field access: {project_field:FieldName} diff --git a/tsconfig.json b/tsconfig.json index c3939c0..1ae404d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ "importHelpers": true, "isolatedModules": true, "strictNullChecks": true, - "lib": ["DOM", "ES5", "ES6", "ES7"] + "lib": ["DOM", "ES5", "ES6", "ES7", "ES2021"] }, "include": ["**/*.ts"] } From cba08fa4944e155e81cf8f772421069111fdf562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Platte?= Date: Fri, 16 Jan 2026 09:47:18 +0100 Subject: [PATCH 7/7] fix: include sub-issues in default content templates --- .gitignore | 3 +++ src/content-generator.ts | 39 +++++++++++++++++++++++++++++++++++++++ src/file-manager.ts | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+) diff --git a/.gitignore b/.gitignore index 2c5f2ac..456d934 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,6 @@ main.js data.json .DS_Store + +# Snyk Security Extension - AI Rules (auto-generated) +.github/instructions/snyk_rules.instructions.md diff --git a/src/content-generator.ts b/src/content-generator.ts index 62e17a2..7bc5275 100644 --- a/src/content-generator.ts +++ b/src/content-generator.ts @@ -78,6 +78,24 @@ labels: [${( updateMode: "${repo.issueUpdateMode}" allowDelete: ${repo.allowDeleteIssue ? true : false}`; + // Add parent issue if available + if (parentIssue) { + frontmatter += ` +parent_issue: ${parentIssue.number} +parent_issue_url: "${parentIssue.url}"`; + } + + // Add sub-issues metadata if available + if (subIssues && subIssues.length > 0) { + const closedCount = subIssues.filter((si: any) => si.state === "closed").length; + const openCount = subIssues.length - closedCount; + frontmatter += ` +sub_issues: [${subIssues.map((si: any) => si.number).join(", ")}] +sub_issues_count: ${subIssues.length} +sub_issues_open: ${openCount} +sub_issues_closed: ${closedCount}`; + } + // Add projectData if available if (projectData && projectData.length > 0) { frontmatter += ` @@ -100,6 +118,27 @@ ${ ${this.fileHelpers.formatComments(comments, settings.escapeMode, settings.dateFormat, shouldEscapeHashTags)}`; + // Add sub-issues section if available + if (subIssues && subIssues.length > 0) { + frontmatter += ` + +## Sub-Issues +${subIssues.map((si: any) => { + const statusIcon = si.state === "closed" + ? '' + : ''; + return `- ${statusIcon} [#${si.number} ${si.title}](${si.url})`; +}).join("\n")}`; + } + + // Add parent issue link if available + if (parentIssue) { + frontmatter += ` + +## Parent Issue +- [#${parentIssue.number} ${parentIssue.title}](${parentIssue.url})`; + } + return frontmatter; } diff --git a/src/file-manager.ts b/src/file-manager.ts index 3d8041f..35c7f9a 100644 --- a/src/file-manager.ts +++ b/src/file-manager.ts @@ -360,6 +360,24 @@ project_status: "${status}"`; requested_reviewers: [${reviewers.join(", ")}]`; } + // Add parent issue if available + if (parentIssue) { + frontmatter += ` +parent_issue: ${parentIssue.number} +parent_issue_url: "${parentIssue.url}"`; + } + + // Add sub-issues metadata if available + if (subIssues && subIssues.length > 0) { + const closedCount = subIssues.filter((si: any) => si.state === "closed").length; + const openCount = subIssues.length - closedCount; + frontmatter += ` +sub_issues: [${subIssues.map((si: any) => si.number).join(", ")}] +sub_issues_count: ${subIssues.length} +sub_issues_open: ${openCount} +sub_issues_closed: ${closedCount}`; + } + frontmatter += ` --- @@ -370,6 +388,27 @@ ${ : "_No description provided._" }`; + // Add sub-issues section if available + if (subIssues && subIssues.length > 0) { + frontmatter += ` + +## Sub-Issues +${subIssues.map((si: any) => { + const statusIcon = si.state === "closed" + ? '' + : ''; + return `- ${statusIcon} [#${si.number} ${si.title}](${si.url})`; +}).join("\n")}`; + } + + // Add parent issue link if available + if (parentIssue) { + frontmatter += ` + +## Parent Issue +- [#${parentIssue.number} ${parentIssue.title}](${parentIssue.url})`; + } + return frontmatter; }