mirror of
https://github.com/jacobtread/obsidian-timekeep.git
synced 2026-07-22 10:10:27 +00:00
feat: openItemRef helper for opening registry item refs, update locator
modal to use openItemRef This brings the behavior of scrolling to the line the timesheet is on like the status bar items have
This commit is contained in:
parent
fac1467a5d
commit
3da8d5415c
6 changed files with 126 additions and 29 deletions
|
|
@ -495,6 +495,13 @@ export const MockNotice = vi.fn(
|
|||
}
|
||||
);
|
||||
|
||||
export class MockMarkdownView {
|
||||
editor = {
|
||||
setCursor: vi.fn(),
|
||||
scrollIntoView: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create the {@link Node.createEl} function added by obsidian
|
||||
*
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { afterEach, vi } from "vitest";
|
|||
import {
|
||||
MockButtonComponent,
|
||||
MockComponent,
|
||||
MockMarkdownView,
|
||||
MockModal,
|
||||
MockNotice,
|
||||
MockSetting,
|
||||
|
|
@ -34,5 +35,6 @@ vi.mock("obsidian", () => {
|
|||
}),
|
||||
Setting: MockSetting,
|
||||
ButtonComponent: MockButtonComponent,
|
||||
MarkdownView: MockMarkdownView,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { App, Component, MarkdownView } from "obsidian";
|
||||
import { type App } from "obsidian";
|
||||
import { Component } from "obsidian";
|
||||
|
||||
import {
|
||||
TimekeepEntryItemType,
|
||||
|
|
@ -117,18 +118,6 @@ export class TimesheetStatusBar extends Component {
|
|||
}
|
||||
|
||||
async onOpen(ref: TimekeepRegistryItemRef) {
|
||||
const leaf = this.app.workspace.getLeaf();
|
||||
await leaf.openFile(ref.file);
|
||||
|
||||
const view = leaf.view;
|
||||
|
||||
if (view instanceof MarkdownView && ref.type === TimekeepEntryItemType.MARKDOWN) {
|
||||
const editor = view.editor;
|
||||
const line = ref.position.startLine;
|
||||
|
||||
// Focus the line we opened to
|
||||
editor.setCursor({ line: Math.max(line - 1, 0), ch: 0 });
|
||||
editor.scrollIntoView({ from: { line, ch: 0 }, to: { line, ch: 0 } }, true);
|
||||
}
|
||||
await TimekeepRegistry.openItemRef(this.app.workspace, ref);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
import type { App, TFile } from "obsidian";
|
||||
import type { App } from "obsidian";
|
||||
|
||||
import { SuggestModal } from "obsidian";
|
||||
|
||||
import type { TimekeepSettings } from "@/settings";
|
||||
import type { Store } from "@/store";
|
||||
import type { Timekeep, TimeEntry } from "@/timekeep/schema";
|
||||
import type { TimeEntry } from "@/timekeep/schema";
|
||||
|
||||
import { TimekeepEntryItemType, TimekeepRegistry, TimekeepRegistryEntry } from "@/service/registry";
|
||||
import {
|
||||
TimekeepEntryItemType,
|
||||
TimekeepRegistry,
|
||||
TimekeepRegistryEntry,
|
||||
TimekeepRegistryItemRef,
|
||||
} from "@/service/registry";
|
||||
import { getRunningEntry } from "@/timekeep/queries";
|
||||
|
||||
interface TimekeepResult {
|
||||
timekeep: Timekeep;
|
||||
ref: TimekeepRegistryItemRef;
|
||||
running: TimeEntry;
|
||||
file: TFile;
|
||||
}
|
||||
|
||||
export class TimekeepLocatorModal extends SuggestModal<TimekeepResult> {
|
||||
|
|
@ -49,18 +53,18 @@ export class TimekeepLocatorModal extends SuggestModal<TimekeepResult> {
|
|||
return this.results.filter((result) => {
|
||||
return (
|
||||
result.running.name.toLowerCase().contains(queryLower) ||
|
||||
result.file.path.toLowerCase().contains(queryLower)
|
||||
result.ref.file.path.toLowerCase().contains(queryLower)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
renderSuggestion(value: TimekeepResult, el: HTMLElement) {
|
||||
el.createEl("div", { text: value.running.name });
|
||||
el.createEl("small", { text: value.file.path });
|
||||
el.createEl("small", { text: value.ref.file.path });
|
||||
}
|
||||
|
||||
onChooseSuggestion(item: TimekeepResult, _evt: MouseEvent | KeyboardEvent) {
|
||||
void this.app.workspace.getLeaf().openFile(item.file);
|
||||
async onChooseSuggestion(item: TimekeepResult, _evt: MouseEvent | KeyboardEvent) {
|
||||
await TimekeepRegistry.openItemRef(this.app.workspace, item.ref);
|
||||
}
|
||||
|
||||
static getResultsFromEntries(entries: TimekeepRegistryEntry[]): TimekeepResult[] {
|
||||
|
|
@ -71,7 +75,13 @@ export class TimekeepLocatorModal extends SuggestModal<TimekeepResult> {
|
|||
const timekeep = entry.timekeep;
|
||||
const running = getRunningEntry(timekeep.entries);
|
||||
if (running !== null) {
|
||||
results.push({ file: entry.file, running: running, timekeep });
|
||||
results.push({
|
||||
running: running,
|
||||
ref: {
|
||||
type: entry.type,
|
||||
file: entry.file,
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -80,7 +90,14 @@ export class TimekeepLocatorModal extends SuggestModal<TimekeepResult> {
|
|||
const timekeep = timekeepWithPosition.timekeep;
|
||||
const running = getRunningEntry(timekeep.entries);
|
||||
if (running !== null) {
|
||||
results.push({ file: entry.file, running: running, timekeep });
|
||||
results.push({
|
||||
running: running,
|
||||
ref: {
|
||||
type: entry.type,
|
||||
file: entry.file,
|
||||
position: timekeepWithPosition,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,21 @@
|
|||
import type { Workspace } from "obsidian";
|
||||
|
||||
import moment from "moment";
|
||||
import { v4 } from "uuid";
|
||||
import { describe, vi, it, expect } from "vitest";
|
||||
|
||||
import { MockVault } from "@/__mocks__/obsidian";
|
||||
import { MockMarkdownView, MockVault } from "@/__mocks__/obsidian";
|
||||
import { defaultSettings } from "@/settings";
|
||||
import { createStore } from "@/store";
|
||||
import { stripTimekeepRuntimeData, Timekeep } from "@/timekeep/schema";
|
||||
import { createCodeBlock } from "@/utils/codeblock";
|
||||
|
||||
import { TimekeepEntryItemType, TimekeepRegistry, TimekeepRegistryEntryMarkdown } from "./registry";
|
||||
import {
|
||||
TimekeepEntryItemType,
|
||||
TimekeepRegistry,
|
||||
TimekeepRegistryEntryMarkdown,
|
||||
TimekeepRegistryItemRef,
|
||||
} from "./registry";
|
||||
|
||||
describe("TimekeepRegistry", () => {
|
||||
describe("getFileRegistryEntry", () => {
|
||||
|
|
@ -641,4 +648,63 @@ describe("TimekeepRegistry", () => {
|
|||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("openItemRef", () => {
|
||||
it("should attempt to open the file", async () => {
|
||||
const vault = new MockVault();
|
||||
const testFile = vault.addFile("test.md", "");
|
||||
const ref: TimekeepRegistryItemRef = {
|
||||
file: testFile,
|
||||
type: TimekeepEntryItemType.FILE,
|
||||
};
|
||||
|
||||
const openFile = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
const workspace = {
|
||||
getLeaf() {
|
||||
const view = new MockMarkdownView();
|
||||
(view as any).openFile = openFile;
|
||||
return view;
|
||||
},
|
||||
} as any as Workspace;
|
||||
|
||||
await TimekeepRegistry.openItemRef(workspace, ref);
|
||||
|
||||
expect(openFile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("opening a markdown entry should scroll to the specific timekeep position", async () => {
|
||||
const vault = new MockVault();
|
||||
const testFile = vault.addFile("test.md", "");
|
||||
const ref: TimekeepRegistryItemRef = {
|
||||
file: testFile,
|
||||
type: TimekeepEntryItemType.MARKDOWN,
|
||||
position: { startLine: 1, endLine: 2 },
|
||||
};
|
||||
|
||||
const openFile = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
const view = new MockMarkdownView();
|
||||
|
||||
const workspace = {
|
||||
getLeaf() {
|
||||
return { view, openFile };
|
||||
},
|
||||
} as any as Workspace;
|
||||
|
||||
await TimekeepRegistry.openItemRef(workspace, ref);
|
||||
expect(openFile).toHaveBeenCalled();
|
||||
|
||||
const line = ref.position.startLine;
|
||||
|
||||
expect(view.editor.setCursor).toHaveBeenCalledExactlyOnceWith({
|
||||
line: line - 1,
|
||||
ch: 0,
|
||||
});
|
||||
expect(view.editor.scrollIntoView).toHaveBeenCalledExactlyOnceWith(
|
||||
{ from: { line, ch: 0 }, to: { line, ch: 0 } },
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { TAbstractFile, Vault } from "obsidian";
|
||||
import type { TAbstractFile, Vault, Workspace } from "obsidian";
|
||||
|
||||
import moment from "moment";
|
||||
import { Component, TFile } from "obsidian";
|
||||
import { Component, MarkdownView, TFile } from "obsidian";
|
||||
import { limitFunction } from "p-limit";
|
||||
|
||||
import type { TimekeepSettings } from "@/settings";
|
||||
|
|
@ -357,4 +357,20 @@ export class TimekeepRegistry extends Component {
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
static async openItemRef(workspace: Workspace, ref: TimekeepRegistryItemRef) {
|
||||
const leaf = workspace.getLeaf();
|
||||
await leaf.openFile(ref.file);
|
||||
|
||||
const view = leaf.view;
|
||||
|
||||
if (view instanceof MarkdownView && ref.type === TimekeepEntryItemType.MARKDOWN) {
|
||||
const editor = view.editor;
|
||||
const line = ref.position.startLine;
|
||||
|
||||
// Focus the line we opened to
|
||||
editor.setCursor({ line: Math.max(line - 1, 0), ch: 0 });
|
||||
editor.scrollIntoView({ from: { line, ch: 0 }, to: { line, ch: 0 } }, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue