fix review bot issues: sentence case, promises, activeWindow

- sentence case for all UI text (enum values, labels, headings, buttons)
- await workspace.revealLeaf (was floating promise)
- activeWindow.setTimeout with block-body arrows (no-misused-promises)
- window -> activeWindow for popout window compatibility
- window.moment() -> obsidian module moment export
- bump minAppVersion to 1.7.2 (required by revealLeaf)
- add eslint-plugin-obsidianmd for local validation
This commit is contained in:
Sam Wildman 2026-04-23 10:23:25 -05:00
parent 37193cf693
commit d28c0849d6
10 changed files with 5050 additions and 88 deletions

14
eslint.config.mjs Normal file
View file

@ -0,0 +1,14 @@
import tsparser from "@typescript-eslint/parser";
import { defineConfig } from "eslint/config";
import obsidianmd from "eslint-plugin-obsidianmd";
export default defineConfig([
...obsidianmd.configs.recommended,
{
files: ["src/**/*.ts"],
languageOptions: {
parser: tsparser,
parserOptions: { project: "./tsconfig.json" },
},
},
]);

View file

@ -2,7 +2,7 @@
"id": "gitlab-inbox",
"name": "GitLab Inbox",
"version": "1.0.0",
"minAppVersion": "1.7.0",
"minAppVersion": "1.7.2",
"description": "Auto-refreshing GitLab inbox showing MRs to review, your open MRs, mentions, and todos with check-off support.",
"author": "Sam Wildman",
"authorUrl": "https://github.com/SrWildman",

5031
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -5,7 +5,8 @@
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production"
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"lint": "eslint src/"
},
"keywords": [
"obsidian",
@ -16,9 +17,15 @@
],
"license": "MIT",
"devDependencies": {
"@eslint/js": "^10.0.1",
"@eslint/json": "^1.2.0",
"@types/node": "^22.0.0",
"@typescript-eslint/parser": "^8.59.0",
"esbuild": "^0.25.0",
"eslint": "^9.39.4",
"eslint-plugin-obsidianmd": "^0.2.4",
"obsidian": "latest",
"typescript": "^5.8.0"
"typescript": "^5.8.0",
"typescript-eslint": "^8.59.0"
}
}

View file

@ -1,4 +1,4 @@
import { ItemView, WorkspaceLeaf } from "obsidian";
import { ItemView, moment, WorkspaceLeaf } from "obsidian";
import type GitLabInboxPlugin from "./main";
import {
Category,
@ -29,7 +29,7 @@ export class InboxView extends ItemView {
}
getDisplayText(): string {
return "GitLab Inbox";
return "GitLab inbox";
}
getIcon(): string {
@ -64,7 +64,7 @@ export class InboxView extends ItemView {
// Header
const header = container.createDiv({ cls: "gi-header" });
const titleRow = header.createDiv({ cls: "gi-title-row" });
titleRow.createEl("h4", { text: "GitLab Inbox" });
titleRow.createEl("h4", { text: "GitLab inbox" });
const headerButtons = titleRow.createDiv({ cls: "gi-header-buttons" });
@ -107,7 +107,7 @@ export class InboxView extends ItemView {
const batchBar = container.createDiv({ cls: "gi-batch-bar" });
batchBar.createSpan({ text: this.selectedKeys.size > 0 ? `${this.selectedKeys.size} selected` : "Select items" });
const batchDone = batchBar.createEl("button", { cls: "gi-batch-btn", text: "\u2713 Done" });
const batchDone = batchBar.createEl("button", { cls: "gi-batch-btn", text: "\u2713 done" });
if (this.selectedKeys.size === 0) batchDone.setAttribute("disabled", "");
batchDone.addEventListener("click", () => {
if (this.selectedKeys.size === 0) return;
@ -188,7 +188,7 @@ export class InboxView extends ItemView {
});
link.addEventListener("click", (e) => {
e.preventDefault();
window.open(item.url, "_blank");
activeWindow.open(item.url, "_blank");
});
info.createSpan({ cls: "gi-item-title", text: ` ${item.title}` });
@ -290,7 +290,7 @@ export class InboxView extends ItemView {
await this.plugin.checkOffItem(item);
}
row.addClass("gi-item-done");
setTimeout(() => {
activeWindow.setTimeout(() => {
if (this.data) {
this.data.items = this.data.items.filter((i) => i.key !== item.key);
this.render();
@ -299,10 +299,10 @@ export class InboxView extends ItemView {
}
private async handleSnooze(item: InboxItem, row: HTMLElement): Promise<void> {
const tomorrow = window.moment().add(1, "day").format("YYYY-MM-DD");
const tomorrow = moment().add(1, "day").format("YYYY-MM-DD");
await this.plugin.snoozeItem(item, tomorrow);
row.addClass("gi-item-done");
setTimeout(() => {
activeWindow.setTimeout(() => {
if (this.data) {
this.data.items = this.data.items.filter((i) => i.key !== item.key);
this.render();
@ -313,7 +313,7 @@ export class InboxView extends ItemView {
private renderTeamLoad(parent: HTMLElement, teamLoad: TeamMemberLoad[]): void {
const section = parent.createDiv({ cls: "gi-section gi-team-section" });
section.createDiv({ cls: "gi-section-header" })
.createSpan({ cls: "gi-section-title", text: "Team Review Load" });
.createSpan({ cls: "gi-section-title", text: "Team review load" });
const table = section.createEl("table", { cls: "gi-team-table" });
const thead = table.createEl("thead");
@ -336,7 +336,7 @@ export class InboxView extends ItemView {
const selected = this.data.items.filter((i) => this.selectedKeys.has(i.key));
if (selected.length === 0) return;
const tomorrow = window.moment().add(1, "day").format("YYYY-MM-DD");
const tomorrow = moment().add(1, "day").format("YYYY-MM-DD");
for (const item of selected) {
if (action === "done") {

View file

@ -1,4 +1,4 @@
import { normalizePath, Notice, Plugin } from "obsidian";
import { moment, normalizePath, Notice, Plugin } from "obsidian";
import { GitLabApi } from "./gitlab-api";
import { fetchInboxData } from "./inbox-processor";
import { InboxView, VIEW_TYPE_INBOX } from "./inbox-view";
@ -30,7 +30,7 @@ export default class GitLabInboxPlugin extends Plugin {
this.registerView(VIEW_TYPE_INBOX, (leaf) => new InboxView(leaf, this));
// Ribbon icon
this.addRibbonIcon("inbox", "GitLab Inbox", () => {
this.addRibbonIcon("inbox", "GitLab inbox", () => {
void this.activateView();
});
@ -62,8 +62,8 @@ export default class GitLabInboxPlugin extends Plugin {
if (this.settings.gitlabHostname && this.settings.personalAccessToken) {
this.startInterval();
// Initial fetch after a short delay to let Obsidian finish loading
const t = window.setTimeout(() => { void this.refresh(); }, 5000);
this.register(() => window.clearTimeout(t));
const t = activeWindow.setTimeout(() => { void this.refresh(); }, 5000);
this.register(() => activeWindow.clearTimeout(t));
}
// Watch for changes to the inbox file (user checking items off)
@ -81,7 +81,7 @@ export default class GitLabInboxPlugin extends Plugin {
}
async loadSettings(): Promise<void> {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData() as Partial<GitLabInboxSettings>);
}
async saveSettings(): Promise<void> {
@ -96,13 +96,13 @@ export default class GitLabInboxPlugin extends Plugin {
this.stopInterval();
const ms = this.settings.refreshIntervalMinutes * 60 * 1000;
this.intervalId = this.registerInterval(
window.setInterval(() => { void this.refresh(); }, ms)
activeWindow.setInterval(() => { void this.refresh(); }, ms)
);
}
stopInterval(): void {
if (this.intervalId !== null) {
window.clearInterval(this.intervalId);
activeWindow.clearInterval(this.intervalId);
this.intervalId = null;
}
}
@ -291,7 +291,7 @@ export default class GitLabInboxPlugin extends Plugin {
const keyPattern = `<!-- ${item.key} -->`;
const updated = content.replace(
new RegExp(`^(\\s*- )\\[ \\](.+${keyPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}.*)$`, "m"),
`$1[x]$2 done on ${window.moment().format("YYYY-MM-DD")}`
`$1[x]$2 done on ${moment().format("YYYY-MM-DD")}`
);
if (updated !== content) {
@ -348,7 +348,7 @@ export default class GitLabInboxPlugin extends Plugin {
}
if (leaf) {
workspace.revealLeaf(leaf);
await workspace.revealLeaf(leaf);
// If we have data, show it filtered by current note state
const view = leaf.view as InboxView;
if (this.lastData) {

View file

@ -1,4 +1,4 @@
import { App, normalizePath, TFile } from "obsidian";
import { App, moment, normalizePath, TFile } from "obsidian";
import {
Category,
CheckedState,
@ -179,7 +179,7 @@ export function generateNote(
// Team review load
if (data.teamLoad.length > 0) {
lines.push("## Team Review Load");
lines.push("## Team review load");
lines.push("");
lines.push("| Reviewer | Open Reviews | Oldest |");
lines.push("|----------|-------------|--------|");
@ -244,7 +244,7 @@ export async function logToDailyNote(
): Promise<void> {
if (!settings.enableDailyNoteLogging || checkedItems.length === 0) return;
const today = window.moment().format(settings.dailyNoteDateFormat);
const today = moment().format(settings.dailyNoteDateFormat);
const dailyPath = normalizePath(`${settings.dailyNotesFolder}/${today}.md`);
const dailyFile = app.vault.getFileByPath(dailyPath);
if (!dailyFile || !(dailyFile instanceof TFile)) return;

View file

@ -18,7 +18,7 @@ export class GitLabInboxSettingTab extends PluginSettingTab {
new Setting(containerEl)
.setName("GitLab hostname")
.setDesc("Your self-hosted GitLab instance (e.g. gitlab.company.com).")
.setDesc("Your self-hosted GitLab instance (e.g. GitLab.company.com).")
.addText((text) => {
text
.setPlaceholder("gitlab.example.com")
@ -31,9 +31,10 @@ export class GitLabInboxSettingTab extends PluginSettingTab {
new Setting(containerEl)
.setName("Personal access token")
.setDesc("Token with API scope. Create at GitLab > Settings > Access tokens.")
.setDesc("Token with API scope. Create at GitLab > settings > access tokens.")
.addText((text) => {
text
// eslint-disable-next-line obsidianmd/ui/sentence-case
.setPlaceholder("glpat-...")
.setValue(this.plugin.settings.personalAccessToken)
.onChange(async (value) => {
@ -65,10 +66,10 @@ export class GitLabInboxSettingTab extends PluginSettingTab {
new Setting(containerEl)
.setName("Inbox filename")
.setDesc("Name of the markdown note written to your vault root.")
.setDesc("Name of the Markdown note written to your vault root.")
.addText((text) => {
text
.setPlaceholder("GitLab Inbox.md")
.setPlaceholder("GitLab inbox.md")
.setValue(this.plugin.settings.inboxFilename)
.onChange(async (value) => {
this.plugin.settings.inboxFilename = value;
@ -118,6 +119,7 @@ export class GitLabInboxSettingTab extends PluginSettingTab {
.setDesc("Moment.js format for daily note filenames.")
.addText((text) => {
text
// eslint-disable-next-line obsidianmd/ui/sentence-case
.setPlaceholder("YYYY-MM-DD")
.setValue(this.plugin.settings.dailyNoteDateFormat)
.onChange(async (value) => {
@ -256,9 +258,9 @@ export class GitLabInboxSettingTab extends PluginSettingTab {
new Setting(container)
.addButton((btn) => {
btn.setButtonText("+ Add Label").onClick(async () => {
btn.setButtonText("+ add label").onClick(async () => {
const id = `label_${Date.now()}`;
labels.push({ id, label: "New Label", color: "var(--text-muted)" });
labels.push({ id, label: "New label", color: "var(--text-muted)" });
await this.plugin.saveSettings();
this.renderLabels(container);
});
@ -364,7 +366,7 @@ export class GitLabInboxSettingTab extends PluginSettingTab {
// Add rule button
new Setting(container)
.addButton((btn) => {
btn.setButtonText("+ Add Rule").onClick(async () => {
btn.setButtonText("+ add rule").onClick(async () => {
const firstLabel = labels[0]?.id ?? "";
if (!firstLabel) return;
rules.push({
@ -384,10 +386,10 @@ export class GitLabInboxSettingTab extends PluginSettingTab {
const api = this.plugin.createApi();
const username = await api.getUsername();
button.setButtonText(`Connected as ${username}`);
setTimeout(() => button.setButtonText("Test"), 3000);
activeWindow.setTimeout(() => { button.setButtonText("Test"); }, 3000);
} catch {
button.setButtonText("Failed - check settings");
setTimeout(() => button.setButtonText("Test"), 3000);
activeWindow.setTimeout(() => { button.setButtonText("Test"); }, 3000);
}
}
@ -395,6 +397,6 @@ export class GitLabInboxSettingTab extends PluginSettingTab {
button.setButtonText("Refreshing...");
await this.plugin.refresh();
button.setButtonText("Done");
setTimeout(() => button.setButtonText("Refresh"), 2000);
activeWindow.setTimeout(() => { button.setButtonText("Refresh"); }, 2000);
}
}

View file

@ -1,11 +1,11 @@
// Enums
export enum Category {
NeedsReview = "Needs Your Review",
ReReview = "Re-Review",
NeedsReview = "Needs your review",
ReReview = "Re-review",
Approved = "Approved",
YourMRs = "Your MRs",
Todos = "Mentions & Todos",
Todos = "Mentions & todos",
}
export enum ConditionType {
@ -81,11 +81,11 @@ export interface GitLabInboxSettings {
}
export const DEFAULT_SECTION_ORDER: SectionConfig[] = [
{ category: Category.NeedsReview, label: "Needs Your Review", enabled: true },
{ category: Category.ReReview, label: "Re-Review", enabled: true },
{ category: Category.NeedsReview, label: "Needs your review", enabled: true },
{ category: Category.ReReview, label: "Re-review", enabled: true },
{ category: Category.Approved, label: "Approved", enabled: true },
{ category: Category.YourMRs, label: "Your MRs", enabled: true },
{ category: Category.Todos, label: "Mentions & Todos", enabled: true },
{ category: Category.Todos, label: "Mentions & todos", enabled: true },
];
export const DEFAULT_LABELS: PriorityLabel[] = [

View file

@ -1,3 +1,3 @@
{
"1.0.0": "1.7.0"
"1.0.0": "1.7.2"
}