jacobtread_obsidian-timekeep/src/components/TimesheetRow/TimesheetRowContentEditing.ts

243 lines
6.3 KiB
TypeScript

import type { App } from "obsidian";
import { assert } from "vitest";
import type { TimekeepSettings } from "@/settings";
import type { Store } from "@/store";
import type { TimeEntry, Timekeep } from "@/timekeep/schema";
import { createObsidianIcon } from "@/components/obsidianIcon";
import { ReplaceableComponent } from "@/components/ReplaceableComponent";
import { ConfirmModal } from "@/modals/ConfirmModal";
import { removeEntry, updateEntry } from "@/timekeep/update";
import { formatEditableTimestamp, parseEditableTimestamp } from "@/utils/time";
/**
* Component for a timesheet row entry that is currently
* being edited
*/
export class TimesheetRowContentEditing extends ReplaceableComponent {
/** Access to the app instance */
app: App;
/** Access to the timekeep */
timekeep: Store<Timekeep>;
/** Access to the timekeep settings */
settings: Store<TimekeepSettings>;
/** The entry for this row */
entry: TimeEntry;
/** Label container for the start time */
#startTimeLabelEl: HTMLLabelElement | undefined;
/** Label container for the end time */
#endTimeLabelEl: HTMLLabelElement | undefined;
/** Input for the entry name */
#nameInputEl: HTMLInputElement | undefined;
/** Input for the start time */
#startTimeInputEl: HTMLInputElement | undefined;
/** Input for the end time */
#endTimeInputEl: HTMLInputElement | undefined;
/** Callback for editing finished / cancelled */
onFinishEditing: VoidFunction;
constructor(
containerEl: HTMLElement,
app: App,
timekeep: Store<Timekeep>,
settings: Store<TimekeepSettings>,
entry: TimeEntry,
onFinishEditing: VoidFunction
) {
super(containerEl);
this.app = app;
this.timekeep = timekeep;
this.settings = settings;
this.entry = entry;
this.onFinishEditing = onFinishEditing;
}
createContainer(): HTMLElement {
return createEl("tr", { cls: "timekeep-row" });
}
render(wrapperEl: HTMLElement): void {
const colEl = wrapperEl.createEl("td");
colEl.colSpan = 5;
const formEl = colEl.createEl("form", { cls: "timesheet-editing" });
this.registerDomEvent(formEl, "submit", this.onSubmit.bind(this));
const nameLabelEl = formEl.createEl("label", {
cls: "timekeep-input-label",
text: "Name",
});
const nameInputEl = nameLabelEl.createEl("input", {
cls: "timekeep-input",
type: "text",
});
nameInputEl.name = "name";
this.#nameInputEl = nameInputEl;
const startTimeLabelEl = formEl.createEl("label", {
text: "Start Time",
});
this.#startTimeLabelEl = startTimeLabelEl;
const startTimeInputEl = startTimeLabelEl.createEl("input", {
cls: "timekeep-input",
type: "text",
});
startTimeInputEl.name = "start-time";
this.#startTimeInputEl = startTimeInputEl;
const endTimeLabelEl = formEl.createEl("label", {
cls: "timekeep-input-label",
text: "End Time",
});
this.#endTimeLabelEl = endTimeLabelEl;
const endTimeInputEl = endTimeLabelEl.createEl("input", {
cls: "timekeep-input",
type: "text",
});
endTimeInputEl.name = "end-time";
this.#endTimeInputEl = endTimeInputEl;
const actionsEl = formEl.createDiv({
cls: "timesheet-editing-actions",
});
const saveButton = actionsEl.createEl("button", {
cls: "timekeep-action",
attr: {
"data-action": "save",
},
});
saveButton.type = "submit";
createObsidianIcon(saveButton, "edit", "text-button-icon");
saveButton.appendText("Save");
const cancelButton = actionsEl.createEl("button", {
cls: "timekeep-action",
attr: {
"data-action": "cancel",
},
});
cancelButton.type = "button";
createObsidianIcon(cancelButton, "x", "text-button-icon");
this.registerDomEvent(cancelButton, "click", this.onFinishEditing);
cancelButton.appendText("Cancel");
const deleteButton = actionsEl.createEl("button", {
cls: "timekeep-action",
attr: {
"data-action": "delete",
},
});
deleteButton.type = "button";
createObsidianIcon(deleteButton, "trash", "text-button-icon");
deleteButton.appendText("Delete");
this.registerDomEvent(deleteButton, "click", this.onConfirmDelete.bind(this));
const onUpdateState = this.onUpdateState.bind(this);
const unsubscribeSettings = this.settings.subscribe(onUpdateState);
this.register(unsubscribeSettings);
onUpdateState();
}
onUpdateState() {
assert(
this.#nameInputEl &&
this.#startTimeInputEl &&
this.#startTimeLabelEl &&
this.#endTimeInputEl &&
this.#endTimeLabelEl,
"Elements expected to be defined"
);
const settings = this.settings.getState();
const entry = this.entry;
this.#nameInputEl.value = entry.name;
this.#startTimeLabelEl.hidden = entry.startTime === null;
this.#startTimeInputEl.value = entry.startTime
? formatEditableTimestamp(entry.startTime, settings)
: "";
this.#endTimeLabelEl.hidden = entry.endTime === null;
this.#endTimeInputEl.value = entry.endTime
? formatEditableTimestamp(entry.endTime, settings)
: "";
}
onConfirmDelete() {
const modal = new ConfirmModal(
this.app,
"Are you sure you want to delete this entry?",
this.onConfirmedDelete.bind(this)
);
modal.setTitle("Confirm Delete");
modal.open();
}
onConfirmedDelete(confirmed: boolean) {
if (!confirmed) {
return;
}
const entry = this.entry;
this.timekeep.setState((timekeep) => ({
entries: removeEntry(timekeep.entries, entry),
}));
}
onSubmit(event: Event) {
assert(
this.#nameInputEl && this.#startTimeInputEl && this.#endTimeInputEl,
"Expected inputs to be defined"
);
event.preventDefault();
event.stopPropagation();
const name = this.#nameInputEl.value;
const startTime = this.#startTimeInputEl.value;
const endTime = this.#endTimeInputEl.value;
const settings = this.settings.getState();
const entry = this.entry;
const newEntry = { ...entry, name };
// Update the start and end times for non groups
if (newEntry.subEntries === null) {
if (entry.startTime !== null) {
const startTimeValue = parseEditableTimestamp(startTime, settings);
if (startTimeValue.isValid()) {
newEntry.startTime = startTimeValue;
}
}
if (entry.endTime !== null) {
const endTimeValue = parseEditableTimestamp(endTime, settings);
if (endTimeValue.isValid()) {
newEntry.endTime = endTimeValue;
}
}
}
// Save the updated entry
this.timekeep.setState((timekeep) => ({
entries: updateEntry(timekeep.entries, entry.id, newEntry),
}));
this.onFinishEditing();
}
}