mirror of
https://github.com/jacobtread/obsidian-timekeep.git
synced 2026-07-22 10:10:27 +00:00
feat: status bar option to include the folder path
This commit is contained in:
parent
eb68b1d8b5
commit
7d6d345f77
7 changed files with 173 additions and 25 deletions
|
|
@ -35,7 +35,7 @@ describe("TimesheetStatusBar", () => {
|
|||
settings = createStore(defaultSettings);
|
||||
registry = new TimekeepRegistry(vault.asVault(), settings);
|
||||
|
||||
component = new TimesheetStatusBar(containerEl, app, registry);
|
||||
component = new TimesheetStatusBar(containerEl, app, registry, settings);
|
||||
});
|
||||
|
||||
it("should load without error", () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { type App } from "obsidian";
|
||||
import { Component } from "obsidian";
|
||||
|
||||
import { TimekeepSettings } from "@/settings";
|
||||
import { Store } from "@/store";
|
||||
import { assert } from "@/utils/assert";
|
||||
|
||||
import { TimesheetStatusBarItem } from "./TimesheetStatusBarItem";
|
||||
|
|
@ -20,17 +22,25 @@ export class TimesheetStatusBar extends Component {
|
|||
app: App;
|
||||
/** Access to the app registry */
|
||||
registry: TimekeepRegistry;
|
||||
/** Access to the timekeep settings */
|
||||
settings: Store<TimekeepSettings>;
|
||||
|
||||
/** Currently rendered items */
|
||||
items: TimesheetStatusBarItem[] = [];
|
||||
|
||||
constructor(containerEl: HTMLElement, app: App, registry: TimekeepRegistry) {
|
||||
constructor(
|
||||
containerEl: HTMLElement,
|
||||
app: App,
|
||||
registry: TimekeepRegistry,
|
||||
settings: Store<TimekeepSettings>
|
||||
) {
|
||||
super();
|
||||
|
||||
this.#containerEl = containerEl;
|
||||
|
||||
this.app = app;
|
||||
this.registry = registry;
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
onload(): void {
|
||||
|
|
@ -51,6 +61,10 @@ export class TimesheetStatusBar extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
// No need to subscribe to settings, this component is recreated when settings changes
|
||||
const settings = this.settings.getState();
|
||||
|
||||
|
||||
const entries = this.registry.entries.getState();
|
||||
const runningEntries = TimekeepRegistry.getRunningEntries(entries);
|
||||
|
||||
|
|
@ -61,14 +75,21 @@ export class TimesheetStatusBar extends Component {
|
|||
|
||||
// Load the new children
|
||||
for (const runningEntry of runningEntries) {
|
||||
this.renderEntry(runningEntry.running, runningEntry.ref);
|
||||
this.renderEntry(runningEntry.running, runningEntry.ref, settings);
|
||||
}
|
||||
}
|
||||
|
||||
renderEntry(entry: TimeEntry, ref: TimekeepRegistryItemRef) {
|
||||
renderEntry(entry: TimeEntry, ref: TimekeepRegistryItemRef, settings: TimekeepSettings) {
|
||||
const wrapperEl = this.wrapperEl;
|
||||
assert(wrapperEl, "Wrapper element should be defined on render");
|
||||
const item = new TimesheetStatusBarItem(wrapperEl, this.app, this.registry, entry, ref);
|
||||
const item = new TimesheetStatusBarItem(
|
||||
wrapperEl,
|
||||
this.app,
|
||||
this.registry,
|
||||
entry,
|
||||
ref,
|
||||
settings.statusBarShowFolderPath
|
||||
);
|
||||
this.items.push(item);
|
||||
item.load();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,20 +51,34 @@ describe("TimesheetStatusBarItem", () => {
|
|||
|
||||
it("should load without error", () => {
|
||||
const file = vault.addFile("test.timekeep", "");
|
||||
const component = new TimesheetStatusBarItem(containerEl, app, registry, entry, {
|
||||
file,
|
||||
type: TimekeepEntryItemType.FILE,
|
||||
});
|
||||
const component = new TimesheetStatusBarItem(
|
||||
containerEl,
|
||||
app,
|
||||
registry,
|
||||
entry,
|
||||
{
|
||||
file,
|
||||
type: TimekeepEntryItemType.FILE,
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
expect(() => component.load()).not.toThrow();
|
||||
});
|
||||
|
||||
it("should call onStop when the stop icon is clicked", () => {
|
||||
const file = vault.addFile("test.timekeep", "");
|
||||
const component = new TimesheetStatusBarItem(containerEl, app, registry, entry, {
|
||||
file,
|
||||
type: TimekeepEntryItemType.FILE,
|
||||
});
|
||||
const component = new TimesheetStatusBarItem(
|
||||
containerEl,
|
||||
app,
|
||||
registry,
|
||||
entry,
|
||||
{
|
||||
file,
|
||||
type: TimekeepEntryItemType.FILE,
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
const onStop = vi.spyOn(component, "onStop");
|
||||
component.load();
|
||||
|
|
@ -85,10 +99,17 @@ describe("TimesheetStatusBarItem", () => {
|
|||
//
|
||||
.mockRejectedValue(new Error("failed to stop"));
|
||||
|
||||
const component = new TimesheetStatusBarItem(containerEl, app, registry, entry, {
|
||||
file,
|
||||
type: TimekeepEntryItemType.FILE,
|
||||
});
|
||||
const component = new TimesheetStatusBarItem(
|
||||
containerEl,
|
||||
app,
|
||||
registry,
|
||||
entry,
|
||||
{
|
||||
file,
|
||||
type: TimekeepEntryItemType.FILE,
|
||||
},
|
||||
false
|
||||
);
|
||||
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
const onStop = vi.spyOn(component, "onStop");
|
||||
component.load();
|
||||
|
|
@ -101,10 +122,17 @@ describe("TimesheetStatusBarItem", () => {
|
|||
|
||||
it("should call onOpen when the content area is clicked", () => {
|
||||
const file = vault.addFile("test.timekeep", "");
|
||||
const component = new TimesheetStatusBarItem(containerEl, app, registry, entry, {
|
||||
file,
|
||||
type: TimekeepEntryItemType.FILE,
|
||||
});
|
||||
const component = new TimesheetStatusBarItem(
|
||||
containerEl,
|
||||
app,
|
||||
registry,
|
||||
entry,
|
||||
{
|
||||
file,
|
||||
type: TimekeepEntryItemType.FILE,
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
const onOpen = vi.spyOn(component, "onOpen");
|
||||
component.load();
|
||||
|
|
@ -118,4 +146,48 @@ describe("TimesheetStatusBarItem", () => {
|
|||
|
||||
expect(onOpen).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("nested file path should be included in name when showFolderPath is true", () => {
|
||||
const file = vault.addFile("nested/path/test.timekeep", "");
|
||||
const component = new TimesheetStatusBarItem(
|
||||
containerEl,
|
||||
app,
|
||||
registry,
|
||||
entry,
|
||||
{
|
||||
file,
|
||||
type: TimekeepEntryItemType.FILE,
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
component.load();
|
||||
|
||||
const nameEl = component.containerEl.querySelector(".timekeep-status-item__name");
|
||||
expect(nameEl).not.toBeNull();
|
||||
|
||||
expect(nameEl!.textContent.startsWith("nested/path: ")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("nested file path should not be included in name when showFolderPath is false", () => {
|
||||
const file = vault.addFile("nested/path/test.timekeep", "");
|
||||
const component = new TimesheetStatusBarItem(
|
||||
containerEl,
|
||||
app,
|
||||
registry,
|
||||
entry,
|
||||
{
|
||||
file,
|
||||
type: TimekeepEntryItemType.FILE,
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
component.load();
|
||||
|
||||
const nameEl = component.containerEl.querySelector(".timekeep-status-item__name");
|
||||
expect(nameEl).not.toBeNull();
|
||||
|
||||
expect(nameEl!.textContent.startsWith("nested/path: ")).not.toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ import { TimesheetEntryDuration } from "@/components/TimesheetEntryDuration";
|
|||
|
||||
import { TimeEntry } from "@/timekeep/schema";
|
||||
|
||||
import { TimekeepRegistry, TimekeepRegistryItemRef } from "@/service/registry";
|
||||
import {
|
||||
TimekeepEntryItemType,
|
||||
TimekeepRegistry,
|
||||
TimekeepRegistryItemRef,
|
||||
} from "@/service/registry";
|
||||
|
||||
export class TimesheetStatusBarItem extends DomComponent {
|
||||
/** Access to the obsidian app */
|
||||
|
|
@ -15,15 +19,20 @@ export class TimesheetStatusBarItem extends DomComponent {
|
|||
entry: TimeEntry;
|
||||
/** The registry for timekeeps */
|
||||
registry: TimekeepRegistry;
|
||||
|
||||
/** The reference to the item */
|
||||
ref: TimekeepRegistryItemRef;
|
||||
|
||||
/** Whether to show the folder path in the name */
|
||||
showFolderPath: boolean;
|
||||
|
||||
constructor(
|
||||
containerEl: HTMLElement,
|
||||
app: App,
|
||||
registry: TimekeepRegistry,
|
||||
entry: TimeEntry,
|
||||
ref: TimekeepRegistryItemRef
|
||||
ref: TimekeepRegistryItemRef,
|
||||
showFolderPath: boolean
|
||||
) {
|
||||
super(containerEl);
|
||||
|
||||
|
|
@ -31,6 +40,7 @@ export class TimesheetStatusBarItem extends DomComponent {
|
|||
this.registry = registry;
|
||||
this.entry = entry;
|
||||
this.ref = ref;
|
||||
this.showFolderPath = showFolderPath;
|
||||
}
|
||||
|
||||
onload(): void {
|
||||
|
|
@ -53,7 +63,8 @@ export class TimesheetStatusBarItem extends DomComponent {
|
|||
|
||||
contentEl.createSpan({
|
||||
cls: "timekeep-status-item__name",
|
||||
text: entry.name + ":",
|
||||
text: this.getFolderPath() + entry.name + ":",
|
||||
title: this.getDisplayTitle(),
|
||||
});
|
||||
|
||||
this.addChild(new TimesheetEntryDuration(contentEl, entry));
|
||||
|
|
@ -62,6 +73,28 @@ export class TimesheetStatusBarItem extends DomComponent {
|
|||
this.registerDomEvent(contentEl, "click", this.onOpen.bind(this));
|
||||
}
|
||||
|
||||
getDisplayTitle() {
|
||||
switch (this.ref.type) {
|
||||
case TimekeepEntryItemType.FILE:
|
||||
return `${this.ref.file.path}`;
|
||||
case TimekeepEntryItemType.MARKDOWN:
|
||||
return `${this.ref.file.path} - ${this.ref.position.startLine}:${this.ref.position.endLine}`;
|
||||
/* v8 ignore start -- @preserve */
|
||||
default: {
|
||||
throw new Error("unknown entry type");
|
||||
}
|
||||
/* v8 ignore stop -- @preserve */
|
||||
}
|
||||
}
|
||||
|
||||
getFolderPath() {
|
||||
if (!this.showFolderPath) return "";
|
||||
const path = this.ref.file.path;
|
||||
const parts = path.split("/");
|
||||
if (parts.length < 2) return "";
|
||||
return parts.slice(0, parts.length - 1).join("/") + ": ";
|
||||
}
|
||||
|
||||
async onStop() {
|
||||
try {
|
||||
await this.registry.tryStopEntry(this.ref);
|
||||
|
|
|
|||
|
|
@ -198,7 +198,12 @@ export default class TimekeepPlugin extends Plugin {
|
|||
if (!settings.statusBarEnabled) return;
|
||||
|
||||
const containerEl = this.addStatusBarItem();
|
||||
const statusBarView = new TimesheetStatusBar(containerEl, this.app, this.registry);
|
||||
const statusBarView = new TimesheetStatusBar(
|
||||
containerEl,
|
||||
this.app,
|
||||
this.registry,
|
||||
this.settingsStore
|
||||
);
|
||||
this.addChild(statusBarView);
|
||||
this.#statusBarView = statusBarView;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -442,6 +442,21 @@ export class TimekeepSettingsTab extends PluginSettingTab {
|
|||
});
|
||||
});
|
||||
|
||||
new Setting(this.containerEl)
|
||||
.setName("Show Folder Path")
|
||||
.setDesc(
|
||||
'Whether to include the folder path of the file in the status item (i.e "Path/To/Entry: Block 1: 3h 5min 30s").'
|
||||
)
|
||||
.addToggle((t) => {
|
||||
t.setValue(settings.statusBarShowFolderPath);
|
||||
t.onChange((v) => {
|
||||
this.settingsStore.setState((currentValue) => ({
|
||||
...currentValue,
|
||||
statusBarShowFolderPath: v,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
// Autocomplete section
|
||||
new Setting(this.containerEl)
|
||||
.setName("Autocomplete")
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ export interface TimekeepSettings {
|
|||
registryConcurrencyLimit: number;
|
||||
|
||||
statusBarEnabled: boolean;
|
||||
statusBarShowFolderPath: boolean;
|
||||
|
||||
autocompleteEnabled: boolean;
|
||||
}
|
||||
|
|
@ -104,6 +105,7 @@ export const defaultSettings: TimekeepSettings = {
|
|||
registryConcurrencyLimit: 15,
|
||||
|
||||
statusBarEnabled: true,
|
||||
statusBarShowFolderPath: true,
|
||||
|
||||
autocompleteEnabled: true,
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue