mirror of
https://github.com/jacobtread/obsidian-timekeep.git
synced 2026-07-22 10:10:27 +00:00
refactor: generalize saving and view logic for more consistent and easy
to manage logic, extract editor scroll tracking, fix content component and replaceable component logic
This commit is contained in:
parent
f71fb6685f
commit
490e9102b8
29 changed files with 708 additions and 620 deletions
|
|
@ -6,7 +6,6 @@ import { createMockContainer } from "@/__mocks__/obsidian";
|
|||
|
||||
import { ContentComponent } from "./ContentComponent";
|
||||
|
||||
import { DomComponent } from "@/components/DomComponent";
|
||||
import { ReplaceableComponent } from "@/components/ReplaceableComponent";
|
||||
|
||||
describe("ContentComponent", () => {
|
||||
|
|
@ -24,29 +23,31 @@ describe("ContentComponent", () => {
|
|||
});
|
||||
|
||||
it("should be able to set content to different components", () => {
|
||||
class FirstComponent extends DomComponent {
|
||||
class FirstComponent extends ReplaceableComponent {
|
||||
constructor(containerEl: HTMLElement) {
|
||||
super(containerEl);
|
||||
}
|
||||
|
||||
onload(): void {
|
||||
super.onload();
|
||||
createContainer(): HTMLElement {
|
||||
return createDiv();
|
||||
}
|
||||
|
||||
const wrapperEl = this.containerEl.createSpan({ text: "First" });
|
||||
this.wrapperEl = wrapperEl;
|
||||
render(containerEl: HTMLElement): void {
|
||||
containerEl.createSpan({ text: "First" });
|
||||
}
|
||||
}
|
||||
|
||||
class SecondComponent extends DomComponent {
|
||||
class SecondComponent extends ReplaceableComponent {
|
||||
constructor(containerEl: HTMLElement) {
|
||||
super(containerEl);
|
||||
}
|
||||
|
||||
onload(): void {
|
||||
super.onload();
|
||||
createContainer(): HTMLElement {
|
||||
return createDiv();
|
||||
}
|
||||
|
||||
const wrapperEl = this.containerEl.createSpan({ text: "Second" });
|
||||
this.wrapperEl = wrapperEl;
|
||||
render(containerEl: HTMLElement): void {
|
||||
containerEl.createSpan({ text: "Second" });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -58,10 +59,6 @@ describe("ContentComponent", () => {
|
|||
|
||||
comp.setContent(new SecondComponent(container));
|
||||
expect(comp.containerEl!.children[0].textContent).toBe("Second");
|
||||
|
||||
comp.setContent(undefined);
|
||||
expect(comp.getContent()).toBeUndefined();
|
||||
expect(comp.containerEl!.children.length).toBe(0);
|
||||
});
|
||||
|
||||
it("using two replaceable component should trigger the previous", () => {
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
import type { Component } from "obsidian";
|
||||
|
||||
import { DomComponent } from "@/components/DomComponent";
|
||||
import { ReplaceableComponent } from "@/components/ReplaceableComponent";
|
||||
|
||||
export class ContentComponent<C extends Component> extends DomComponent {
|
||||
#content: C | undefined = undefined;
|
||||
export class ContentComponent<C extends ReplaceableComponent> extends DomComponent {
|
||||
content: C | undefined = undefined;
|
||||
|
||||
constructor(containerEl: HTMLElement) {
|
||||
super(containerEl);
|
||||
}
|
||||
|
||||
getContent(): C | undefined {
|
||||
return this.#content;
|
||||
return this.content;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -19,32 +17,28 @@ export class ContentComponent<C extends Component> extends DomComponent {
|
|||
*
|
||||
* @param content The new content to show
|
||||
*/
|
||||
setContent(content: C | undefined) {
|
||||
let previousContent: C | undefined = this.#content;
|
||||
setContent(content: C) {
|
||||
let previousContent: C | undefined = this.content;
|
||||
let previousElement: HTMLElement | undefined;
|
||||
|
||||
if (previousContent) {
|
||||
// Mark for replacement if both are replaceable components
|
||||
if (
|
||||
content instanceof ReplaceableComponent &&
|
||||
previousContent instanceof ReplaceableComponent
|
||||
) {
|
||||
previousElement = previousContent.wrapperEl;
|
||||
previousContent.skipUnloadReplace = true;
|
||||
}
|
||||
previousElement = previousContent.wrapperEl;
|
||||
previousContent.skipUnloadReplace = true;
|
||||
|
||||
this.removeChild(previousContent);
|
||||
}
|
||||
|
||||
this.#content = content;
|
||||
this.content = content;
|
||||
|
||||
// Set the previous element to be replaced by the new content
|
||||
if (content instanceof ReplaceableComponent && previousElement) {
|
||||
if (previousElement !== undefined) {
|
||||
content.previousElement = previousElement;
|
||||
}
|
||||
|
||||
if (content !== undefined) {
|
||||
this.addChild(content);
|
||||
}
|
||||
this.addChild(content);
|
||||
}
|
||||
|
||||
isForcedUnload(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,10 @@ export class DomComponent extends Component {
|
|||
}
|
||||
|
||||
isUnloadSkipped() {
|
||||
return this.parent !== null && this.parent.unmounted;
|
||||
if (this.parent === null) return false;
|
||||
if (this.parent.isForcedUnload()) return false;
|
||||
|
||||
return this.parent.unmounted;
|
||||
}
|
||||
|
||||
unload(): void {
|
||||
|
|
@ -55,9 +58,6 @@ export class DomComponent extends Component {
|
|||
onunload(): void {
|
||||
super.onunload();
|
||||
|
||||
// Mark ourselves as unmounted
|
||||
this.unmounted = true;
|
||||
|
||||
// Skip unmounting from the DOM if the parent is already unmounted
|
||||
if (this.isUnloadSkipped()) {
|
||||
return;
|
||||
|
|
@ -66,4 +66,15 @@ export class DomComponent extends Component {
|
|||
// Remove our wrapper if the parent is not unmounted already
|
||||
this.wrapperEl?.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to force the unloading of children, used by ContentComponent
|
||||
* which itself does not detach anything from the DOM instead relying
|
||||
* on the underlying ReplaceableComponent for DOM content
|
||||
*
|
||||
* @returns Whether the unload of children should be forced
|
||||
*/
|
||||
isForcedUnload() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
22
src/components/EmptyComponent/EmptyComponent.test.ts
Normal file
22
src/components/EmptyComponent/EmptyComponent.test.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
// @vitest-environment happy-dom
|
||||
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
|
||||
import { createMockContainer } from "@/__mocks__/obsidian";
|
||||
|
||||
import { EmptyComponent } from "./EmptyComponent";
|
||||
|
||||
describe("EmptyComponent", () => {
|
||||
let container: HTMLElement;
|
||||
let component: EmptyComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
container = createMockContainer();
|
||||
component = new EmptyComponent(container);
|
||||
});
|
||||
|
||||
it("should load without error", () => {
|
||||
expect(() => component.load()).not.toThrow();
|
||||
expect(container.children.length).toBe(1);
|
||||
});
|
||||
});
|
||||
13
src/components/EmptyComponent/EmptyComponent.ts
Normal file
13
src/components/EmptyComponent/EmptyComponent.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { ReplaceableComponent } from "../ReplaceableComponent";
|
||||
|
||||
export class EmptyComponent extends ReplaceableComponent {
|
||||
constructor(containerEl: HTMLElement) {
|
||||
super(containerEl);
|
||||
}
|
||||
|
||||
createContainer(): HTMLElement {
|
||||
return createDiv();
|
||||
}
|
||||
|
||||
render(_wrapperEl: HTMLElement): void {}
|
||||
}
|
||||
1
src/components/EmptyComponent/index.ts
Normal file
1
src/components/EmptyComponent/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { EmptyComponent } from "./EmptyComponent";
|
||||
|
|
@ -24,7 +24,6 @@ export abstract class ReplaceableComponent extends DomComponent {
|
|||
}
|
||||
|
||||
this.wrapperEl = wrapperEl;
|
||||
|
||||
super.onload();
|
||||
|
||||
this.render(wrapperEl);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import type { App } from "obsidian";
|
||||
|
||||
import { beforeEach, describe, expect, it, Mock, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import type { CustomOutputFormat } from "@/output";
|
||||
|
||||
|
|
@ -12,9 +12,6 @@ import { createStore, type Store } from "@/store";
|
|||
|
||||
import { Timesheet } from "./Timesheet";
|
||||
|
||||
import { TimesheetApp } from "@/components/TimesheetApp";
|
||||
import { TimesheetSaveError } from "@/components/TimesheetSaveError";
|
||||
|
||||
import type { Timekeep } from "@/timekeep/schema";
|
||||
|
||||
import { TimekeepAutocomplete } from "@/service/autocomplete";
|
||||
|
|
@ -25,52 +22,33 @@ describe("Timesheet", () => {
|
|||
let vault: MockVault;
|
||||
let app: App;
|
||||
let timekeep: Store<Timekeep>;
|
||||
let saveError: Store<boolean>;
|
||||
let settings: Store<TimekeepSettings>;
|
||||
let customOutputFormats: Store<Record<string, CustomOutputFormat>>;
|
||||
let registry: TimekeepRegistry;
|
||||
let autocomplete: TimekeepAutocomplete;
|
||||
let component: Timesheet;
|
||||
|
||||
let handleSaveTimekeep: Mock<(value: Timekeep) => Promise<void>>;
|
||||
|
||||
beforeEach(() => {
|
||||
app = {} as App;
|
||||
timekeep = createStore({ entries: [] });
|
||||
saveError = createStore(false);
|
||||
vault = new MockVault();
|
||||
containerEl = createMockContainer();
|
||||
settings = createStore(defaultSettings);
|
||||
customOutputFormats = createStore({});
|
||||
registry = new TimekeepRegistry(vault.asVault(), settings);
|
||||
autocomplete = new TimekeepAutocomplete(registry, settings);
|
||||
handleSaveTimekeep = vi.fn();
|
||||
|
||||
component = new Timesheet(
|
||||
containerEl,
|
||||
app,
|
||||
timekeep,
|
||||
saveError,
|
||||
settings,
|
||||
customOutputFormats,
|
||||
autocomplete,
|
||||
handleSaveTimekeep
|
||||
autocomplete
|
||||
);
|
||||
});
|
||||
|
||||
it("should load without error", () => {
|
||||
expect(() => component.load()).not.toThrow();
|
||||
});
|
||||
|
||||
it("default content should be the timesheet app", () => {
|
||||
component.load();
|
||||
expect(component.getContent()).toBeInstanceOf(TimesheetApp);
|
||||
});
|
||||
|
||||
it("on save error the content should be a save error message", () => {
|
||||
component.load();
|
||||
|
||||
saveError.setState(true);
|
||||
expect(component.getContent()).toBeInstanceOf(TimesheetSaveError);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,28 +1,29 @@
|
|||
import type { App } from "obsidian";
|
||||
import { App } from "obsidian";
|
||||
|
||||
import type { CustomOutputFormat } from "@/output";
|
||||
import type { TimekeepSettings } from "@/settings";
|
||||
import type { Store } from "@/store";
|
||||
import { CustomOutputFormat } from "@/output";
|
||||
import { TimekeepSettings } from "@/settings";
|
||||
import { Store } from "@/store";
|
||||
|
||||
import { ContentComponent } from "@/components/ContentComponent";
|
||||
import { TimesheetApp } from "@/components/TimesheetApp";
|
||||
import { TimesheetSaveError } from "@/components/TimesheetSaveError";
|
||||
import { ReplaceableComponent } from "../ReplaceableComponent";
|
||||
|
||||
import type { Timekeep } from "@/timekeep/schema";
|
||||
import { TimesheetCounters } from "@/components/TimesheetCounters";
|
||||
import { TimesheetExportActions } from "@/components/TimesheetExportActions";
|
||||
import { TimesheetRunningEntry } from "@/components/TimesheetRunningEntry";
|
||||
import { TimesheetStartForm } from "@/components/TimesheetStartForm";
|
||||
import { TimesheetTable } from "@/components/TimesheetTable";
|
||||
|
||||
import { Timekeep } from "@/timekeep/schema";
|
||||
|
||||
import { TimekeepAutocomplete } from "@/service/autocomplete";
|
||||
|
||||
/**
|
||||
* Wrapper component for the timesheet app that handles
|
||||
* display error messages when saving fails
|
||||
* View component for the timesheet app as a whole
|
||||
*/
|
||||
export class Timesheet extends ContentComponent<TimesheetApp | TimesheetSaveError> {
|
||||
export class Timesheet extends ReplaceableComponent {
|
||||
/** Access to the app instance */
|
||||
app: App;
|
||||
/** Access to the timekeep */
|
||||
timekeep: Store<Timekeep>;
|
||||
/** Store for save error state */
|
||||
saveError: Store<boolean>;
|
||||
/** Access to the timekeep settings */
|
||||
settings: Store<TimekeepSettings>;
|
||||
/** Access to custom output formats */
|
||||
|
|
@ -30,56 +31,55 @@ export class Timesheet extends ContentComponent<TimesheetApp | TimesheetSaveErro
|
|||
/** Autocomplete */
|
||||
autocomplete: TimekeepAutocomplete;
|
||||
|
||||
/** Callback to save the timekeep */
|
||||
handleSaveTimekeep: (value: Timekeep) => Promise<void>;
|
||||
|
||||
constructor(
|
||||
containerEl: HTMLElement,
|
||||
app: App,
|
||||
timekeep: Store<Timekeep>,
|
||||
saveError: Store<boolean>,
|
||||
settings: Store<TimekeepSettings>,
|
||||
customOutputFormats: Store<Record<string, CustomOutputFormat>>,
|
||||
autocomplete: TimekeepAutocomplete,
|
||||
handleSaveTimekeep: (value: Timekeep) => Promise<void>
|
||||
autocomplete: TimekeepAutocomplete
|
||||
) {
|
||||
super(containerEl);
|
||||
|
||||
this.app = app;
|
||||
this.timekeep = timekeep;
|
||||
this.saveError = saveError;
|
||||
this.settings = settings;
|
||||
this.customOutputFormats = customOutputFormats;
|
||||
this.autocomplete = autocomplete;
|
||||
this.handleSaveTimekeep = handleSaveTimekeep;
|
||||
}
|
||||
|
||||
onload(): void {
|
||||
super.onload();
|
||||
|
||||
const render = this.update.bind(this);
|
||||
const unsubscribeSaveError = this.saveError.subscribe(render);
|
||||
this.register(unsubscribeSaveError);
|
||||
render();
|
||||
createContainer(): HTMLElement {
|
||||
return createDiv({
|
||||
cls: "timekeep-container",
|
||||
});
|
||||
}
|
||||
|
||||
update() {
|
||||
const saveError = this.saveError.getState();
|
||||
if (saveError) {
|
||||
this.setContent(
|
||||
new TimesheetSaveError(this.containerEl, this.timekeep, this.handleSaveTimekeep)
|
||||
);
|
||||
} else {
|
||||
this.setContent(
|
||||
new TimesheetApp(
|
||||
this.containerEl,
|
||||
this.app,
|
||||
this.timekeep,
|
||||
this.settings,
|
||||
this.customOutputFormats,
|
||||
this.autocomplete
|
||||
)
|
||||
);
|
||||
}
|
||||
render(wrapperEl: HTMLElement): void {
|
||||
const counters = new TimesheetCounters(wrapperEl, this.settings, this.timekeep);
|
||||
|
||||
const startForm = new TimesheetStartForm(
|
||||
wrapperEl,
|
||||
this.timekeep,
|
||||
this.settings,
|
||||
this.autocomplete
|
||||
);
|
||||
|
||||
const runningEntry = new TimesheetRunningEntry(wrapperEl, this.timekeep, this.settings);
|
||||
|
||||
const table = new TimesheetTable(wrapperEl, this.app, this.timekeep, this.settings);
|
||||
|
||||
const exportActions = new TimesheetExportActions(
|
||||
wrapperEl,
|
||||
this.app,
|
||||
this.timekeep,
|
||||
this.settings,
|
||||
this.customOutputFormats
|
||||
);
|
||||
|
||||
this.addChild(counters);
|
||||
this.addChild(runningEntry);
|
||||
this.addChild(startForm);
|
||||
this.addChild(table);
|
||||
this.addChild(exportActions);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
// @vitest-environment happy-dom
|
||||
|
||||
import type { App } from "obsidian";
|
||||
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import type { CustomOutputFormat } from "@/output";
|
||||
|
||||
import { createMockContainer, MockVault } from "@/__mocks__/obsidian";
|
||||
import { defaultSettings, type TimekeepSettings } from "@/settings";
|
||||
import { createStore, type Store } from "@/store";
|
||||
|
||||
import { TimesheetApp } from "./TimesheetApp";
|
||||
|
||||
import type { Timekeep } from "@/timekeep/schema";
|
||||
|
||||
import { TimekeepAutocomplete } from "@/service/autocomplete";
|
||||
import { TimekeepRegistry } from "@/service/registry";
|
||||
|
||||
describe("TimesheetApp", () => {
|
||||
let containerEl: HTMLElement;
|
||||
let vault: MockVault;
|
||||
let app: App;
|
||||
let timekeep: Store<Timekeep>;
|
||||
let settings: Store<TimekeepSettings>;
|
||||
let customOutputFormats: Store<Record<string, CustomOutputFormat>>;
|
||||
let registry: TimekeepRegistry;
|
||||
let autocomplete: TimekeepAutocomplete;
|
||||
let component: TimesheetApp;
|
||||
|
||||
beforeEach(() => {
|
||||
app = {} as App;
|
||||
timekeep = createStore({ entries: [] });
|
||||
vault = new MockVault();
|
||||
containerEl = createMockContainer();
|
||||
settings = createStore(defaultSettings);
|
||||
customOutputFormats = createStore({});
|
||||
registry = new TimekeepRegistry(vault.asVault(), settings);
|
||||
autocomplete = new TimekeepAutocomplete(registry, settings);
|
||||
|
||||
component = new TimesheetApp(
|
||||
containerEl,
|
||||
app,
|
||||
timekeep,
|
||||
settings,
|
||||
customOutputFormats,
|
||||
autocomplete
|
||||
);
|
||||
});
|
||||
|
||||
it("should load without error", () => {
|
||||
expect(() => component.load()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
import { App } from "obsidian";
|
||||
|
||||
import { CustomOutputFormat } from "@/output";
|
||||
import { TimekeepSettings } from "@/settings";
|
||||
import { Store } from "@/store";
|
||||
|
||||
import { DomComponent } from "@/components/DomComponent";
|
||||
import { TimesheetCounters } from "@/components/TimesheetCounters";
|
||||
import { TimesheetExportActions } from "@/components/TimesheetExportActions";
|
||||
import { TimesheetRunningEntry } from "@/components/TimesheetRunningEntry";
|
||||
import { TimesheetStartForm } from "@/components/TimesheetStartForm";
|
||||
import { TimesheetTable } from "@/components/TimesheetTable";
|
||||
|
||||
import { Timekeep } from "@/timekeep/schema";
|
||||
|
||||
import { TimekeepAutocomplete } from "@/service/autocomplete";
|
||||
|
||||
/**
|
||||
* View component for the timesheet app as a whole
|
||||
*/
|
||||
export class TimesheetApp extends DomComponent {
|
||||
/** Access to the app instance */
|
||||
app: App;
|
||||
/** Access to the timekeep */
|
||||
timekeep: Store<Timekeep>;
|
||||
/** Access to the timekeep settings */
|
||||
settings: Store<TimekeepSettings>;
|
||||
/** Access to custom output formats */
|
||||
customOutputFormats: Store<Record<string, CustomOutputFormat>>;
|
||||
/** Autocomplete */
|
||||
autocomplete: TimekeepAutocomplete;
|
||||
|
||||
constructor(
|
||||
containerEl: HTMLElement,
|
||||
app: App,
|
||||
timekeep: Store<Timekeep>,
|
||||
settings: Store<TimekeepSettings>,
|
||||
customOutputFormats: Store<Record<string, CustomOutputFormat>>,
|
||||
autocomplete: TimekeepAutocomplete
|
||||
) {
|
||||
super(containerEl);
|
||||
|
||||
this.app = app;
|
||||
this.timekeep = timekeep;
|
||||
this.settings = settings;
|
||||
this.customOutputFormats = customOutputFormats;
|
||||
this.autocomplete = autocomplete;
|
||||
}
|
||||
|
||||
onload(): void {
|
||||
super.onload();
|
||||
|
||||
const wrapperEl = this.containerEl.createDiv({
|
||||
cls: "timekeep-container",
|
||||
});
|
||||
|
||||
this.wrapperEl = wrapperEl;
|
||||
|
||||
const counters = new TimesheetCounters(wrapperEl, this.settings, this.timekeep);
|
||||
|
||||
const startForm = new TimesheetStartForm(
|
||||
wrapperEl,
|
||||
this.timekeep,
|
||||
this.settings,
|
||||
this.autocomplete
|
||||
);
|
||||
|
||||
const runningEntry = new TimesheetRunningEntry(wrapperEl, this.timekeep, this.settings);
|
||||
|
||||
const table = new TimesheetTable(wrapperEl, this.app, this.timekeep, this.settings);
|
||||
|
||||
const exportActions = new TimesheetExportActions(
|
||||
wrapperEl,
|
||||
this.app,
|
||||
this.timekeep,
|
||||
this.settings,
|
||||
this.customOutputFormats
|
||||
);
|
||||
|
||||
this.addChild(counters);
|
||||
this.addChild(runningEntry);
|
||||
this.addChild(startForm);
|
||||
this.addChild(table);
|
||||
this.addChild(exportActions);
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { TimesheetApp } from "./TimesheetApp";
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { DomComponent } from "@/components/DomComponent";
|
||||
import { ReplaceableComponent } from "../ReplaceableComponent";
|
||||
|
||||
/**
|
||||
* Component for rendering a load error for a timesheet
|
||||
*/
|
||||
export class TimesheetLoadError extends DomComponent {
|
||||
export class TimesheetLoadError extends ReplaceableComponent {
|
||||
/** The load error message */
|
||||
error: string;
|
||||
|
||||
|
|
@ -13,13 +13,13 @@ export class TimesheetLoadError extends DomComponent {
|
|||
this.error = error;
|
||||
}
|
||||
|
||||
onload(): void {
|
||||
super.onload();
|
||||
|
||||
const wrapperEl = this.containerEl.createDiv({
|
||||
createContainer(): HTMLElement {
|
||||
return createDiv({
|
||||
cls: "timekeep-container",
|
||||
});
|
||||
this.wrapperEl = wrapperEl;
|
||||
}
|
||||
|
||||
render(wrapperEl: HTMLElement): void {
|
||||
wrapperEl.createEl("p", {
|
||||
text: `Failed to load timekeep: ${this.error}`,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { createMockContainer } from "@/__mocks__/obsidian";
|
|||
import { defaultSettings } from "@/settings";
|
||||
import { createStore } from "@/store";
|
||||
|
||||
import { EmptyComponent } from "../EmptyComponent";
|
||||
import { TimesheetRunningEntry } from "./TimesheetRunningEntry";
|
||||
import { TimesheetRunningEntryEditing } from "./TimesheetRunningEntryEditing";
|
||||
import { TimesheetRunningEntryViewing } from "./TimesheetRunningEntryViewing";
|
||||
|
|
@ -41,7 +42,7 @@ describe("TimesheetRunningEntry", () => {
|
|||
it("should be undefined content without a running entry", () => {
|
||||
component = new TimesheetRunningEntry(containerEl, timekeep, settings);
|
||||
component.load();
|
||||
expect(component.getContent()).toBeUndefined();
|
||||
expect(component.getContent()).toBeInstanceOf(EmptyComponent);
|
||||
});
|
||||
|
||||
it("should be TimesheetStartRunning when theres a running entry", () => {
|
||||
|
|
@ -114,7 +115,7 @@ describe("TimesheetRunningEntry", () => {
|
|||
expect(component.getContent()).toBeInstanceOf(TimesheetRunningEntryEditing);
|
||||
});
|
||||
|
||||
it("after a timekeep update when editing if theres no more current entry should return to undefined state", () => {
|
||||
it("after a timekeep update when editing if theres no more current entry should return to empty component state", () => {
|
||||
const start = moment();
|
||||
timekeep.setState({
|
||||
entries: [
|
||||
|
|
@ -138,6 +139,6 @@ describe("TimesheetRunningEntry", () => {
|
|||
|
||||
timekeep.setState({ entries: [] });
|
||||
|
||||
expect(component.getContent()).toBeUndefined();
|
||||
expect(component.getContent()).toBeInstanceOf(EmptyComponent);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type { Store } from "@/store";
|
|||
|
||||
import { assert } from "@/utils/assert";
|
||||
|
||||
import { EmptyComponent } from "../EmptyComponent";
|
||||
import { TimesheetRunningEntryEditing } from "./TimesheetRunningEntryEditing";
|
||||
import { TimesheetRunningEntryViewing } from "./TimesheetRunningEntryViewing";
|
||||
|
||||
|
|
@ -12,7 +13,7 @@ import { getRunningEntry } from "@/timekeep/queries";
|
|||
import type { Timekeep } from "@/timekeep/schema";
|
||||
|
||||
export class TimesheetRunningEntry extends ContentComponent<
|
||||
TimesheetRunningEntryViewing | TimesheetRunningEntryEditing
|
||||
TimesheetRunningEntryViewing | TimesheetRunningEntryEditing | EmptyComponent
|
||||
> {
|
||||
/** Access to the timekeep */
|
||||
timekeep: Store<Timekeep>;
|
||||
|
|
@ -57,7 +58,7 @@ export class TimesheetRunningEntry extends ContentComponent<
|
|||
const timekeep = this.timekeep.getState();
|
||||
const currentEntry = getRunningEntry(timekeep.entries);
|
||||
if (!currentEntry) {
|
||||
this.setContent(undefined);
|
||||
this.setContent(new EmptyComponent(contentEl));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -82,7 +83,7 @@ export class TimesheetRunningEntry extends ContentComponent<
|
|||
const timekeep = this.timekeep.getState();
|
||||
const currentEntry = getRunningEntry(timekeep.entries);
|
||||
if (!currentEntry) {
|
||||
this.setContent(undefined);
|
||||
this.setContent(new EmptyComponent(contentEl));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import type { Store } from "@/store";
|
|||
|
||||
import { assert } from "@/utils/assert";
|
||||
|
||||
import { DomComponent } from "@/components/DomComponent";
|
||||
import { ReplaceableComponent } from "../ReplaceableComponent";
|
||||
|
||||
import { createObsidianIcon } from "@/components/obsidianIcon";
|
||||
|
||||
import { getRunningEntry } from "@/timekeep/queries";
|
||||
|
|
@ -14,7 +15,7 @@ import { updateEntry } from "@/timekeep/update";
|
|||
* The editing section for editing the currently
|
||||
* running time entry within the start section
|
||||
*/
|
||||
export class TimesheetRunningEntryEditing extends DomComponent {
|
||||
export class TimesheetRunningEntryEditing extends ReplaceableComponent {
|
||||
/** Access to the timekeep */
|
||||
timekeep: Store<Timekeep>;
|
||||
/** Access to the timekeep settings */
|
||||
|
|
@ -47,15 +48,17 @@ export class TimesheetRunningEntryEditing extends DomComponent {
|
|||
this.onFinishEditing = onFinishEditing;
|
||||
}
|
||||
|
||||
onload(): void {
|
||||
super.onload();
|
||||
|
||||
const formEl = this.containerEl.createEl("form", {
|
||||
createContainer(): HTMLElement {
|
||||
return createEl("form", {
|
||||
cls: "timekeep-start-area",
|
||||
attr: {
|
||||
"data-area": "start",
|
||||
},
|
||||
});
|
||||
formEl.setAttribute("data-area", "start");
|
||||
}
|
||||
|
||||
render(formEl: HTMLElement): void {
|
||||
this.registerDomEvent(formEl, "submit", this.onSave.bind(this));
|
||||
this.wrapperEl = formEl;
|
||||
|
||||
const nameWrapperEl = formEl.createDiv({
|
||||
cls: "timekeep-name-wrapper",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import type { Store } from "@/store";
|
|||
import { assert } from "@/utils/assert";
|
||||
import { formatTimestamp } from "@/utils/time";
|
||||
|
||||
import { DomComponent } from "@/components/DomComponent";
|
||||
import { ReplaceableComponent } from "../ReplaceableComponent";
|
||||
|
||||
import { createObsidianIcon } from "@/components/obsidianIcon";
|
||||
|
||||
import { getPathToEntry } from "@/timekeep/queries";
|
||||
|
|
@ -16,7 +17,7 @@ import { stopTimekeep } from "@/timekeep/update";
|
|||
/**
|
||||
* The "Running" timer section of the timesheet start are
|
||||
*/
|
||||
export class TimesheetRunningEntryViewing extends DomComponent {
|
||||
export class TimesheetRunningEntryViewing extends ReplaceableComponent {
|
||||
/** Access to the timekeep */
|
||||
timekeep: Store<Timekeep>;
|
||||
/** Access to the timekeep settings */
|
||||
|
|
@ -52,14 +53,16 @@ export class TimesheetRunningEntryViewing extends DomComponent {
|
|||
this.onStartEditing = onStartEditing;
|
||||
}
|
||||
|
||||
onload(): void {
|
||||
super.onload();
|
||||
|
||||
const formEl = this.containerEl.createEl("form", {
|
||||
createContainer(): HTMLElement {
|
||||
return createEl("form", {
|
||||
cls: "timekeep-start-area",
|
||||
attr: {
|
||||
"data-area": "running",
|
||||
},
|
||||
});
|
||||
formEl.setAttribute("data-area", "running");
|
||||
this.wrapperEl = formEl;
|
||||
}
|
||||
|
||||
render(formEl: HTMLElement): void {
|
||||
this.registerDomEvent(formEl, "submit", this.onStop.bind(this));
|
||||
|
||||
const nameWrapperEl = formEl.createDiv({
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import { stripTimekeepRuntimeData, Timekeep } from "@/timekeep/schema";
|
|||
describe("TimesheetSaveError", () => {
|
||||
let container: HTMLElement;
|
||||
let timekeepStore: Store<Timekeep>;
|
||||
let handleSaveTimekeep: (value: Timekeep) => Promise<void>;
|
||||
let component: TimesheetSaveError;
|
||||
|
||||
let writeText: Mock<() => any>;
|
||||
|
|
@ -20,8 +19,7 @@ describe("TimesheetSaveError", () => {
|
|||
beforeEach(() => {
|
||||
container = createMockContainer();
|
||||
timekeepStore = createStore({ entries: [] });
|
||||
handleSaveTimekeep = vi.fn().mockResolvedValue(undefined as void);
|
||||
component = new TimesheetSaveError(container, timekeepStore, handleSaveTimekeep);
|
||||
component = new TimesheetSaveError(container, timekeepStore);
|
||||
|
||||
writeText = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
|
|
@ -55,9 +53,10 @@ describe("TimesheetSaveError", () => {
|
|||
});
|
||||
|
||||
it("calls handleSaveTimekeep on retry button click", () => {
|
||||
const setState = vi.spyOn(timekeepStore, "setState");
|
||||
component.load();
|
||||
component.onRetrySave();
|
||||
expect(handleSaveTimekeep).toHaveBeenCalledWith(timekeepStore.getState());
|
||||
expect(setState).toHaveBeenCalledWith(timekeepStore.getState());
|
||||
});
|
||||
|
||||
it("writes JSON to clipboard on copy button click", async () => {
|
||||
|
|
|
|||
|
|
@ -1,39 +1,28 @@
|
|||
import type { Store } from "@/store";
|
||||
|
||||
import { DomComponent } from "@/components/DomComponent";
|
||||
import { ReplaceableComponent } from "../ReplaceableComponent";
|
||||
|
||||
import { stripTimekeepRuntimeData, type Timekeep } from "@/timekeep/schema";
|
||||
|
||||
type HandleSaveTimekeep = (value: Timekeep) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Component for showing the "Failed to save current timekeep" error
|
||||
*/
|
||||
export class TimesheetSaveError extends DomComponent {
|
||||
export class TimesheetSaveError extends ReplaceableComponent {
|
||||
/** Access to the timekeep */
|
||||
timekeep: Store<Timekeep>;
|
||||
/** Callback to save the timekeep */
|
||||
handleSaveTimekeep: HandleSaveTimekeep;
|
||||
|
||||
constructor(
|
||||
containerEl: HTMLElement,
|
||||
timekeep: Store<Timekeep>,
|
||||
handleSaveTimekeep: HandleSaveTimekeep
|
||||
) {
|
||||
constructor(containerEl: HTMLElement, timekeep: Store<Timekeep>) {
|
||||
super(containerEl);
|
||||
|
||||
this.timekeep = timekeep;
|
||||
this.handleSaveTimekeep = handleSaveTimekeep;
|
||||
}
|
||||
|
||||
onload(): void {
|
||||
super.onload();
|
||||
|
||||
const wrapperEl = this.containerEl.createDiv({
|
||||
createContainer(): HTMLElement {
|
||||
return createDiv({
|
||||
cls: "timekeep-container",
|
||||
});
|
||||
this.wrapperEl = wrapperEl;
|
||||
}
|
||||
|
||||
render(wrapperEl: HTMLElement): void {
|
||||
const errorEl = wrapperEl.createDiv({ cls: "timekeep-error" });
|
||||
errorEl.createEl("h1", { text: "Warning" });
|
||||
errorEl.createEl("p", { text: "Failed to save current timekeep" });
|
||||
|
|
@ -57,8 +46,8 @@ export class TimesheetSaveError extends DomComponent {
|
|||
}
|
||||
|
||||
onRetrySave() {
|
||||
// Attempt to save the current timekeep
|
||||
void this.handleSaveTimekeep(this.timekeep.getState());
|
||||
// Set the state to itself to trigger another save attempt
|
||||
this.timekeep.setState(this.timekeep.getState());
|
||||
}
|
||||
|
||||
async onCopy() {
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ export class TimesheetTable extends DomComponent {
|
|||
clearRows() {
|
||||
// Unload existing children and reset the rows list
|
||||
for (const row of this.#rows) {
|
||||
console.log(row);
|
||||
this.removeChild(row);
|
||||
}
|
||||
|
||||
|
|
|
|||
36
src/save/TimesheetFileSaveAdapter.ts
Normal file
36
src/save/TimesheetFileSaveAdapter.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import type { TFile, Vault } from "obsidian";
|
||||
|
||||
import type { TimesheetSaveAdapter } from "./TimesheetSaveAdapter";
|
||||
|
||||
import { stripTimekeepRuntimeData, type Timekeep } from "@/timekeep/schema";
|
||||
|
||||
export class TimesheetFileSaveAdapter implements TimesheetSaveAdapter {
|
||||
/** Access to the vault */
|
||||
vault: Vault;
|
||||
|
||||
/** File that should be saved to */
|
||||
file: TFile | null;
|
||||
|
||||
constructor(vault: Vault, file: TFile | null) {
|
||||
this.vault = vault;
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
onLoad(): void {}
|
||||
onUnload(): void {}
|
||||
|
||||
async onSave(timekeep: Timekeep): Promise<void> {
|
||||
if (this.file === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const stripped = stripTimekeepRuntimeData(timekeep);
|
||||
const serialized = JSON.stringify(stripped);
|
||||
|
||||
await this.vault.modify(this.file, serialized);
|
||||
} catch (error) {
|
||||
console.error("failed to save file", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
72
src/save/TimesheetMarkdownSaveAdapter.ts
Normal file
72
src/save/TimesheetMarkdownSaveAdapter.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import {
|
||||
TFile,
|
||||
Vault,
|
||||
type EventRef,
|
||||
type MarkdownPostProcessorContext,
|
||||
type TAbstractFile,
|
||||
} from "obsidian";
|
||||
|
||||
import type { TimesheetSaveAdapter } from "./TimesheetSaveAdapter";
|
||||
|
||||
import { replaceTimekeepCodeblock } from "@/timekeep/parser";
|
||||
import type { Timekeep } from "@/timekeep/schema";
|
||||
|
||||
export class TimesheetMarkdownSaveAdapter implements TimesheetSaveAdapter {
|
||||
/** Access to the vault */
|
||||
vault: Vault;
|
||||
|
||||
/** Context from the markdown */
|
||||
context: MarkdownPostProcessorContext;
|
||||
/** HTML container element */
|
||||
containerEl: HTMLElement;
|
||||
/** Path to the file the timekeep is within */
|
||||
fileSourcePath: string;
|
||||
|
||||
/** Event ref for the registered rename event */
|
||||
renameEventRef: EventRef | undefined;
|
||||
|
||||
constructor(vault: Vault, containerEl: HTMLElement, context: MarkdownPostProcessorContext) {
|
||||
this.vault = vault;
|
||||
this.containerEl = containerEl;
|
||||
this.context = context;
|
||||
this.fileSourcePath = context.sourcePath;
|
||||
}
|
||||
|
||||
onLoad(): void {
|
||||
this.renameEventRef = this.vault.on("rename", this.onVaultRename.bind(this));
|
||||
}
|
||||
|
||||
onUnload(): void {
|
||||
if (this.renameEventRef) {
|
||||
this.vault.offref(this.renameEventRef);
|
||||
}
|
||||
}
|
||||
|
||||
onVaultRename(file: TAbstractFile, oldName: string) {
|
||||
if (file instanceof TFile && oldName == this.fileSourcePath) {
|
||||
this.fileSourcePath = file.path;
|
||||
}
|
||||
}
|
||||
|
||||
async onSave(timekeep: Timekeep): Promise<void> {
|
||||
const sectionInfo = this.context.getSectionInfo(this.containerEl);
|
||||
|
||||
// Ensure we actually have a section to write to
|
||||
if (sectionInfo === null) throw new Error("Section to write did not exist");
|
||||
|
||||
const file = this.vault.getFileByPath(this.fileSourcePath);
|
||||
|
||||
// Ensure the file still exists
|
||||
if (file === null) throw new Error("File no longer exists");
|
||||
|
||||
// Replace the stored timekeep block with the new one
|
||||
await this.vault.process(file, (data) => {
|
||||
return replaceTimekeepCodeblock(
|
||||
timekeep,
|
||||
data,
|
||||
sectionInfo.lineStart,
|
||||
sectionInfo.lineEnd
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
9
src/save/TimesheetSaveAdapter.ts
Normal file
9
src/save/TimesheetSaveAdapter.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { Timekeep } from "@/timekeep/schema";
|
||||
|
||||
export interface TimesheetSaveAdapter {
|
||||
onLoad(): void;
|
||||
|
||||
onUnload(): void;
|
||||
|
||||
onSave(timekeep: Timekeep): Promise<void>;
|
||||
}
|
||||
134
src/utils/editorScrollTracker.test.ts
Normal file
134
src/utils/editorScrollTracker.test.ts
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
import { EditorScrollTracker } from "./editorScrollTracker";
|
||||
|
||||
describe("EditorScrollTracker", () => {
|
||||
let mockApp: any;
|
||||
let mockEditor: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockEditor = {
|
||||
getScrollInfo: vi.fn(),
|
||||
scrollTo: vi.fn(),
|
||||
};
|
||||
|
||||
mockApp = {
|
||||
workspace: {
|
||||
activeEditor: {
|
||||
editor: mockEditor,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe("save", () => {
|
||||
it("saves scroll info when editor exists", () => {
|
||||
const scrollInfo = { top: 100, left: 50 };
|
||||
mockEditor.getScrollInfo.mockReturnValue(scrollInfo);
|
||||
|
||||
const tracker = new EditorScrollTracker(mockApp);
|
||||
tracker.save();
|
||||
|
||||
expect(mockEditor.getScrollInfo).toHaveBeenCalled();
|
||||
expect(tracker.restoreScrollInfo).toEqual(scrollInfo);
|
||||
});
|
||||
|
||||
it("does nothing if activeEditor is missing", () => {
|
||||
mockApp.workspace.activeEditor = null;
|
||||
|
||||
const tracker = new EditorScrollTracker(mockApp);
|
||||
tracker.save();
|
||||
|
||||
expect(tracker.restoreScrollInfo).toBeNull();
|
||||
});
|
||||
|
||||
it("does nothing if editor is missing", () => {
|
||||
mockApp.workspace.activeEditor.editor = null;
|
||||
|
||||
const tracker = new EditorScrollTracker(mockApp);
|
||||
tracker.save();
|
||||
|
||||
expect(tracker.restoreScrollInfo).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("restore", () => {
|
||||
it("restores scroll when info and editor exist", () => {
|
||||
const tracker = new EditorScrollTracker(mockApp);
|
||||
tracker.restoreScrollInfo = { top: 200, left: 75 };
|
||||
|
||||
tracker.restore();
|
||||
|
||||
expect(mockEditor.scrollTo).toHaveBeenCalledWith(75, 200);
|
||||
});
|
||||
|
||||
it("does nothing if restoreScrollInfo is null", () => {
|
||||
const tracker = new EditorScrollTracker(mockApp);
|
||||
|
||||
tracker.restore();
|
||||
|
||||
expect(mockEditor.scrollTo).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does nothing if activeEditor is missing", () => {
|
||||
const tracker = new EditorScrollTracker(mockApp);
|
||||
tracker.restoreScrollInfo = { top: 10, left: 20 };
|
||||
|
||||
mockApp.workspace.activeEditor = null;
|
||||
|
||||
tracker.restore();
|
||||
|
||||
expect(mockEditor.scrollTo).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does nothing if editor is missing", () => {
|
||||
const tracker = new EditorScrollTracker(mockApp);
|
||||
tracker.restoreScrollInfo = { top: 10, left: 20 };
|
||||
|
||||
mockApp.workspace.activeEditor.editor = null;
|
||||
|
||||
tracker.restore();
|
||||
|
||||
expect(mockEditor.scrollTo).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("queueRestore", () => {
|
||||
it("queues restore after delay", () => {
|
||||
const tracker = new EditorScrollTracker(mockApp);
|
||||
tracker.restoreScrollInfo = { top: 300, left: 150 };
|
||||
|
||||
tracker.queueRestore(100);
|
||||
|
||||
expect(mockEditor.scrollTo).not.toHaveBeenCalled();
|
||||
|
||||
vi.advanceTimersByTime(100);
|
||||
|
||||
expect(mockEditor.scrollTo).toHaveBeenCalledWith(150, 300);
|
||||
});
|
||||
|
||||
it("clears previous timeout before setting a new one", () => {
|
||||
const tracker = new EditorScrollTracker(mockApp);
|
||||
tracker.restoreScrollInfo = { top: 400, left: 200 };
|
||||
|
||||
const clearSpy = vi.spyOn(global, "clearTimeout");
|
||||
|
||||
tracker.queueRestore(100);
|
||||
tracker.queueRestore(200);
|
||||
|
||||
expect(clearSpy).toHaveBeenCalled();
|
||||
|
||||
vi.advanceTimersByTime(200);
|
||||
|
||||
expect(mockEditor.scrollTo).toHaveBeenCalledTimes(1);
|
||||
expect(mockEditor.scrollTo).toHaveBeenCalledWith(200, 400);
|
||||
});
|
||||
});
|
||||
});
|
||||
48
src/utils/editorScrollTracker.ts
Normal file
48
src/utils/editorScrollTracker.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import type { App } from "obsidian";
|
||||
|
||||
/**
|
||||
* Utility to handle tracking the editor scroll for saving and restoring
|
||||
* scroll on the markdown view
|
||||
*/
|
||||
export class EditorScrollTracker {
|
||||
app: App;
|
||||
|
||||
restoreScrollTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
restoreScrollInfo: { top: number; left: number } | null = null;
|
||||
|
||||
constructor(app: App) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
save() {
|
||||
const activeEditor = this.app.workspace.activeEditor;
|
||||
if (!activeEditor) return;
|
||||
const editor = activeEditor.editor;
|
||||
if (!editor) return;
|
||||
this.restoreScrollInfo = editor.getScrollInfo();
|
||||
}
|
||||
|
||||
restore() {
|
||||
if (this.restoreScrollInfo === null) return;
|
||||
const activeEditor = this.app.workspace.activeEditor;
|
||||
|
||||
if (!activeEditor) return;
|
||||
const editor = activeEditor.editor;
|
||||
|
||||
if (!editor) return;
|
||||
editor.scrollTo(this.restoreScrollInfo.left, this.restoreScrollInfo.top);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue a restore to happen after a delay
|
||||
*
|
||||
* @param delayMs The delay in milliseconds before restoring
|
||||
*/
|
||||
queueRestore(delayMs: number = 50) {
|
||||
if (this.restoreScrollTimeout) {
|
||||
clearTimeout(this.restoreScrollTimeout);
|
||||
}
|
||||
|
||||
this.restoreScrollTimeout = setTimeout(this.restore.bind(this), delayMs);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,13 @@
|
|||
import moment from "moment";
|
||||
import { EditableFileView, TFile, WorkspaceLeaf } from "obsidian";
|
||||
|
||||
import { CustomOutputFormat } from "@/output";
|
||||
import { TimesheetFileSaveAdapter } from "@/save/TimesheetFileSaveAdapter";
|
||||
import { TimekeepSettings } from "@/settings";
|
||||
import { createStore, Store } from "@/store";
|
||||
|
||||
import { Timesheet } from "@/components/Timesheet";
|
||||
import { TimesheetLoadError } from "@/components/TimesheetLoadError";
|
||||
import TimekeepView from "./TimekeepView";
|
||||
|
||||
import { load, LoadResult } from "@/timekeep/parser";
|
||||
import { stripTimekeepRuntimeData, Timekeep } from "@/timekeep/schema";
|
||||
|
||||
import { TimekeepAutocomplete } from "@/service/autocomplete";
|
||||
|
||||
|
|
@ -21,13 +19,16 @@ export default class TimekeepFileView extends EditableFileView {
|
|||
/** Autocomplete */
|
||||
autocomplete: TimekeepAutocomplete;
|
||||
|
||||
/** Loading result for the timekeep data */
|
||||
loadResult: Store<LoadResult | null>;
|
||||
/** Container wrapper element */
|
||||
wrapperEl: HTMLElement | undefined;
|
||||
|
||||
/** The rendered timesheet component */
|
||||
timesheet: Timesheet | TimesheetLoadError | undefined;
|
||||
/** Loading result for the timekeep data */
|
||||
loadResult: Store<LoadResult | null>;
|
||||
/** The timekeep view */
|
||||
timesheet: TimekeepView | undefined;
|
||||
|
||||
/** Adapter for saving to the file */
|
||||
saveAdapter: TimesheetFileSaveAdapter;
|
||||
|
||||
constructor(
|
||||
leaf: WorkspaceLeaf,
|
||||
|
|
@ -38,10 +39,11 @@ export default class TimekeepFileView extends EditableFileView {
|
|||
super(leaf);
|
||||
|
||||
this.loadResult = createStore(null);
|
||||
|
||||
this.settings = settings;
|
||||
this.customOutputFormats = customOutputFormats;
|
||||
this.autocomplete = autocomplete;
|
||||
|
||||
this.saveAdapter = new TimesheetFileSaveAdapter(this.app.vault, this.file);
|
||||
}
|
||||
|
||||
onload(): void {
|
||||
|
|
@ -50,68 +52,16 @@ export default class TimekeepFileView extends EditableFileView {
|
|||
const wrapperEl = this.contentEl.createDiv({ cls: "timekeep-file" });
|
||||
this.wrapperEl = wrapperEl;
|
||||
|
||||
const render = this.render.bind(this);
|
||||
const unsubscribeLoadResult = this.loadResult.subscribe(render);
|
||||
render();
|
||||
this.register(unsubscribeLoadResult);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.wrapperEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loadResult = this.loadResult.getState();
|
||||
if (!loadResult) return;
|
||||
|
||||
if (this.timesheet) {
|
||||
this.removeChild(this.timesheet);
|
||||
}
|
||||
|
||||
// Render the content
|
||||
if (loadResult.success) {
|
||||
const timekeep = loadResult.timekeep;
|
||||
|
||||
const timekeepStore = createStore(timekeep);
|
||||
const saveErrorStore = createStore(false);
|
||||
|
||||
const trySave = this.trySave.bind(this);
|
||||
|
||||
const handleSaveTimekeep = async (timekeep: Timekeep) => {
|
||||
// Attempt to save the timekeep changes
|
||||
const result = await trySave(timekeep);
|
||||
|
||||
const saveError = !result;
|
||||
|
||||
// Update the save error state
|
||||
if (saveErrorStore.getState() !== saveError) {
|
||||
saveErrorStore.setState(saveError);
|
||||
}
|
||||
};
|
||||
|
||||
// Subscribe to save when timekeep changes
|
||||
timekeepStore.subscribe(() => {
|
||||
void handleSaveTimekeep(timekeepStore.getState());
|
||||
});
|
||||
|
||||
const timesheet = new Timesheet(
|
||||
this.wrapperEl,
|
||||
this.app,
|
||||
timekeepStore,
|
||||
saveErrorStore,
|
||||
this.settings,
|
||||
this.customOutputFormats,
|
||||
this.autocomplete,
|
||||
handleSaveTimekeep
|
||||
);
|
||||
|
||||
this.addChild(timesheet);
|
||||
this.timesheet = timesheet;
|
||||
} else {
|
||||
const timesheet = new TimesheetLoadError(this.wrapperEl, loadResult.error);
|
||||
this.addChild(timesheet);
|
||||
this.timesheet = timesheet;
|
||||
}
|
||||
this.timesheet = new TimekeepView(
|
||||
wrapperEl,
|
||||
this.app,
|
||||
this.settings,
|
||||
this.customOutputFormats,
|
||||
this.autocomplete,
|
||||
this.loadResult,
|
||||
this.saveAdapter
|
||||
);
|
||||
this.addChild(this.timesheet);
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
|
|
@ -129,74 +79,8 @@ export default class TimekeepFileView extends EditableFileView {
|
|||
async onLoadFile(file: TFile): Promise<void> {
|
||||
await super.onLoadFile(file);
|
||||
|
||||
this.saveAdapter.file = file;
|
||||
const fileData = await this.app.vault.read(file);
|
||||
this.loadResult.setState(load(fileData));
|
||||
}
|
||||
|
||||
async onUnloadFile(file: TFile): Promise<void> {
|
||||
await super.onUnloadFile(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to save the file normally, if this fails it also attempts
|
||||
* to save a fallback file
|
||||
*
|
||||
* @param timekeep
|
||||
* @returns Promise of a boolean indicating weather the save was a success
|
||||
*/
|
||||
async trySave(timekeep: Timekeep): Promise<boolean> {
|
||||
try {
|
||||
await this.save(timekeep);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error("Failed to save timekeep", e);
|
||||
|
||||
try {
|
||||
await this.saveFallback(timekeep);
|
||||
} catch (e) {
|
||||
console.error("Couldn't save timekeep fallback", e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to save the timekeep within the current file
|
||||
*
|
||||
* @param timekeep The new timekeep data to save
|
||||
*/
|
||||
async save(timekeep: Timekeep) {
|
||||
if (this.file === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const stripped = stripTimekeepRuntimeData(timekeep);
|
||||
const serialized = JSON.stringify(stripped);
|
||||
|
||||
await this.app.vault.modify(this.file, serialized);
|
||||
} catch (error) {
|
||||
console.error("failed to save file", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback saving in case writing back to the timekeep block fails,
|
||||
* if writing back fails attempt to write to a backup temporary file
|
||||
* using the current date time
|
||||
*
|
||||
* @param timekeep The timekeep to save
|
||||
*/
|
||||
async saveFallback(timekeep: Timekeep) {
|
||||
// Fallback in case of write failure, attempt to write to another file
|
||||
const backupFileName = `timekeep-write-backup-${moment().format("YYYY-MM-DD HH-mm-ss")}.json`;
|
||||
|
||||
// Write to the backup file
|
||||
await this.app.vault.create(
|
||||
backupFileName,
|
||||
JSON.stringify(stripTimekeepRuntimeData(timekeep))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,45 +1,46 @@
|
|||
import type { App } from "obsidian";
|
||||
import type { TAbstractFile, MarkdownPostProcessorContext } from "obsidian";
|
||||
import type { MarkdownPostProcessorContext } from "obsidian";
|
||||
|
||||
import moment from "moment";
|
||||
import { TFile, MarkdownRenderChild } from "obsidian";
|
||||
import { MarkdownRenderChild } from "obsidian";
|
||||
|
||||
import type { CustomOutputFormat } from "@/output";
|
||||
import type { TimekeepSettings } from "@/settings";
|
||||
|
||||
import { TimesheetMarkdownSaveAdapter } from "@/save/TimesheetMarkdownSaveAdapter";
|
||||
import { type Store, createStore } from "@/store";
|
||||
import { EditorScrollTracker } from "@/utils/editorScrollTracker";
|
||||
|
||||
import { Timesheet } from "@/components/Timesheet";
|
||||
import { TimesheetLoadError } from "@/components/TimesheetLoadError";
|
||||
import TimekeepView from "./TimekeepView";
|
||||
|
||||
import { type LoadResult, load, replaceTimekeepCodeblock } from "@/timekeep/parser";
|
||||
import { type Timekeep, stripTimekeepRuntimeData } from "@/timekeep/schema";
|
||||
import { type LoadResult, load } from "@/timekeep/parser";
|
||||
|
||||
import { TimekeepAutocomplete } from "@/service/autocomplete";
|
||||
|
||||
export default class TimekeepMarkdownView extends MarkdownRenderChild {
|
||||
// Obsidian app instance
|
||||
/** Obsidian app instance */
|
||||
app: App;
|
||||
// Timekeep settings store
|
||||
settingsStore: Store<TimekeepSettings>;
|
||||
// Custom output formats store
|
||||
/** Timekeep settings store */
|
||||
settings: Store<TimekeepSettings>;
|
||||
/** Custom output formats store */
|
||||
customOutputFormats: Store<Record<string, CustomOutputFormat>>;
|
||||
// Markdown context for the current markdown block
|
||||
context: MarkdownPostProcessorContext;
|
||||
// Timekeep load result
|
||||
loadResult: LoadResult;
|
||||
/** The rendered timesheet component */
|
||||
timesheet: Timesheet | TimesheetLoadError | undefined;
|
||||
/** Autocomplete */
|
||||
autocomplete: TimekeepAutocomplete;
|
||||
|
||||
// Path to the file the timekeep is within
|
||||
fileSourcePath: string;
|
||||
/** Timekeep load result */
|
||||
loadResult: Store<LoadResult | null>;
|
||||
/** The timekeep view */
|
||||
timesheet: TimekeepView | undefined;
|
||||
|
||||
/** Scroll tracking for restoring scroll after save */
|
||||
scrollTracker: EditorScrollTracker;
|
||||
|
||||
/** Adapter for how the timesheet should be saved */
|
||||
saveAdapter: TimesheetMarkdownSaveAdapter;
|
||||
|
||||
constructor(
|
||||
containerEl: HTMLElement,
|
||||
app: App,
|
||||
settingsStore: Store<TimekeepSettings>,
|
||||
settings: Store<TimekeepSettings>,
|
||||
customOutputFormats: Store<Record<string, CustomOutputFormat>>,
|
||||
autocomplete: TimekeepAutocomplete,
|
||||
context: MarkdownPostProcessorContext,
|
||||
|
|
@ -47,24 +48,24 @@ export default class TimekeepMarkdownView extends MarkdownRenderChild {
|
|||
) {
|
||||
super(containerEl);
|
||||
this.app = app;
|
||||
this.settingsStore = settingsStore;
|
||||
this.settings = settings;
|
||||
this.customOutputFormats = customOutputFormats;
|
||||
this.autocomplete = autocomplete;
|
||||
this.context = context;
|
||||
this.loadResult = loadResult;
|
||||
|
||||
// Set initial file path
|
||||
this.fileSourcePath = context.sourcePath;
|
||||
this.loadResult = createStore(loadResult);
|
||||
|
||||
this.scrollTracker = new EditorScrollTracker(app);
|
||||
this.saveAdapter = new TimesheetMarkdownSaveAdapter(this.app.vault, containerEl, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a markdown code block processor for this view
|
||||
*
|
||||
* @param app
|
||||
* @param settingsStore
|
||||
* @param customOutputFormats
|
||||
* @param autocomplete
|
||||
* @returns
|
||||
* @param app The app to use
|
||||
* @param settingsStore The settings store
|
||||
* @param customOutputFormats The custom output formats store
|
||||
* @param autocomplete The autocomplete store
|
||||
* @returns The markdown post processor factory
|
||||
*/
|
||||
static markdownPostProcessor(
|
||||
app: App,
|
||||
|
|
@ -89,155 +90,22 @@ export default class TimekeepMarkdownView extends MarkdownRenderChild {
|
|||
}
|
||||
|
||||
onload(): void {
|
||||
// Hook file renaming to update the file we are saving to if its renamed
|
||||
this.registerEvent(
|
||||
this.app.vault.on("rename", (file: TAbstractFile, oldName: string) => {
|
||||
if (file instanceof TFile && oldName == this.fileSourcePath) {
|
||||
this.fileSourcePath = file.path;
|
||||
}
|
||||
})
|
||||
super.onload();
|
||||
|
||||
this.timesheet = new TimekeepView(
|
||||
this.containerEl,
|
||||
this.app,
|
||||
this.settings,
|
||||
this.customOutputFormats,
|
||||
this.autocomplete,
|
||||
this.loadResult,
|
||||
this.saveAdapter
|
||||
);
|
||||
|
||||
// Render the content
|
||||
if (this.loadResult.success) {
|
||||
const timekeep = this.loadResult.timekeep;
|
||||
// Bind save event handlers to update scroll
|
||||
this.timesheet.onBeforeSave = this.scrollTracker.save.bind(this.scrollTracker);
|
||||
this.timesheet.onAfterSave = this.scrollTracker.queueRestore.bind(this.scrollTracker);
|
||||
|
||||
const timekeepStore = createStore(timekeep);
|
||||
const saveErrorStore = createStore(false);
|
||||
|
||||
const trySave = this.trySave.bind(this);
|
||||
|
||||
const handleSaveTimekeep = async (timekeep: Timekeep) => {
|
||||
// Attempt to save the timekeep changes
|
||||
const result = await trySave(timekeep);
|
||||
|
||||
const saveError = !result;
|
||||
|
||||
// Update the save error state
|
||||
if (saveErrorStore.getState() !== saveError) {
|
||||
saveErrorStore.setState(saveError);
|
||||
}
|
||||
};
|
||||
|
||||
// Subscribe to save when timekeep changes
|
||||
timekeepStore.subscribe(() => {
|
||||
void handleSaveTimekeep(timekeepStore.getState());
|
||||
});
|
||||
|
||||
const timesheet = new Timesheet(
|
||||
this.containerEl,
|
||||
this.app,
|
||||
timekeepStore,
|
||||
saveErrorStore,
|
||||
this.settingsStore,
|
||||
this.customOutputFormats,
|
||||
this.autocomplete,
|
||||
handleSaveTimekeep
|
||||
);
|
||||
|
||||
this.addChild(timesheet);
|
||||
this.timesheet = timesheet;
|
||||
} else {
|
||||
const timesheet = new TimesheetLoadError(this.containerEl, this.loadResult.error);
|
||||
this.addChild(timesheet);
|
||||
this.timesheet = timesheet;
|
||||
}
|
||||
}
|
||||
|
||||
restoreScrollTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
restoreScrollInfo: { top: number; left: number } | null = null;
|
||||
|
||||
saveEditorScroll() {
|
||||
const activeEditor = this.app.workspace.activeEditor;
|
||||
if (!activeEditor) return;
|
||||
const editor = activeEditor.editor;
|
||||
if (!editor) return;
|
||||
this.restoreScrollInfo = editor.getScrollInfo();
|
||||
}
|
||||
|
||||
restoreEditorScroll() {
|
||||
if (this.restoreScrollInfo === null) return;
|
||||
const activeEditor = this.app.workspace.activeEditor;
|
||||
|
||||
if (!activeEditor) return;
|
||||
const editor = activeEditor.editor;
|
||||
|
||||
if (!editor) return;
|
||||
editor.scrollTo(this.restoreScrollInfo.left, this.restoreScrollInfo.top);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to save the file normally, if this fails it also attempts
|
||||
* to save a fallback file
|
||||
*
|
||||
* @param timekeep
|
||||
* @returns Promise of a boolean indicating weather the save was a success
|
||||
*/
|
||||
async trySave(timekeep: Timekeep): Promise<boolean> {
|
||||
this.saveEditorScroll();
|
||||
|
||||
try {
|
||||
await this.save(timekeep);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error("Failed to save timekeep", e);
|
||||
|
||||
try {
|
||||
await this.saveFallback(timekeep);
|
||||
} catch (e) {
|
||||
console.error("Couldn't save timekeep fallback", e);
|
||||
}
|
||||
|
||||
return false;
|
||||
} finally {
|
||||
// Queue scroll restoration after 50ms (Should be enough for the codeblock to re-render)
|
||||
this.restoreScrollTimeout = setTimeout(this.restoreEditorScroll.bind(this), 50);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to save the timekeep within the current file
|
||||
*
|
||||
* @param timekeep The new timekeep data to save
|
||||
*/
|
||||
async save(timekeep: Timekeep) {
|
||||
const sectionInfo = this.context.getSectionInfo(this.containerEl);
|
||||
|
||||
// Ensure we actually have a section to write to
|
||||
if (sectionInfo === null) throw new Error("Section to write did not exist");
|
||||
|
||||
const file = this.app.vault.getFileByPath(this.fileSourcePath);
|
||||
|
||||
// Ensure the file still exists
|
||||
if (file === null) throw new Error("File no longer exists");
|
||||
|
||||
// Replace the stored timekeep block with the new one
|
||||
await this.app.vault.process(file, (data) => {
|
||||
return replaceTimekeepCodeblock(
|
||||
timekeep,
|
||||
data,
|
||||
sectionInfo.lineStart,
|
||||
sectionInfo.lineEnd
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback saving in case writing back to the timekeep block fails,
|
||||
* if writing back fails attempt to write to a backup temporary file
|
||||
* using the current date time
|
||||
*
|
||||
* @param timekeep The timekeep to save
|
||||
*/
|
||||
async saveFallback(timekeep: Timekeep) {
|
||||
// Fallback in case of write failure, attempt to write to another file
|
||||
const backupFileName = `timekeep-write-backup-${moment().format("YYYY-MM-DD HH-mm-ss")}.json`;
|
||||
|
||||
// Write to the backup file
|
||||
await this.app.vault.create(
|
||||
backupFileName,
|
||||
JSON.stringify(stripTimekeepRuntimeData(timekeep))
|
||||
);
|
||||
this.addChild(this.timesheet);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
166
src/views/TimekeepView.ts
Normal file
166
src/views/TimekeepView.ts
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
import moment from "moment";
|
||||
import { App } from "obsidian";
|
||||
|
||||
import { CustomOutputFormat } from "@/output";
|
||||
import { TimesheetSaveAdapter } from "@/save/TimesheetSaveAdapter";
|
||||
import { TimekeepSettings } from "@/settings";
|
||||
import { createStore, Store } from "@/store";
|
||||
|
||||
import { ContentComponent } from "@/components/ContentComponent";
|
||||
import { EmptyComponent } from "@/components/EmptyComponent";
|
||||
import { Timesheet } from "@/components/Timesheet";
|
||||
import { TimesheetLoadError } from "@/components/TimesheetLoadError";
|
||||
import { TimesheetSaveError } from "@/components/TimesheetSaveError";
|
||||
|
||||
import { LoadResult } from "@/timekeep/parser";
|
||||
import { stripTimekeepRuntimeData, Timekeep } from "@/timekeep/schema";
|
||||
|
||||
import { TimekeepAutocomplete } from "@/service/autocomplete";
|
||||
|
||||
export default class TimekeepView extends ContentComponent<
|
||||
Timesheet | TimesheetLoadError | TimesheetSaveError | EmptyComponent
|
||||
> {
|
||||
/** Access to the obsidian app */
|
||||
app: App;
|
||||
/** Access to the timekeep settings */
|
||||
settings: Store<TimekeepSettings>;
|
||||
/** Access to custom output formats */
|
||||
customOutputFormats: Store<Record<string, CustomOutputFormat>>;
|
||||
/** Autocomplete */
|
||||
autocomplete: TimekeepAutocomplete;
|
||||
|
||||
/** Loading result for the timekeep data */
|
||||
loadResult: Store<LoadResult | null>;
|
||||
/** Store for the timekeep state */
|
||||
timekeep: Store<Timekeep>;
|
||||
/** Store for save error state */
|
||||
saveError: Store<boolean>;
|
||||
|
||||
/** Adapter for how the timesheet should be saved */
|
||||
saveAdapter: TimesheetSaveAdapter;
|
||||
|
||||
onBeforeSave: VoidFunction | undefined;
|
||||
onAfterSave: VoidFunction | undefined;
|
||||
|
||||
constructor(
|
||||
containerEl: HTMLElement,
|
||||
app: App,
|
||||
settings: Store<TimekeepSettings>,
|
||||
customOutputFormats: Store<Record<string, CustomOutputFormat>>,
|
||||
autocomplete: TimekeepAutocomplete,
|
||||
loadResult: Store<LoadResult | null>,
|
||||
saveAdapter: TimesheetSaveAdapter
|
||||
) {
|
||||
super(containerEl);
|
||||
|
||||
this.app = app;
|
||||
|
||||
this.loadResult = loadResult;
|
||||
this.timekeep = createStore({ entries: [] });
|
||||
this.saveError = createStore(false);
|
||||
|
||||
this.settings = settings;
|
||||
this.customOutputFormats = customOutputFormats;
|
||||
this.autocomplete = autocomplete;
|
||||
|
||||
this.saveAdapter = saveAdapter;
|
||||
}
|
||||
|
||||
onload(): void {
|
||||
super.onload();
|
||||
|
||||
this.saveAdapter.onLoad();
|
||||
|
||||
const onUpdateContent = this.onUpdateContent.bind(this);
|
||||
|
||||
this.register(this.loadResult.subscribe(onUpdateContent));
|
||||
this.register(this.saveError.subscribe(onUpdateContent));
|
||||
|
||||
onUpdateContent();
|
||||
}
|
||||
|
||||
onunload(): void {
|
||||
super.onunload();
|
||||
this.saveAdapter.onUnload();
|
||||
}
|
||||
|
||||
onUpdateContent() {
|
||||
const saveError = this.saveError.getState();
|
||||
const loadResult = this.loadResult.getState();
|
||||
|
||||
if (!loadResult) {
|
||||
return this.setContent(new EmptyComponent(this.containerEl));
|
||||
}
|
||||
|
||||
if (saveError) {
|
||||
this.setContent(new TimesheetSaveError(this.containerEl, this.timekeep));
|
||||
} else if (loadResult.success) {
|
||||
const timekeep = loadResult.timekeep;
|
||||
this.timekeep.setState(timekeep);
|
||||
|
||||
this.register(this.timekeep.subscribe(this.onSave.bind(this)));
|
||||
|
||||
this.setContent(
|
||||
new Timesheet(
|
||||
this.containerEl,
|
||||
this.app,
|
||||
this.timekeep,
|
||||
this.settings,
|
||||
this.customOutputFormats,
|
||||
this.autocomplete
|
||||
)
|
||||
);
|
||||
} else {
|
||||
this.setContent(new TimesheetLoadError(this.containerEl, loadResult.error));
|
||||
}
|
||||
}
|
||||
|
||||
async onSave() {
|
||||
if (this.onBeforeSave) this.onBeforeSave();
|
||||
|
||||
const timekeep = this.timekeep.getState();
|
||||
|
||||
try {
|
||||
await this.saveAdapter.onSave(timekeep);
|
||||
|
||||
// Clear error state on success
|
||||
if (this.saveError.getState()) {
|
||||
this.saveError.setState(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error("Failed to save timekeep", e);
|
||||
|
||||
try {
|
||||
await this.saveFallback(timekeep);
|
||||
} catch (e) {
|
||||
console.error("Couldn't save timekeep fallback", e);
|
||||
}
|
||||
|
||||
this.saveError.setState(true);
|
||||
} finally {
|
||||
if (this.onAfterSave) this.onAfterSave();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback handling if saving the timekeep through the regular
|
||||
* adapter fails:
|
||||
*
|
||||
* Write a backup to a temporary file using the current date and
|
||||
* time in the file name
|
||||
*
|
||||
* @param timekeep The timekeep to save
|
||||
*/
|
||||
async saveFallback(timekeep: Timekeep) {
|
||||
// Fallback in case of write failure, attempt to write to another file
|
||||
const backupFileName = `timekeep-write-backup-${moment().format("YYYY-MM-DD HH-mm-ss")}.json`;
|
||||
|
||||
// Write to the backup file
|
||||
await this.app.vault.create(
|
||||
backupFileName,
|
||||
JSON.stringify(stripTimekeepRuntimeData(timekeep))
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue