From 8214cd04484f6d8fce0f3ee9832b3db8efdd518a Mon Sep 17 00:00:00 2001 From: Jacobtread Date: Mon, 7 Jul 2025 22:07:28 +1200 Subject: [PATCH] feat: show path to running entry --- src/components/TimesheetStart.tsx | 24 ++++++++- src/styles.css | 5 ++ .../checking/findRunningEntryPath.ts | 51 +++++++++++++++++++ src/timekeep/index.test.ts | 11 ++++ src/timekeep/index.ts | 41 +++++++++++++++ 5 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 src/timekeep/__fixtures__/checking/findRunningEntryPath.ts diff --git a/src/components/TimesheetStart.tsx b/src/components/TimesheetStart.tsx index 8261903..b189e6e 100644 --- a/src/components/TimesheetStart.tsx +++ b/src/components/TimesheetStart.tsx @@ -7,6 +7,7 @@ import { useTimekeepStore } from "@/contexts/use-timekeep-store"; import { withEntry, isKeepRunning, + getPathToEntry, getRunningEntry, stopRunningEntries, } from "@/timekeep"; @@ -29,6 +30,14 @@ export default function TimekeepStart() { [timekeep] ); + const pathToEntry = useMemo( + () => + currentEntry + ? getPathToEntry(timekeep.entries, currentEntry) + : null, + [timekeep, currentEntry] + ); + const isTimekeepRunning = currentEntry !== null; const onStart = (event: FormEvent) => { @@ -86,7 +95,20 @@ export default function TimekeepStart() {
- Name: {currentEntry.name} + Name: {" "} + {pathToEntry && pathToEntry.length > 0 && ( + + {pathToEntry.map((path, index) => ( + + {path.name} + + {index < + pathToEntry.length - 1 && + " >"} + + ))} + + )} {"Started at: "} diff --git a/src/styles.css b/src/styles.css index 97820fc..54ea10f 100644 --- a/src/styles.css +++ b/src/styles.css @@ -224,3 +224,8 @@ color: #777; margin-left: 0.5rem; } + +.timekeep-path-to-entry { + display: inline-flex; + gap: 0.5rem; +} diff --git a/src/timekeep/__fixtures__/checking/findRunningEntryPath.ts b/src/timekeep/__fixtures__/checking/findRunningEntryPath.ts new file mode 100644 index 0000000..87588b8 --- /dev/null +++ b/src/timekeep/__fixtures__/checking/findRunningEntryPath.ts @@ -0,0 +1,51 @@ +import moment from "moment"; + +export const currentTime = moment(); + +export const runningEntry = { + id: "9054dee3-8c15-493b-ad31-f070e08c2699", + name: "Running Entry", + startTime: currentTime, + endTime: null, + subEntries: null, +}; + +export const input = [ + { + id: "8054dee3-8c15-493b-ad31-f070e08c2699", + name: "Block 1", + startTime: null, + endTime: null, + subEntries: [ + { + id: "7054dee3-8c15-493b-ad31-f070e08c2699", + name: "Part 1", + startTime: currentTime, + endTime: currentTime, + subEntries: null, + }, + { + id: "7054dee3-8c15-493b-ad31-f070e08c269a", + name: "Part 2", + startTime: null, + endTime: null, + subEntries: [runningEntry], + }, + ], + }, +]; + +export const path = [ + { + id: "8054dee3-8c15-493b-ad31-f070e08c2699", + name: "Block 1", + }, + { + id: "7054dee3-8c15-493b-ad31-f070e08c269a", + name: "Part 2", + }, + { + id: "9054dee3-8c15-493b-ad31-f070e08c2699", + name: "Running Entry", + }, +]; diff --git a/src/timekeep/index.test.ts b/src/timekeep/index.test.ts index f17223f..df9a5e0 100644 --- a/src/timekeep/index.test.ts +++ b/src/timekeep/index.test.ts @@ -23,6 +23,7 @@ import { isKeepRunning, isEntryRunning, removeSubEntry, + getPathToEntry, getRunningEntry, getEntryDuration, getTotalDuration, @@ -300,6 +301,16 @@ describe("checking entries", () => { expect(output).toBe(runningEntry); }); + it("should find running entry path", async () => { + const { input, runningEntry, path } = await import( + "./__fixtures__/checking/findRunningEntryPath" + ); + + const output = getPathToEntry(input, runningEntry); + + expect(output).toEqual(path); + }); + it("should not find running entry", async () => { const { input } = await import( "./__fixtures__/checking/shouldNotFindRunningEntry" diff --git a/src/timekeep/index.ts b/src/timekeep/index.ts index d84ece0..1020d59 100644 --- a/src/timekeep/index.ts +++ b/src/timekeep/index.ts @@ -288,6 +288,47 @@ export function isEntryRunning(entry: TimeEntry) { return entry.startTime !== null && entry.endTime === null; } +/** + * Get the path to the provided target entry + * + * @param entries The collection of entries + * @param target The target entry + * @returns The path to the entry + */ +export function getPathToEntry( + entries: TimeEntry[], + target: TimeEntry +): { id: string; name: string }[] { + const path: { id: string; name: string }[] = []; + + function dfs(entry: TimeEntry): boolean { + path.push({ id: entry.id, name: entry.name }); + + if (entry.id === target.id) { + return true; + } + + if (entry.subEntries) { + for (const sub of entry.subEntries) { + if (dfs(sub)) { + return true; + } + } + } + + path.pop(); + return false; + } + + for (const entry of entries) { + if (dfs(entry)) { + return path; + } + } + + return []; +} + /** * Searches through the nested list of time entries using * a "stack" depth-first search approach attempting to