feat: shared create new timekeep file logic, create new timekeep file

command
This commit is contained in:
Jacobtread 2026-04-10 00:54:29 +12:00
parent 3da8d5415c
commit 7a169be170
5 changed files with 129 additions and 29 deletions

View file

@ -24,7 +24,7 @@ export abstract class MockTAbstractFile {
constructor(vault: Vault, path: string, parent: MockTFolder | null) {
const nameIndex = path.lastIndexOf("/");
const name = path.substring(nameIndex);
const name = path.substring(nameIndex + 1);
this.vault = vault;
this.path = path;
@ -44,6 +44,7 @@ export class MockTFolder extends MockTAbstractFile {
) {
super(vault, path, parent);
this.children = children;
this.path = path + "/";
}
isRoot(): boolean {
@ -144,6 +145,13 @@ export class MockVault {
}
});
create = vi.fn(async (path, data: string) => {
const file = new MockTFile(this.asVault(), path, data);
this._files[path] = file;
this._cache[file.path] = data;
this.emitEvent("create", file);
});
cachedRead = vi.fn(async (file: TFile) => {
if (!(file instanceof MockTFile)) {
throw new Error("can only read files");
@ -158,6 +166,16 @@ export class MockVault {
addFile(path: string, value: string): TFile {
const file = new MockTFile(this.asVault(), path, value);
const nameIndex = path.lastIndexOf("/");
const folderPath = path.substring(0, nameIndex);
const folder = this._files[folderPath];
if (folder && folder instanceof MockTFolder) {
folder.children.push(file);
}
this._files[path] = file;
this.emitEvent("create", file);

View file

@ -0,0 +1,11 @@
import { App, Command } from "obsidian";
import { createNewTimekeepFile } from "@/timekeep/createNewTimekeepFile";
export default function (app: App): Command {
return {
id: "new-timekeep-file",
name: `Create new timekeep file`,
callback: () => createNewTimekeepFile(app, app.vault.getRoot()),
};
}

View file

@ -1,5 +1,5 @@
import type { Moment } from "moment";
import type { Vault, TFile, PluginManifest, App } from "obsidian";
import type { Vault, TFile, PluginManifest, App, TAbstractFile, Menu } from "obsidian";
import { Plugin, TFolder } from "obsidian";
@ -11,6 +11,7 @@ import createMerged from "@/commands/createMerged";
import exportMergedPdf from "@/commands/exportMergedPdf";
import findRunningTrackers from "@/commands/findRunningTrackers";
import insertTracker from "@/commands/insertTracker";
import newTimekeepFile from "@/commands/newTimekeepFile";
import stopAllTimekeepsCommand from "@/commands/stopAllTimekeeps";
import stopFileTimekeepsCommand from "@/commands/stopFileTimekeeps";
import { TimesheetStatusBar } from "@/components/TimesheetStatusBar";
@ -32,6 +33,8 @@ import { stopFileTimekeeps } from "@/timekeep/stopFileTimekeeps";
import TimekeepFileView from "@/views/TimekeepFileView";
import TimekeepMarkdownView from "@/views/TimekeepMarkdownView";
import { createNewTimekeepFile } from "./timekeep/createNewTimekeepFile";
export default class TimekeepPlugin extends Plugin {
/** Store containing the plugin settings */
settingsStore: Store<TimekeepSettings> = createStore(defaultSettings);
@ -126,6 +129,7 @@ export default class TimekeepPlugin extends Plugin {
this.addCommand(exportMergedPdf(this.app, this.registry, this.settingsStore));
this.addCommand(stopAllTimekeepsCommand(this.app));
this.addCommand(stopFileTimekeepsCommand(this.app));
this.addCommand(newTimekeepFile(this.app));
// Custom timekeep file format
this.registerView("timekeep", (leaf) => {
@ -139,36 +143,20 @@ export default class TimekeepPlugin extends Plugin {
this.registerExtensions(["timekeep"], "timekeep");
this.app.workspace.on("file-menu", (menu, parent) => {
if (!(parent instanceof TFolder)) return;
this.app.workspace.on("file-menu", this.onFileMenu.bind(this));
}
const folder = parent;
private onFileMenu(menu: Menu, parent: TAbstractFile) {
if (!(parent instanceof TFolder)) return;
menu.addItem((item) => {
item.setTitle("New Timekeep")
.setIcon("clock")
.onClick(async () => {
const folderPath = folder.path;
const folder = parent;
const isNameTaken = (name: string) =>
folder.children.find((child) => child.name === name) !== undefined;
let name = "Untitled.timekeep";
let index = 1;
while (isNameTaken(name)) {
name = `Untitled ${index}.timekeep`;
index += 1;
}
const filePath = `${folderPath}${name}`;
const file = await this.app.vault.create(filePath, "");
// Open the created file
const leaf = this.app.workspace.getLeaf();
await leaf.openFile(file);
});
});
menu.addItem((item) => {
item.setTitle("New Timekeep")
.setIcon("clock")
.onClick(async () => {
await createNewTimekeepFile(this.app, folder);
});
});
}

View file

@ -0,0 +1,54 @@
import { App } from "obsidian";
import { describe, expect, it, vi } from "vitest";
import { MockVault } from "@/__mocks__/obsidian";
import { createNewTimekeepFile } from "./createNewTimekeepFile";
describe("createNewTimekeepFile", () => {
it("should use Untitled.timekeep when the file name is not in use", async () => {
const vault = new MockVault();
const openFile = vi.fn();
const app = {
vault,
workspace: {
getLeaf() {
return { openFile };
},
},
} as any as App;
const folder = vault.addFolder("test");
await createNewTimekeepFile(app, folder);
const files = vault.getFiles();
const file = files.find((file) => file.name === "Untitled.timekeep");
expect(file).toBeDefined();
});
it("should increment the name counter until a unused name is found", async () => {
const vault = new MockVault();
const openFile = vi.fn();
const app = {
vault,
workspace: {
getLeaf() {
return { openFile };
},
},
} as any as App;
const folder = vault.addFolder("test");
vault.addFile("test/Untitled.timekeep", "");
for (let i = 1; i <= 10; i++) {
vault.addFile(`test/Untitled ${i}.timekeep`, "");
}
await createNewTimekeepFile(app, folder);
const files = vault.getFiles();
const file = files.find((file) => file.name === "Untitled 11.timekeep");
expect(file).toBeDefined();
});
});

View file

@ -0,0 +1,29 @@
import type { App, TFolder } from "obsidian";
/**
* Create a new timekeep file within the provided folder
*
* @param app App to use for the vault and workspace
* @param folder
*/
export async function createNewTimekeepFile(app: App, folder: TFolder) {
const folderPath = folder.path;
const isNameTaken = (name: string) =>
folder.children.find((child) => child.name === name) !== undefined;
let name = "Untitled.timekeep";
let index = 1;
while (isNameTaken(name)) {
name = `Untitled ${index}.timekeep`;
index += 1;
}
const filePath = `${folderPath}${name}`;
const file = await app.vault.create(filePath, "");
// Open the created file
const leaf = app.workspace.getLeaf();
await leaf.openFile(file);
}