mirror of
https://github.com/jacobtread/obsidian-timekeep.git
synced 2026-07-22 10:10:27 +00:00
feat: sorting, options for sort order and where to put unstarted entries, replace reverse segment order option
This commit is contained in:
parent
eef3dce6be
commit
e3bd533cbc
8 changed files with 601 additions and 33 deletions
|
|
@ -6,6 +6,7 @@
|
|||
"Roboto",
|
||||
"stylesheet",
|
||||
"timekeep",
|
||||
"timesheet"
|
||||
"timesheet",
|
||||
"unstarted"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { TimeEntry } from "@/schema";
|
|||
import React, { useMemo, Fragment } from "react";
|
||||
import TimesheetRow from "@/components/TimesheetRow";
|
||||
import { useSettings } from "@/contexts/use-settings-context";
|
||||
import { getEntriesOrdered, getUniqueEntryHash } from "@/timekeep";
|
||||
import { getEntriesSorted, getUniqueEntryHash } from "@/timekeep";
|
||||
|
||||
type Props = {
|
||||
// Collection of entries
|
||||
|
|
@ -25,7 +25,7 @@ export default function TimesheetRows({
|
|||
const settings = useSettings();
|
||||
// Memoized sub entries
|
||||
const entriesOrdered = useMemo(
|
||||
() => getEntriesOrdered(entries, settings),
|
||||
() => getEntriesSorted(entries, settings),
|
||||
[entries, settings]
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { TimeEntry } from "@/schema";
|
|||
import type { Moment } from "moment";
|
||||
import { TimekeepSettings } from "@/settings";
|
||||
import { formatDuration, formatTimestamp } from "@/utils";
|
||||
import { getEntryDuration, getEntriesOrdered } from "@/timekeep";
|
||||
import { getEntryDuration, getEntriesSorted } from "@/timekeep";
|
||||
|
||||
export { createCSV } from "./csv";
|
||||
export { createMarkdownTable } from "./markdown-table";
|
||||
|
|
@ -62,7 +62,7 @@ export function createRawTableEntries(
|
|||
];
|
||||
|
||||
if (entry.subEntries) {
|
||||
const entries = getEntriesOrdered(entry.subEntries, settings);
|
||||
const entries = getEntriesSorted(entry.subEntries, settings);
|
||||
|
||||
for (const entry of entries) {
|
||||
rows.push(...createRawTableEntries(entry, settings, currentTime));
|
||||
|
|
|
|||
18
src/main.ts
18
src/main.ts
|
|
@ -2,8 +2,8 @@ import { Moment } from "moment";
|
|||
import { Store, createStore } from "@/store";
|
||||
import { TimekeepSettingsTab } from "@/settings-tab";
|
||||
import { PluginManifest, App as ObsidianApp } from "obsidian";
|
||||
import { defaultSettings, TimekeepSettings } from "@/settings";
|
||||
import { Plugin, MarkdownPostProcessorContext } from "obsidian";
|
||||
import { SortOrder, defaultSettings, TimekeepSettings } from "@/settings";
|
||||
import {
|
||||
load,
|
||||
isKeepRunning,
|
||||
|
|
@ -51,11 +51,21 @@ export default class TimekeepPlugin extends Plugin {
|
|||
}
|
||||
|
||||
async onload(): Promise<void> {
|
||||
// Load saved settings and combine with defaults
|
||||
this.settingsStore.setState(
|
||||
Object.assign({}, defaultSettings, await this.loadData())
|
||||
const loadedSettings: TimekeepSettings = Object.assign(
|
||||
{},
|
||||
defaultSettings,
|
||||
await this.loadData()
|
||||
);
|
||||
|
||||
// Compatibility with old reverse segment order
|
||||
if (loadedSettings.reverseSegmentOrder) {
|
||||
delete loadedSettings.reverseSegmentOrder;
|
||||
loadedSettings.sortOrder = SortOrder.REVERSE_INSERTION;
|
||||
}
|
||||
|
||||
// Load saved settings and combine with defaults
|
||||
this.settingsStore.setState(loadedSettings);
|
||||
|
||||
this.addSettingTab(new TimekeepSettingsTab(this.app, this));
|
||||
|
||||
this.registerMarkdownCodeBlockProcessor(
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ import { Store } from "@/store";
|
|||
import TimekeepPlugin from "@/main";
|
||||
import { App, Setting, PluginSettingTab } from "obsidian";
|
||||
import {
|
||||
SortOrder,
|
||||
DurationFormat,
|
||||
UnstartedOrder,
|
||||
defaultSettings,
|
||||
TimekeepSettings,
|
||||
PdfExportBehavior,
|
||||
|
|
@ -217,16 +219,49 @@ export class TimekeepSettingsTab extends PluginSettingTab {
|
|||
});
|
||||
|
||||
new Setting(this.containerEl)
|
||||
.setName("Display segments in reverse order")
|
||||
.setName("Sort order")
|
||||
.setDesc(
|
||||
"Whether older tracker segments should be displayed towards the bottom of the tracker, rather than the top."
|
||||
"How entries should be sorted both when viewing and exporting"
|
||||
)
|
||||
.addToggle((t) => {
|
||||
t.setValue(settings.reverseSegmentOrder);
|
||||
|
||||
.addDropdown((t) => {
|
||||
t.addOptions({
|
||||
[SortOrder.INSERTION]:
|
||||
"Insertion - Don't sort, leave entries in the order they were created",
|
||||
[SortOrder.REVERSE_INSERTION]:
|
||||
"Reverse Insertion - Opposite order to how entries were inserted",
|
||||
[SortOrder.NEWEST_START]:
|
||||
"Newest First - Sort most recently started entries to the start",
|
||||
[SortOrder.OLDEST_START]:
|
||||
"Newest Last - Sort most recently started entries to the end",
|
||||
});
|
||||
t.setValue(String(settings.sortOrder));
|
||||
t.onChange((v) => {
|
||||
this.settingsStore.setState((currentValue) => ({
|
||||
...currentValue,
|
||||
reverseSegmentOrder: v,
|
||||
sortOrder: v as SortOrder,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(this.containerEl)
|
||||
.setName("Unstarted Sort order")
|
||||
.setDesc(
|
||||
"Where in the order should unstarted entries be put (Only applied when using 'Newest First' or 'Newest Last' sort order)"
|
||||
)
|
||||
|
||||
.addDropdown((t) => {
|
||||
t.addOptions({
|
||||
[UnstartedOrder.FIRST]:
|
||||
"First - Put non started entries at the top of the list",
|
||||
[UnstartedOrder.LAST]:
|
||||
"Last - Put non started entries at the bottom of the list",
|
||||
});
|
||||
t.setValue(String(settings.unstartedOrder));
|
||||
t.onChange((v) => {
|
||||
this.settingsStore.setState((currentValue) => ({
|
||||
...currentValue,
|
||||
unstartedOrder: v as UnstartedOrder,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,6 +16,27 @@ export enum DurationFormat {
|
|||
DECIMAL = "DECIMAL",
|
||||
}
|
||||
|
||||
export enum SortOrder {
|
||||
// Don't sort the the list, maintain insertion order
|
||||
INSERTION = "INSERTION",
|
||||
|
||||
// Flip the insertion order
|
||||
REVERSE_INSERTION = "REVERSE_INSERTION",
|
||||
|
||||
// Sort by the most recently started
|
||||
NEWEST_START = "NEWEST_START",
|
||||
|
||||
// Sort by the oldest started entry
|
||||
OLDEST_START = "OLDEST_START",
|
||||
}
|
||||
|
||||
export enum UnstartedOrder {
|
||||
// Sort unstarted items to the start
|
||||
FIRST = "FIRST",
|
||||
// Sort unstarted items to the end
|
||||
LAST = "LAST",
|
||||
}
|
||||
|
||||
export interface TimekeepSettings {
|
||||
csvDelimiter: string;
|
||||
csvTitle: boolean;
|
||||
|
|
@ -26,11 +47,14 @@ export interface TimekeepSettings {
|
|||
pdfExportBehavior: PdfExportBehavior;
|
||||
pdfDateFormat: string;
|
||||
pdfRowDateFormat: string;
|
||||
reverseSegmentOrder: boolean;
|
||||
reverseSegmentOrder?: boolean;
|
||||
timestampFormat: string;
|
||||
showDecimalHours: boolean;
|
||||
exportDurationFormat: DurationFormat;
|
||||
formatCopiedJSON: boolean;
|
||||
|
||||
sortOrder: SortOrder;
|
||||
unstartedOrder: UnstartedOrder;
|
||||
}
|
||||
|
||||
export const defaultSettings: TimekeepSettings = {
|
||||
|
|
@ -44,9 +68,11 @@ export const defaultSettings: TimekeepSettings = {
|
|||
editableTimestampFormat: "YYYY-MM-DD HH:mm:ss",
|
||||
csvTitle: true,
|
||||
csvDelimiter: ",",
|
||||
reverseSegmentOrder: false,
|
||||
limitTableSize: true,
|
||||
showDecimalHours: true,
|
||||
exportDurationFormat: DurationFormat.SHORT,
|
||||
formatCopiedJSON: false,
|
||||
|
||||
sortOrder: SortOrder.INSERTION,
|
||||
unstartedOrder: UnstartedOrder.LAST,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
import moment from "moment";
|
||||
|
||||
import { Timekeep, TimeEntry, TimeEntryGroup } from "./schema";
|
||||
import { defaultSettings, TimekeepSettings } from "./settings";
|
||||
import {
|
||||
SortOrder,
|
||||
UnstartedOrder,
|
||||
defaultSettings,
|
||||
TimekeepSettings,
|
||||
} from "./settings";
|
||||
import {
|
||||
load,
|
||||
LoadError,
|
||||
|
|
@ -16,7 +21,7 @@ import {
|
|||
getRunningEntry,
|
||||
getEntryDuration,
|
||||
getTotalDuration,
|
||||
getEntriesOrdered,
|
||||
getEntriesSorted,
|
||||
setEntryCollapsed,
|
||||
getUniqueEntryHash,
|
||||
stopRunningEntries,
|
||||
|
|
@ -1149,9 +1154,47 @@ describe("ordering entries", () => {
|
|||
endTime: currentTime,
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Part 3",
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
subEntries: [
|
||||
{
|
||||
name: "Part 3 1",
|
||||
startTime: currentTime,
|
||||
endTime: currentTime,
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Part 3 2",
|
||||
startTime: currentTime,
|
||||
endTime: currentTime,
|
||||
subEntries: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const expected = [
|
||||
{
|
||||
name: "Part 3",
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
subEntries: [
|
||||
{
|
||||
name: "Part 3 2",
|
||||
startTime: currentTime,
|
||||
endTime: currentTime,
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Part 3 1",
|
||||
startTime: currentTime,
|
||||
endTime: currentTime,
|
||||
subEntries: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Part 2",
|
||||
startTime: currentTime,
|
||||
|
|
@ -1167,9 +1210,309 @@ describe("ordering entries", () => {
|
|||
];
|
||||
|
||||
const settings: TimekeepSettings = defaultSettings;
|
||||
settings.reverseSegmentOrder = true;
|
||||
settings.sortOrder = SortOrder.REVERSE_INSERTION;
|
||||
|
||||
const output = getEntriesOrdered(input, settings);
|
||||
const output = getEntriesSorted(input, settings);
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
it("should be in newest first order", () => {
|
||||
const currentTime = moment();
|
||||
|
||||
const futureStartTime = currentTime.clone().add(5000, "ms");
|
||||
|
||||
const input = [
|
||||
{
|
||||
name: "Part 1",
|
||||
startTime: currentTime,
|
||||
endTime: currentTime,
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Part 1 null",
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Part 2 null",
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Part 2",
|
||||
startTime: futureStartTime,
|
||||
endTime: futureStartTime,
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Part 3",
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
subEntries: [
|
||||
{
|
||||
name: "Part 3 1",
|
||||
startTime: currentTime,
|
||||
endTime: currentTime,
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Part 3 2",
|
||||
startTime: futureStartTime,
|
||||
endTime: futureStartTime,
|
||||
subEntries: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const expected = [
|
||||
{
|
||||
name: "Part 2",
|
||||
startTime: futureStartTime,
|
||||
endTime: futureStartTime,
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Part 1",
|
||||
startTime: currentTime,
|
||||
endTime: currentTime,
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Part 3",
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
subEntries: [
|
||||
{
|
||||
name: "Part 3 2",
|
||||
startTime: futureStartTime,
|
||||
endTime: futureStartTime,
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Part 3 1",
|
||||
startTime: currentTime,
|
||||
endTime: currentTime,
|
||||
subEntries: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Part 1 null",
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Part 2 null",
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
subEntries: null,
|
||||
},
|
||||
];
|
||||
|
||||
const settings: TimekeepSettings = defaultSettings;
|
||||
settings.sortOrder = SortOrder.NEWEST_START;
|
||||
|
||||
const output = getEntriesSorted(input, settings);
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
it("should be in newest last order", () => {
|
||||
const currentTime = moment();
|
||||
|
||||
const futureStartTime = currentTime.clone().add(5000, "ms");
|
||||
|
||||
const input = [
|
||||
{
|
||||
name: "Part 1",
|
||||
startTime: currentTime,
|
||||
endTime: currentTime,
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Part 2",
|
||||
startTime: futureStartTime,
|
||||
endTime: futureStartTime,
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Part 1 null",
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Part 3",
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
subEntries: [
|
||||
{
|
||||
name: "Part 3 1",
|
||||
startTime: currentTime,
|
||||
endTime: currentTime,
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Part 3 2",
|
||||
startTime: currentTime,
|
||||
endTime: currentTime,
|
||||
subEntries: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const expected = [
|
||||
{
|
||||
name: "Part 1",
|
||||
startTime: currentTime,
|
||||
endTime: currentTime,
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Part 3",
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
subEntries: [
|
||||
{
|
||||
name: "Part 3 1",
|
||||
startTime: currentTime,
|
||||
endTime: currentTime,
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Part 3 2",
|
||||
startTime: currentTime,
|
||||
endTime: currentTime,
|
||||
subEntries: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Part 2",
|
||||
startTime: futureStartTime,
|
||||
endTime: futureStartTime,
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Part 1 null",
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
subEntries: null,
|
||||
},
|
||||
];
|
||||
|
||||
const settings: TimekeepSettings = defaultSettings;
|
||||
settings.sortOrder = SortOrder.OLDEST_START;
|
||||
|
||||
const output = getEntriesSorted(input, settings);
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
it("should be in newest last order with nulls first", () => {
|
||||
const currentTime = moment();
|
||||
|
||||
const futureStartTime = currentTime.clone().add(5000, "ms");
|
||||
|
||||
const input = [
|
||||
{
|
||||
name: "Part 2 null",
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Part 1",
|
||||
startTime: currentTime,
|
||||
endTime: currentTime,
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Part 2",
|
||||
startTime: futureStartTime,
|
||||
endTime: futureStartTime,
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Part 1 null",
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Part 3",
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
subEntries: [
|
||||
{
|
||||
name: "Part 3 1",
|
||||
startTime: currentTime,
|
||||
endTime: currentTime,
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Part 3 2",
|
||||
startTime: currentTime,
|
||||
endTime: currentTime,
|
||||
subEntries: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const expected = [
|
||||
{
|
||||
name: "Part 2 null",
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Part 1 null",
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Part 1",
|
||||
startTime: currentTime,
|
||||
endTime: currentTime,
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Part 3",
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
subEntries: [
|
||||
{
|
||||
name: "Part 3 1",
|
||||
startTime: currentTime,
|
||||
endTime: currentTime,
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
name: "Part 3 2",
|
||||
startTime: currentTime,
|
||||
endTime: currentTime,
|
||||
subEntries: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Part 2",
|
||||
startTime: futureStartTime,
|
||||
endTime: futureStartTime,
|
||||
subEntries: null,
|
||||
},
|
||||
];
|
||||
|
||||
const settings: TimekeepSettings = defaultSettings;
|
||||
settings.sortOrder = SortOrder.OLDEST_START;
|
||||
settings.unstartedOrder = UnstartedOrder.FIRST;
|
||||
|
||||
const output = getEntriesSorted(input, settings);
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
|
|
@ -1207,9 +1550,9 @@ describe("ordering entries", () => {
|
|||
];
|
||||
|
||||
const settings: TimekeepSettings = defaultSettings;
|
||||
settings.reverseSegmentOrder = false;
|
||||
settings.sortOrder = SortOrder.INSERTION;
|
||||
|
||||
const output = getEntriesOrdered(input, settings);
|
||||
const output = getEntriesSorted(input, settings);
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
175
src/timekeep.ts
175
src/timekeep.ts
|
|
@ -1,7 +1,7 @@
|
|||
import type { Moment } from "moment";
|
||||
import { strHash } from "@/utils/text";
|
||||
import { isEmptyString } from "@/utils";
|
||||
import { TimekeepSettings } from "@/settings";
|
||||
import { SortOrder, UnstartedOrder, TimekeepSettings } from "@/settings";
|
||||
import { TIMEKEEP, Timekeep, TimeEntry, TimeEntryGroup } from "@/schema";
|
||||
|
||||
export type LoadResult = LoadSuccess | LoadError;
|
||||
|
|
@ -475,23 +475,176 @@ export function getTotalDuration(
|
|||
}
|
||||
|
||||
/**
|
||||
* Provides a new list of time entires re-ordered to match
|
||||
* the current timekeep settings ordering
|
||||
* Provides a sorted copy of the provided entries list.
|
||||
*
|
||||
* @param entries The collection of entries
|
||||
* @param settings The timekeep settings
|
||||
* @returns The entries in the timekeep order
|
||||
* Recursively sorts the groups and sorts groups based
|
||||
* on the time entries within the group
|
||||
*
|
||||
* @param entries The list of entries
|
||||
* @param settings The timekeep settings for which order to use
|
||||
* @returns The sorted entries list
|
||||
*/
|
||||
export function getEntriesOrdered(
|
||||
export function getEntriesSorted(
|
||||
entries: TimeEntry[],
|
||||
settings: TimekeepSettings
|
||||
): TimeEntry[] {
|
||||
// Reverse ordered entries
|
||||
if (settings.reverseSegmentOrder) {
|
||||
return entries.slice().reverse();
|
||||
// List order should be unchanged
|
||||
if (settings.sortOrder === SortOrder.INSERTION) {
|
||||
return entries;
|
||||
}
|
||||
|
||||
return entries;
|
||||
// Reverse insertion is just .reverse on all the arrays
|
||||
if (settings.sortOrder === SortOrder.REVERSE_INSERTION) {
|
||||
return entries
|
||||
.map((entry): TimeEntry => {
|
||||
if (entry.subEntries !== null) {
|
||||
return {
|
||||
...entry,
|
||||
subEntries: getEntriesSorted(
|
||||
entry.subEntries,
|
||||
settings
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return entry;
|
||||
})
|
||||
.reverse();
|
||||
}
|
||||
|
||||
return stripEntriesIndex(
|
||||
entries
|
||||
// Map entries to recursively sort the subEntries
|
||||
.map((entry, index): TimeEntry & { index: number } => {
|
||||
if (entry.subEntries !== null) {
|
||||
return {
|
||||
...entry,
|
||||
subEntries: getEntriesSorted(
|
||||
entry.subEntries,
|
||||
settings
|
||||
),
|
||||
index,
|
||||
};
|
||||
}
|
||||
|
||||
return { ...entry, index };
|
||||
})
|
||||
// Sort by comparator
|
||||
.sort(
|
||||
createEntriesComparator(
|
||||
settings.sortOrder === SortOrder.NEWEST_START,
|
||||
settings.unstartedOrder
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips the "index" property from items, this property
|
||||
* is only used for sorting and needs to be removed after
|
||||
*
|
||||
* @param entries The entries to strip the index from
|
||||
* @returns The entries without the inmdex
|
||||
*/
|
||||
function stripEntriesIndex(
|
||||
entries: (TimeEntry & {
|
||||
index: number;
|
||||
})[]
|
||||
) {
|
||||
return (
|
||||
entries
|
||||
// Map entries to recursively sort the subEntries
|
||||
.map(({ index: _, ...entry }): TimeEntry => {
|
||||
if (entry.subEntries !== null) {
|
||||
return {
|
||||
...entry,
|
||||
subEntries: stripEntriesIndex(
|
||||
entry.subEntries as (TimeEntry & {
|
||||
index: number;
|
||||
})[]
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return entry;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a comparator function for stable sorting a list
|
||||
* of entries
|
||||
*
|
||||
* Entries are sorted in newest/oldest order based on the value
|
||||
* provided
|
||||
*
|
||||
* Any entries without a start time are sorted based on their
|
||||
* original order
|
||||
*
|
||||
* @param newest Whether to sort based on newest or oldest entries
|
||||
* @returns The comparator function
|
||||
*/
|
||||
function createEntriesComparator(
|
||||
newest: boolean,
|
||||
unstartedOrder: UnstartedOrder
|
||||
) {
|
||||
return (
|
||||
a: TimeEntry & { index: number },
|
||||
b: TimeEntry & { index: number }
|
||||
): number => {
|
||||
// Get the start time for both
|
||||
const aStartTime = getStartTime(a, newest);
|
||||
const bStartTime = getStartTime(b, newest);
|
||||
|
||||
// Sort newest when both have a start time
|
||||
if (aStartTime && bStartTime) {
|
||||
return newest
|
||||
? bStartTime.diff(aStartTime)
|
||||
: aStartTime.diff(bStartTime);
|
||||
}
|
||||
|
||||
if (aStartTime) return unstartedOrder === UnstartedOrder.FIRST ? 1 : -1;
|
||||
if (bStartTime) return unstartedOrder === UnstartedOrder.FIRST ? -1 : 1;
|
||||
|
||||
// Fallback to stable sort using the original index
|
||||
return a.index - b.index;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets either the newest or oldest start time from a entry
|
||||
*
|
||||
* @param entry The entry to get the start time from
|
||||
* @param newest Whether to get the newest or oldest
|
||||
* @returns The start time or null if none were available
|
||||
*/
|
||||
function getStartTime(entry: TimeEntry, newest: boolean): Moment | null {
|
||||
// Find the latest start time from entry
|
||||
if (entry.subEntries !== null) {
|
||||
return entry.subEntries.reduce(
|
||||
(previousValue, currentValue) => {
|
||||
if (previousValue === null) {
|
||||
return currentValue.startTime;
|
||||
}
|
||||
|
||||
// Use the current value if its newer
|
||||
if (currentValue.startTime !== null) {
|
||||
const timeDiff = newest
|
||||
? previousValue.diff(currentValue.startTime)
|
||||
: currentValue.startTime.diff(previousValue);
|
||||
|
||||
if (timeDiff > 0) {
|
||||
return currentValue.startTime;
|
||||
}
|
||||
}
|
||||
|
||||
return previousValue;
|
||||
},
|
||||
null as Moment | null
|
||||
);
|
||||
}
|
||||
|
||||
return entry.startTime;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue