Merge pull request #82 from jacobtread/feat-status-bar-item-path

feat: status bar option to include the folder path
This commit is contained in:
Jacob 2026-05-01 17:05:24 +12:00 committed by GitHub
commit e7daf73d6b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 173 additions and 25 deletions

View file

@ -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", () => {

View file

@ -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();
}

View file

@ -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();
});
});

View file

@ -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);

View file

@ -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;
}

View file

@ -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")

View file

@ -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,
};