Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ version with its date and start a fresh empty `[Unreleased]` above it.

### Added

- A settings button in the view header opens Qoderian's settings window in
place, instead of navigating into Obsidian's settings.
- Show the added external context path after dropping a folder or file, surface
rejected additions, and show the context count even for a single path.
- Drag files and folders from your operating system (e.g. macOS Finder) onto
Expand Down
10 changes: 10 additions & 0 deletions src/features/chat/chat-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { setButtonTooltip } from '../../shared/dom/tooltip';
import { createIconSvg, QODER_ICON,QODERIAN_ICON_ID } from '../../shared/icons';
import { openFeedbackModal } from '../feedback/ui/feedback-modal';
import { QoderianSettingsModal } from '../settings/settings-modal';
import type { HistoryConversationStatus } from './controllers/conversation-controller';
import {
sendTabInputMessageFromExplicitEnterShortcut,
Expand Down Expand Up @@ -230,6 +231,11 @@ export class QoderianView extends ItemView {
setIcon(feedbackBtn, 'message-circle-question');
setButtonTooltip(feedbackBtn, t('commands.submitFeedback'));
feedbackBtn.addEventListener('click', () => this.openFeedback());

const settingsBtn = headerActions.createDiv({ cls: 'qoderian-header-btn' });
setIcon(settingsBtn, 'settings');
setButtonTooltip(settingsBtn, t('settings.title'));
settingsBtn.addEventListener('click', () => this.openSettings());
}

/**
Expand Down Expand Up @@ -307,6 +313,10 @@ export class QoderianView extends ItemView {
});
}

private openSettings(): void {
new QoderianSettingsModal(this.plugin).open();
}

private buildInputFooter(): void {
if (!this.viewContainerEl) return;

Expand Down
30 changes: 30 additions & 0 deletions src/features/settings/settings-modal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Modal } from 'obsidian';

import { t } from '../../i18n/i18n';
import type QoderianPlugin from '../../main';
import { QoderianSettingTab } from './settings-tab';

/** Qoderian-owned settings window that does not navigate into Obsidian settings. */
export class QoderianSettingsModal extends Modal {
private settingsTab: QoderianSettingTab | null = null;

constructor(private readonly plugin: QoderianPlugin) {
super(plugin.app);
}

onOpen(): void {
this.modalEl.addClass('qoderian-settings-modal');
this.contentEl.addClass('qoderian-settings-modal-content');
this.titleEl.setText(t('settings.title'));

const settingsTab = new QoderianSettingTab(this.app, this.plugin);
settingsTab.mount(this.contentEl);
this.settingsTab = settingsTab;
}

onClose(): void {
this.settingsTab?.hide();
this.settingsTab = null;
this.contentEl.empty();
}
}
10 changes: 10 additions & 0 deletions src/features/settings/settings-tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,16 @@ export class QoderianSettingTab extends PluginSettingTab {
}
}

/**
* Imperative renderer for hosts outside Obsidian's settings pane, such as
* the header settings window. Shares the same builders as the pre-1.13
* display() fallback, which Obsidian owns for older versions.
*/
mount(containerEl: HTMLElement): void {
this.containerEl = containerEl;
this.renderLegacySettings();
}

/**
* Imperative entry point for Obsidian versions older than 1.13.0, which
* never consult getSettingDefinitions(). Kept as the documented fallback
Expand Down
12 changes: 12 additions & 0 deletions src/style/settings/base.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
/* Settings page - remove separator lines from setting items */
.qoderian-settings-modal {
width: min(920px, calc(100vw - 48px));
max-width: 920px;
height: min(86vh, 900px);
}

.qoderian-settings-modal-content {
overflow-y: auto;
overscroll-behavior: contain;
padding-inline-end: 8px;
}

.qoderian-settings .setting-item {
border-top: none;
}
Expand Down
6 changes: 6 additions & 0 deletions tests/__mocks__/obsidian.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,12 @@ export class Modal {
empty: jest.fn(),
addClass: jest.fn(),
};
modalEl: any = {
addClass: jest.fn(),
};
titleEl: any = {
setText: jest.fn(),
};

constructor(app: any) {
this.app = app;
Expand Down
74 changes: 74 additions & 0 deletions tests/unit/features/settings/settings-modal.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { createMockEl } from '@test/helpers/mock-element';

import { QoderianSettingsModal } from '@/features/settings/settings-modal';

let lastModalInstance: any;
const mockSettingsTab = {
mount: jest.fn(),
hide: jest.fn(),
};

jest.mock('obsidian', () => {
const actual = jest.requireActual('obsidian');

class MockModal {
app: any;
modalEl: any = { addClass: jest.fn() };
titleEl: any = { setText: jest.fn() };
contentEl: any = createMockEl();

constructor(app: any) {
this.app = app;
// eslint-disable-next-line @typescript-eslint/no-this-alias
lastModalInstance = this;
}

open() {
this.onOpen();
}

close() {
this.onClose();
}

onOpen() {
// Overridden by subclass
}

onClose() {
// Overridden by subclass
}
}

return { ...actual, Modal: MockModal };
});

jest.mock('@/features/settings/settings-tab', () => ({
QoderianSettingTab: jest.fn().mockImplementation(() => mockSettingsTab),
}));

describe('QoderianSettingsModal', () => {
beforeEach(() => {
mockSettingsTab.mount.mockClear();
mockSettingsTab.hide.mockClear();
});

it('renders the settings tab inside the settings window', () => {
new QoderianSettingsModal({ app: {} } as any).open();

expect(lastModalInstance.modalEl.addClass).toHaveBeenCalledWith('qoderian-settings-modal');
expect(lastModalInstance.titleEl.setText).toHaveBeenCalledWith('Qoderian Settings');
expect(mockSettingsTab.mount).toHaveBeenCalledWith(lastModalInstance.contentEl);
});

it('detaches the settings tab when the window closes', () => {
const modal = new QoderianSettingsModal({ app: {} } as any);
modal.open();
const emptySpy = jest.spyOn(modal.contentEl, 'empty');

modal.close();

expect(mockSettingsTab.hide).toHaveBeenCalledTimes(1);
expect(emptySpy).toHaveBeenCalledTimes(1);
});
});
Loading