feat: show path to running entry

This commit is contained in:
Jacobtread 2025-07-07 22:07:28 +12:00
parent 1093d23725
commit 8214cd0448
5 changed files with 131 additions and 1 deletions

View file

@ -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() {
</span>
<div className="active-entry__details">
<span className="active-entry__name">
<b>Name: </b> {currentEntry.name}
<b>Name: </b>{" "}
{pathToEntry && pathToEntry.length > 0 && (
<span className="timekeep-path-to-entry">
{pathToEntry.map((path, index) => (
<span key={path.id}>
{path.name}
{index <
pathToEntry.length - 1 &&
" >"}
</span>
))}
</span>
)}
</span>
<span className="active-entry__time">
<b>{"Started at: "}</b>

View file

@ -224,3 +224,8 @@
color: #777;
margin-left: 0.5rem;
}
.timekeep-path-to-entry {
display: inline-flex;
gap: 0.5rem;
}

View file

@ -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",
},
];

View file

@ -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"

View file

@ -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