mirror of
https://github.com/jacobtread/obsidian-timekeep.git
synced 2026-07-22 10:10:27 +00:00
Merge pull request #13 from jacobtread/feat-copy-duration-formats
feat: copy duration formats settings
This commit is contained in:
commit
b0e052d9a4
10 changed files with 157 additions and 32 deletions
|
|
@ -4,7 +4,7 @@ import { Timekeep } from "@/schema";
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { useSettings } from "@/contexts/use-settings-context";
|
||||
import { useTimekeepStore } from "@/contexts/use-timekeep-store";
|
||||
import { formatDuration, formatDurationHoursTrunc } from "@/utils";
|
||||
import { formatDurationLong, formatDurationShort } from "@/utils";
|
||||
import {
|
||||
isKeepRunning,
|
||||
getRunningEntry,
|
||||
|
|
@ -36,10 +36,10 @@ function getTimingState(timekeep: Timekeep): TimingState {
|
|||
|
||||
return {
|
||||
running: runningEntry !== null,
|
||||
current: formatDuration(current),
|
||||
currentShort: formatDurationHoursTrunc(current),
|
||||
total: formatDuration(total),
|
||||
totalShort: formatDurationHoursTrunc(total),
|
||||
current: formatDurationLong(current),
|
||||
currentShort: formatDurationShort(current),
|
||||
total: formatDurationLong(total),
|
||||
totalShort: formatDurationShort(total),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import moment from "moment";
|
||||
import { TimeEntry } from "@/schema";
|
||||
import { formatDuration } from "@/utils";
|
||||
import { formatDurationLong } from "@/utils";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { isEntryRunning, getEntryDuration } from "@/timekeep";
|
||||
|
||||
|
|
@ -46,5 +46,5 @@ export default function TimesheetRowDuration({ entry }: Props) {
|
|||
function getFormattedDuration(entry: TimeEntry): string {
|
||||
const currentTime = moment();
|
||||
const duration = getEntryDuration(entry, currentTime);
|
||||
return formatDuration(duration);
|
||||
return formatDurationLong(duration);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import { TimekeepSettings } from "@/settings";
|
|||
import { Page, View, Text, Document, StyleSheet } from "@/pdf";
|
||||
import {
|
||||
formatPdfDate,
|
||||
formatDuration,
|
||||
formatDurationHoursTrunc,
|
||||
formatDurationLong,
|
||||
formatDurationShort,
|
||||
} from "@/utils";
|
||||
|
||||
import TimesheetPdfTable from "./TimesheetPdfTable";
|
||||
|
|
@ -83,8 +83,8 @@ export default function TimesheetPdf({
|
|||
const duration = getTotalDuration(data.entries, currentTime);
|
||||
|
||||
const currentDate = formatPdfDate(currentTime, settings);
|
||||
const totalDuration = formatDuration(duration);
|
||||
const totalDurationShort = formatDurationHoursTrunc(duration);
|
||||
const totalDuration = formatDurationLong(duration);
|
||||
const totalDurationShort = formatDurationShort(duration);
|
||||
|
||||
// Individual field within the details section
|
||||
const DetailField = ({ name, value }: { name: string; value: string }) => (
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { getEntryDuration } from "@/timekeep";
|
|||
import { TimekeepSettings } from "@/settings";
|
||||
import { Timekeep, TimeEntry } from "@/schema";
|
||||
import { View, Text, StyleSheet } from "@/pdf";
|
||||
import { formatDuration, formatPdfRowDate } from "@/utils";
|
||||
import { formatPdfRowDate, formatDurationLong } from "@/utils";
|
||||
|
||||
type Props = {
|
||||
data: Timekeep;
|
||||
|
|
@ -142,7 +142,7 @@ type RowProps = {
|
|||
|
||||
function TimesheetPdfTableRow({ entry, currentTime, settings }: RowProps) {
|
||||
const duration = getEntryDuration(entry, currentTime);
|
||||
const durationFormatted = formatDuration(duration);
|
||||
const durationFormatted = formatDurationLong(duration);
|
||||
|
||||
// Render start and end timing for individual entries
|
||||
const renderTiming = entry.startTime !== null && (
|
||||
|
|
|
|||
|
|
@ -53,7 +53,10 @@ export function createRawTableEntries(
|
|||
// Include duration for entries that are finished
|
||||
(entry.startTime !== null && entry.endTime !== null) ||
|
||||
entry.subEntries !== null
|
||||
? formatDuration(getEntryDuration(entry, currentTime))
|
||||
? formatDuration(
|
||||
settings.exportDurationFormat,
|
||||
getEntryDuration(entry, currentTime)
|
||||
)
|
||||
: "",
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import type { Moment } from "moment";
|
||||
import { formatDuration } from "@/utils";
|
||||
import { getTotalDuration } from "@/timekeep";
|
||||
import { TimekeepSettings } from "@/settings";
|
||||
import { Timekeep, TimeEntry } from "@/schema";
|
||||
import { DurationFormat, TimekeepSettings } from "@/settings";
|
||||
import { RawTableRow, TOTAL_COLUMNS, createRawTable } from "@/export";
|
||||
|
||||
/**
|
||||
|
|
@ -22,8 +22,13 @@ function createHeader(): RawTableRow {
|
|||
* @param currentTime The current time to use for unfinished entries
|
||||
* @returns The created row
|
||||
*/
|
||||
function createFooter(entries: TimeEntry[], currentTime: Moment): RawTableRow {
|
||||
function createFooter(
|
||||
entries: TimeEntry[],
|
||||
currentTime: Moment,
|
||||
durationFormat: DurationFormat
|
||||
): RawTableRow {
|
||||
const total: string = formatDuration(
|
||||
durationFormat,
|
||||
getTotalDuration(entries, currentTime)
|
||||
);
|
||||
return ["**Total**", "", "", `**${total}**`];
|
||||
|
|
@ -68,7 +73,11 @@ export function createMarkdownTable(
|
|||
// Markdown raw table contents
|
||||
...createRawTable(timekeep.entries, settings, currentTime),
|
||||
// Markdown footer row
|
||||
createFooter(timekeep.entries, currentTime),
|
||||
createFooter(
|
||||
timekeep.entries,
|
||||
currentTime,
|
||||
settings.exportDurationFormat
|
||||
),
|
||||
];
|
||||
|
||||
// Array of indexes for all the columns (0 - TOTAL_COLUMNS)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { Store } from "@/store";
|
|||
import TimekeepPlugin from "@/main";
|
||||
import { App, Setting, PluginSettingTab } from "obsidian";
|
||||
import {
|
||||
DurationFormat,
|
||||
defaultSettings,
|
||||
TimekeepSettings,
|
||||
PdfExportBehavior,
|
||||
|
|
@ -244,5 +245,27 @@ export class TimekeepSettingsTab extends PluginSettingTab {
|
|||
}));
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(this.containerEl)
|
||||
.setName("CSV/Markdown duration format")
|
||||
.setDesc("Format to show durations as when copying as CSV/Markdown")
|
||||
|
||||
.addDropdown((t) => {
|
||||
t.addOptions({
|
||||
[DurationFormat.LONG]:
|
||||
"Long - Format including all units (1h 30m 25s)",
|
||||
[DurationFormat.SHORT]:
|
||||
"Short - Format just including hours (1.5h)",
|
||||
[DurationFormat.DECIMAL]:
|
||||
"Decimal - Short format without units (1.5)",
|
||||
});
|
||||
t.setValue(String(settings.exportDurationFormat));
|
||||
t.onChange((v) => {
|
||||
this.settingsStore.setState((currentValue) => ({
|
||||
...currentValue,
|
||||
exportDurationFormat: v as DurationFormat,
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,15 @@ export enum PdfExportBehavior {
|
|||
OPEN_FILE = "OPEN_FILE",
|
||||
}
|
||||
|
||||
export enum DurationFormat {
|
||||
// Format including all units (1h 30m 25s)
|
||||
LONG = "LONG",
|
||||
// Format just including hours (1.5h)
|
||||
SHORT = "SHORT",
|
||||
// Short format without units (1.5)
|
||||
DECIMAL = "DECIMAL",
|
||||
}
|
||||
|
||||
export interface TimekeepSettings {
|
||||
csvDelimiter: string;
|
||||
csvTitle: boolean;
|
||||
|
|
@ -20,6 +29,7 @@ export interface TimekeepSettings {
|
|||
reverseSegmentOrder: boolean;
|
||||
timestampFormat: string;
|
||||
showDecimalHours: boolean;
|
||||
exportDurationFormat: DurationFormat;
|
||||
}
|
||||
|
||||
export const defaultSettings: TimekeepSettings = {
|
||||
|
|
@ -36,4 +46,5 @@ export const defaultSettings: TimekeepSettings = {
|
|||
reverseSegmentOrder: false,
|
||||
limitTableSize: true,
|
||||
showDecimalHours: true,
|
||||
exportDurationFormat: DurationFormat.SHORT,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
import moment from "moment";
|
||||
import { defaultSettings, TimekeepSettings } from "@/settings";
|
||||
import { DurationFormat, defaultSettings, TimekeepSettings } from "@/settings";
|
||||
|
||||
import {
|
||||
formatPdfDate,
|
||||
formatDuration,
|
||||
formatTimestamp,
|
||||
formatPdfRowDate,
|
||||
formatDurationLong,
|
||||
formatDurationShort,
|
||||
formatDurationDecimal,
|
||||
parseEditableTimestamp,
|
||||
formatEditableTimestamp,
|
||||
formatDurationHoursTrunc,
|
||||
} from "./time";
|
||||
|
||||
it("should format time", () => {
|
||||
|
|
@ -59,7 +61,7 @@ describe("format duration", () => {
|
|||
])(
|
||||
'for duration "%s" should expected formatted "%s"',
|
||||
(input, expected) => {
|
||||
const output = formatDuration(input);
|
||||
const output = formatDurationLong(input);
|
||||
|
||||
expect(output).toBe(expected);
|
||||
}
|
||||
|
|
@ -75,7 +77,52 @@ describe("format duration short", () => {
|
|||
])(
|
||||
'for duration "%s" should expected formatted "%s"',
|
||||
(input, expected) => {
|
||||
const output = formatDurationHoursTrunc(input);
|
||||
const output = formatDurationShort(input);
|
||||
|
||||
expect(output).toBe(expected);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe("format duration decimal", () => {
|
||||
test.each([
|
||||
[1000 * 60 * 60 * 2, "2.00"],
|
||||
[1000 * 60 * 60 * 25.25, "25.25"],
|
||||
[1000 * 60 * 60 * 25.255, "25.25"],
|
||||
[1000 * 60 * 60 * 50.5, "50.50"],
|
||||
])(
|
||||
'for duration "%s" should expected formatted "%s"',
|
||||
(input, expected) => {
|
||||
const output = formatDurationDecimal(input);
|
||||
|
||||
expect(output).toBe(expected);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe("format duration with format", () => {
|
||||
test.each([
|
||||
[DurationFormat.LONG, 1000, "1s"],
|
||||
[DurationFormat.LONG, 1000 * 12, "12s"],
|
||||
[DurationFormat.LONG, 1000 * 60, "1m 0s"],
|
||||
[DurationFormat.LONG, 1000 * 60 * 2, "2m 0s"],
|
||||
[DurationFormat.LONG, 1000 * 60 * 60 * 2, "2h 0s"],
|
||||
[DurationFormat.LONG, 1000 * 60 * 60 * 2.5, "2h 30m 0s"],
|
||||
[DurationFormat.LONG, 1000 * 60 * 60 * 2.505, "2h 30m 18s"],
|
||||
|
||||
[DurationFormat.SHORT, 1000 * 60 * 60 * 2, "2.00h"],
|
||||
[DurationFormat.SHORT, 1000 * 60 * 60 * 25.25, "25.25h"],
|
||||
[DurationFormat.SHORT, 1000 * 60 * 60 * 25.255, "25.25h"],
|
||||
[DurationFormat.SHORT, 1000 * 60 * 60 * 50.5, "50.50h"],
|
||||
|
||||
[DurationFormat.DECIMAL, 1000 * 60 * 60 * 2, "2.00"],
|
||||
[DurationFormat.DECIMAL, 1000 * 60 * 60 * 25.25, "25.25"],
|
||||
[DurationFormat.DECIMAL, 1000 * 60 * 60 * 25.255, "25.25"],
|
||||
[DurationFormat.DECIMAL, 1000 * 60 * 60 * 50.5, "50.50"],
|
||||
])(
|
||||
'for duration "%s" should expected formatted "%s"',
|
||||
(format, input, expected) => {
|
||||
const output = formatDuration(format, input);
|
||||
|
||||
expect(output).toBe(expected);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import moment, { Moment } from "moment";
|
||||
import { TimekeepSettings } from "@/settings";
|
||||
import { DurationFormat, TimekeepSettings } from "@/settings";
|
||||
|
||||
/**
|
||||
* Formats a timestamp for tables and generated output
|
||||
|
|
@ -44,16 +44,37 @@ export function parseEditableTimestamp(
|
|||
return moment(formatted, settings.editableTimestampFormat, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a duration using the provided duration format
|
||||
*
|
||||
* @param format The format to use
|
||||
* @param durationMS The duration to format
|
||||
* @returns The formatted duration
|
||||
*/
|
||||
export function formatDuration(
|
||||
format: DurationFormat,
|
||||
durationMS: number
|
||||
): string {
|
||||
switch (format) {
|
||||
case DurationFormat.LONG:
|
||||
return formatDurationLong(durationMS);
|
||||
case DurationFormat.SHORT:
|
||||
return formatDurationShort(durationMS);
|
||||
case DurationFormat.DECIMAL:
|
||||
return formatDurationDecimal(durationMS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the provided duration in the form
|
||||
* of hours, minutes, and seconds
|
||||
*
|
||||
* @param totalTime The duration to format
|
||||
* @param durationMS The duration to format in milliseconds
|
||||
* @returns The formatted duration
|
||||
*/
|
||||
export function formatDuration(totalTime: number): string {
|
||||
export function formatDurationLong(durationMS: number): string {
|
||||
let ret = "";
|
||||
const duration = moment.duration(totalTime);
|
||||
const duration = moment.duration(durationMS);
|
||||
const hours = Math.floor(duration.asHours());
|
||||
|
||||
if (hours > 0) ret += hours + "h ";
|
||||
|
|
@ -64,19 +85,30 @@ export function formatDuration(totalTime: number): string {
|
|||
}
|
||||
|
||||
/**
|
||||
* Formats a duration in the form of hours only
|
||||
* minutes will be counted a portions of an hour
|
||||
* (i.e 1h 30m will be 1.5h)
|
||||
* Same as {@see formatDurationDecimal} but with a "h" suffix
|
||||
* indicating its hours
|
||||
*
|
||||
* @param totalTime The duration to format
|
||||
* @param durationMS The duration to format in milliseconds
|
||||
* @returns The formatted duration
|
||||
*/
|
||||
export function formatDurationHoursTrunc(totalTime: number): string {
|
||||
const duration = moment.duration(totalTime);
|
||||
export function formatDurationShort(durationMS: number): string {
|
||||
return formatDurationDecimal(durationMS) + "h";
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a duration in the form of hours only
|
||||
* minutes will be counted a portions of an hour
|
||||
* (i.e 1h 30m will be 1.5)
|
||||
*
|
||||
* @param durationMS The duration to format
|
||||
* @returns The formatted duration
|
||||
*/
|
||||
export function formatDurationDecimal(durationMS: number): string {
|
||||
const duration = moment.duration(durationMS);
|
||||
|
||||
const hours = duration.asHours();
|
||||
|
||||
return hours.toFixed(2) + "h";
|
||||
return hours.toFixed(2);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue