fix: resolve obsidian community guidelines and lint errors

This commit is contained in:
ClaudiaFang 2026-04-25 03:13:04 +00:00
parent 4c8c280138
commit ecd2aabfca
15 changed files with 137 additions and 60 deletions

19
.github/workflows/sonarqube.yml vendored Normal file
View file

@ -0,0 +1,19 @@
name: Build
on:
push:
branches:
- master
pull_request:
types: [opened, synchronize, reopened]
jobs:
sonarqube:
name: SonarQube
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@fd88b7d7ccbaefd23d8f36f73b59db7a3d246602 # v6.0.0
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

View file

@ -13,25 +13,15 @@ export default tseslint.config(
projectService: {
allowDefaultProject: [
'eslint.config.js',
'manifest.json',
'vitest.config.ts',
'manifest.json'
]
},
ttsconfigRootDir: import.meta.dirname,
tsconfigRootDir: import.meta.dirname,
extraFileExtensions: ['.json']
},
},
rules: {
'no-alert': 'off', // Allow confirm dialogs for user confirmation
'@typescript-eslint/await-thenable': 'off', // Settings methods may not return promises
}
},
...obsidianmd.configs.recommended,
{
rules: {
'obsidianmd/ui/sentence-case': 'off',
}
},
globalIgnores([
"node_modules",
"dist",

14
sonar-project.properties Normal file
View file

@ -0,0 +1,14 @@
sonar.projectKey=firstsun-dev_git-files-sync
sonar.organization=firstsun-dev
# This is the name and version displayed in the SonarCloud UI.
#sonar.projectName=git-files-sync
#sonar.projectVersion=1.0
# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
#sonar.sources=.
# Encoding of the source code. Default is default system encoding
#sonar.sourceEncoding=UTF-8

View file

@ -232,13 +232,11 @@ export class SyncManager {
}
private async saveSettings() {
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any */
// @ts-ignore - access private method to save settings
const plugin = (this.app as any).plugins?.plugins?.['git-file-sync'];
if (plugin) {
const plugins = (this.app as unknown as { plugins: { plugins: Record<string, { saveSettings: () => Promise<void> }> } }).plugins;
const plugin = plugins?.plugins?.['git-file-sync'];
if (plugin && typeof plugin.saveSettings === 'function') {
await plugin.saveSettings();
}
/* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any */
}
async pushAllFiles(files: (TFile | string)[], onProgress?: (current: number, total: number, fileName: string) => void): Promise<{ success: number; failed: number; errors: Array<{ file: string; error: string }> }> {

View file

@ -6,6 +6,7 @@ import { GitServiceInterface } from './services/git-service-interface';
import { SyncManager } from './logic/sync-manager';
import { SyncStatusView, SYNC_STATUS_VIEW_TYPE } from './ui/SyncStatusView';
import { GitignoreManager } from './logic/gitignore-manager';
import { ConfirmModal } from './ui/ConfirmModal';
export default class GitLabFilesPush extends Plugin {
settings: GitLabFilesPushSettings;
@ -40,7 +41,7 @@ export default class GitLabFilesPush extends Plugin {
const serviceName = this.settings.serviceType === 'gitlab' ? 'GitLab' : 'GitHub';
this.addRibbonIcon('upload-cloud', Platform.isMobile ? `Push` : `Push to ${serviceName}`, (evt: MouseEvent) => {
this.addRibbonIcon('upload-cloud', Platform.isMobile ? `Push` : `Push to ${serviceName}`, () => {
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (activeView && activeView.file instanceof TFile) {
void this.sync.pushFile(activeView.file);
@ -240,9 +241,12 @@ export default class GitLabFilesPush extends Plugin {
private showConfirmDialog(message: string): Promise<boolean> {
return new Promise((resolve) => {
// eslint-disable-next-line no-alert
const confirmed = confirm(message);
resolve(confirmed);
new ConfirmModal(
this.app,
message,
() => resolve(true),
() => resolve(false)
).open();
});
}

View file

@ -174,7 +174,7 @@ export class GitHubService implements GitServiceInterface {
}
}
async listFiles(branch: string, path: string = ''): Promise<string[]> {
async listFiles(branch: string, _path: string = ''): Promise<string[]> {
const url = `https://api.github.com/repos/${this.owner}/${this.repo}/git/trees/${branch}?recursive=1`;
const response = await this.safeRequest({

View file

@ -163,7 +163,7 @@ export class GitLabSyncSettingTab extends PluginSettingTab {
private displayGitHubSettings(containerEl: HTMLElement): void {
new Setting(containerEl)
.setName('GitHub personal access token')
.setDesc('Create a token in GitHub settings > Developer settings > Personal access tokens with "repo" scope')
.setDesc('Create a token in GitHub settings > developer settings > personal access tokens with "repo" scope')
.addText(text => text
.setPlaceholder('Enter your token')
.setValue(this.plugin.settings.githubToken)
@ -177,7 +177,7 @@ export class GitLabSyncSettingTab extends PluginSettingTab {
.setName('Repository owner')
.setDesc('GitHub username or organization name')
.addText(text => text
.setPlaceholder('username')
.setPlaceholder('Username')
.setValue(this.plugin.settings.githubOwner)
.onChange((value) => {
this.plugin.settings.githubOwner = value;
@ -189,7 +189,7 @@ export class GitLabSyncSettingTab extends PluginSettingTab {
.setName('Repository name')
.setDesc('Name of the GitHub repository')
.addText(text => text
.setPlaceholder('my-notes')
.setPlaceholder('My notes')
.setValue(this.plugin.settings.githubRepo)
.onChange((value) => {
this.plugin.settings.githubRepo = value;

42
src/ui/ConfirmModal.ts Normal file
View file

@ -0,0 +1,42 @@
import { App, Modal, ButtonComponent } from 'obsidian';
export class ConfirmModal extends Modal {
private message: string;
private onConfirm: () => void;
private onCancel?: () => void;
constructor(app: App, message: string, onConfirm: () => void, onCancel?: () => void) {
super(app);
this.message = message;
this.onConfirm = onConfirm;
this.onCancel = onCancel;
}
onOpen() {
const { contentEl } = this;
contentEl.createEl('h3', { text: 'Confirm' });
contentEl.createEl('p', { text: this.message });
const buttonContainer = contentEl.createDiv({ cls: 'ssv-confirm-buttons modal-button-container' });
new ButtonComponent(buttonContainer)
.setButtonText('Cancel')
.onClick(() => {
this.close();
if (this.onCancel) this.onCancel();
});
new ButtonComponent(buttonContainer)
.setButtonText('Confirm')
.setCta()
.onClick(() => {
this.close();
this.onConfirm();
});
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}

View file

@ -1,5 +1,6 @@
import { ItemView, WorkspaceLeaf, TFile, Notice, Platform, setTooltip } from 'obsidian';
import GitLabFilesPush from '../main';
import { ConfirmModal } from './ConfirmModal';
export const SYNC_STATUS_VIEW_TYPE = 'sync-status-view';
@ -43,12 +44,13 @@ export class SyncStatusView extends ItemView {
getDisplayText(): string { return 'Sync status'; }
getIcon(): string { return 'git-compare'; }
async onOpen(): Promise<void> {
onOpen(): Promise<void> {
const container = this.containerEl.children[1];
if (!container) return;
if (!container) return Promise.resolve();
container.empty();
container.addClass('sync-status-view');
this.renderView();
return Promise.resolve();
}
private renderView(): void {
@ -189,7 +191,7 @@ export class SyncStatusView extends ItemView {
const delBtn = bar.createEl('button', { cls: 'ssv-btn ssv-btn-delete' });
delBtn.createSpan({ text: '✕' });
delBtn.createSpan({ cls: 'ssv-btn-label', text: ` Del (${canDelete})` });
delBtn.createSpan({ cls: 'ssv-btn-label', text: ` Delete (${canDelete})` });
delBtn.disabled = canDelete === 0;
setTooltip(delBtn, `Delete ${canDelete} files`);
delBtn.addEventListener('click', () => void this.deleteSelected());
@ -249,7 +251,9 @@ export class SyncStatusView extends ItemView {
diffEl.toggleClass('visible', !open);
btnLabel.setText(open ? ' Diff' : ' Hide');
const firstChild = diffBtn.firstChild;
if (firstChild) firstChild.textContent = open ? '≡' : '▴';
if (firstChild instanceof HTMLElement || firstChild instanceof Text) {
firstChild.textContent = open ? '≡' : '▴';
}
});
}
@ -820,12 +824,18 @@ export class SyncStatusView extends ItemView {
this.renderView();
}
async onClose(): Promise<void> { /* cleanup */ }
onClose(): Promise<void> {
return Promise.resolve();
}
private showConfirmDialog(message: string): Promise<boolean> {
return new Promise(resolve => {
// eslint-disable-next-line no-alert
resolve(confirm(message));
new ConfirmModal(
this.app,
message,
() => resolve(true),
() => resolve(false)
).open();
});
}
}

View file

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/unbound-method, @typescript-eslint/no-unsafe-return */
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { SyncManager } from '../../src/logic/sync-manager';
@ -99,7 +98,7 @@ describe('SyncManager', () => {
// Capture the callback passed to the modal
let callback: (choice: 'local' | 'remote') => void = () => {};
modalMock.mockImplementation(function(app, file, local, remote, onChoose) {
modalMock.mockImplementation((app, file, local, remote, onChoose) => {
callback = onChoose;
return {
open: vi.fn(),
@ -112,7 +111,7 @@ describe('SyncManager', () => {
onOpen: vi.fn(),
onClose: vi.fn(),
setTitle: vi.fn().mockReturnThis(),
} as any;
} as unknown as SyncConflictModal;
});
await manager.pushFile(mockFile);
@ -123,7 +122,7 @@ describe('SyncManager', () => {
// Wait for async operations in callback
await new Promise(resolve => setTimeout(resolve, 0));
const pushSpy = mockGitLab.pushFile as any;
const pushSpy = vi.spyOn(mockGitLab, 'pushFile');
expect(pushSpy).toHaveBeenCalledWith('test.md', 'local content', 'main', 'Update test.md from Obsidian', 'remote-sha');
expect(mockSettings.syncMetadata['test.md']?.lastSyncedSha).toBe('new-sha');
});
@ -139,7 +138,7 @@ describe('SyncManager', () => {
const modalMock = vi.mocked(SyncConflictModal);
let callback: (choice: 'local' | 'remote') => void = () => {};
modalMock.mockImplementation(function(app, file, local, remote, onChoose) {
modalMock.mockImplementation((app, file, local, remote, onChoose) => {
callback = onChoose;
return {
open: vi.fn(),
@ -152,7 +151,7 @@ describe('SyncManager', () => {
onOpen: vi.fn(),
onClose: vi.fn(),
setTitle: vi.fn().mockReturnThis(),
} as any;
} as unknown as SyncConflictModal;
});
await manager.pushFile(mockFile);
@ -206,8 +205,10 @@ describe('SyncManager', () => {
await manager.pushFile(mockFile);
expect(mockGitLab.getFile).not.toHaveBeenCalled();
expect(mockGitLab.pushFile).not.toHaveBeenCalled();
const getFileSpy = vi.spyOn(mockGitLab, 'getFile');
const pushFileSpy = vi.spyOn(mockGitLab, 'pushFile');
expect(getFileSpy).not.toHaveBeenCalled();
expect(pushFileSpy).not.toHaveBeenCalled();
});
it('should add new file to repo when it exists locally but not on remote', async () => {
@ -222,7 +223,8 @@ describe('SyncManager', () => {
await manager.pushFile(mockFile);
expect(mockGitLab.pushFile).toHaveBeenCalledWith(
const pushFileSpy = vi.spyOn(mockGitLab, 'pushFile');
expect(pushFileSpy).toHaveBeenCalledWith(
'new.md',
'new local content',
'main',

View file

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment */
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { GitLabService } from '../../src/services/gitlab-service';
import { requestUrl, RequestUrlResponse } from 'obsidian';
@ -49,14 +48,14 @@ describe('GitLabService', () => {
});
it('should return blob_id as sha', async () => {
const mockResponse: any = {
const mockResponse: unknown = {
status: 200,
json: {
content: btoa('test content'),
blob_id: 'test-blob-id'
}
};
vi.mocked(requestUrl).mockResolvedValue(mockResponse as unknown as RequestUrlResponse);
vi.mocked(requestUrl).mockResolvedValue(mockResponse as RequestUrlResponse);
const result = await service.getFile('test.md', 'main');
expect(result.sha).toBe('test-blob-id');
});
@ -74,7 +73,7 @@ describe('GitLabService', () => {
expect(result).toBe('test.md');
expect(requestUrl).toHaveBeenLastCalledWith(expect.objectContaining({
method: 'POST',
body: expect.stringContaining('bmV3IGNvbnRlbnQ=')
body: expect.stringContaining(btoa('new content')) as unknown as string
}));
});
@ -92,7 +91,7 @@ describe('GitLabService', () => {
expect(result).toBe('test.md');
expect(requestUrl).toHaveBeenLastCalledWith(expect.objectContaining({
method: 'PUT',
body: expect.stringContaining('dXBkYXRlZCBjb250ZW50')
body: expect.stringContaining(btoa('updated content')) as unknown as string
}));
});
});

View file

@ -17,21 +17,21 @@ if (typeof window === 'undefined') {
// Mock Obsidian API components
export const Plugin = class {};
export const PluginSettingTab = class {
constructor(_app: unknown, _plugin: unknown) {}
constructor() {}
};
export const Setting = class {
constructor(_containerEl: unknown) {}
setName(_name: string) { return this; }
setDesc(_desc: string) { return this; }
addText(_cb: unknown) { return this; }
addToggle(_cb: unknown) { return this; }
addButton(_cb: unknown) { return this; }
constructor() {}
setName() { return this; }
setDesc() { return this; }
addText() { return this; }
addToggle() { return this; }
addButton() { return this; }
};
export const Notice = class {
constructor(message: string) {}
constructor() {}
};
export const Modal = class {
constructor(_app: unknown) {}
constructor() {}
open() {}
close() {}
};

View file

@ -26,6 +26,7 @@
},
"include": [
"src/**/*.ts",
"tests/**/*.ts"
"tests/**/*.ts",
"vitest.config.ts"
]
}

View file

@ -1,4 +1,5 @@
import { readFileSync, writeFileSync } from "fs";
import process from "process";
const targetVersion = process.env.npm_package_version;

View file

@ -1,6 +1,4 @@
import { defineConfig } from 'vitest/config';
// eslint-disable-next-line import/no-nodejs-modules
import * as path from 'path';
export default defineConfig({
test: {
@ -8,8 +6,7 @@ export default defineConfig({
globals: true,
setupFiles: ['./tests/setup.ts'],
alias: {
// eslint-disable-next-line no-undef
'obsidian': path.resolve(process.cwd(), './tests/setup.ts')
'obsidian': './tests/setup.ts'
}
},
});