mirror of
https://github.com/firstsun-dev/git-files-sync.git
synced 2026-07-22 06:54:27 +00:00
fix: resolve obsidian community guidelines and lint errors
This commit is contained in:
parent
4c8c280138
commit
ecd2aabfca
15 changed files with 137 additions and 60 deletions
19
.github/workflows/sonarqube.yml
vendored
Normal file
19
.github/workflows/sonarqube.yml
vendored
Normal 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 }}
|
||||
|
|
@ -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
14
sonar-project.properties
Normal 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
|
||||
|
|
@ -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 }> }> {
|
||||
|
|
|
|||
12
src/main.ts
12
src/main.ts
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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
42
src/ui/ConfirmModal.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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() {}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"tests/**/*.ts"
|
||||
"tests/**/*.ts",
|
||||
"vitest.config.ts"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { readFileSync, writeFileSync } from "fs";
|
||||
import process from "process";
|
||||
|
||||
const targetVersion = process.env.npm_package_version;
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue