mirror of
https://github.com/jacobtread/obsidian-timekeep.git
synced 2026-07-22 10:10:27 +00:00
Merge pull request #64 from jacobtread/feat-folders
feat: Folder entries
This commit is contained in:
commit
68d82b2c2e
14 changed files with 235 additions and 11 deletions
|
|
@ -14,15 +14,23 @@ type Props = {
|
|||
export default function TimekeepName({ name }: Props) {
|
||||
const segments = parseNameSegments(name);
|
||||
|
||||
return segments.map((segment, index) => {
|
||||
switch (segment.type) {
|
||||
case NameSegmentType.Text:
|
||||
return <TimekeepNameText key={index} segment={segment} />;
|
||||
return (
|
||||
<>
|
||||
{segments.map((segment, index) => {
|
||||
switch (segment.type) {
|
||||
case NameSegmentType.Text:
|
||||
return (
|
||||
<TimekeepNameText key={index} segment={segment} />
|
||||
);
|
||||
|
||||
case NameSegmentType.Link:
|
||||
return <TimekeepNameLink key={index} segment={segment} />;
|
||||
}
|
||||
});
|
||||
case NameSegmentType.Link:
|
||||
return (
|
||||
<TimekeepNameLink key={index} segment={segment} />
|
||||
);
|
||||
}
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function TimekeepNameText({ segment }: { segment: NameSegmentText }) {
|
||||
|
|
|
|||
|
|
@ -94,6 +94,13 @@ export default function TimesheetRow({ entry, indent }: Props) {
|
|||
className="timekeep-entry-name"
|
||||
title={entry.name}
|
||||
onClick={handleToggleCollapsed}>
|
||||
{entry.folder && (
|
||||
<ObsidianIcon
|
||||
icon="folder"
|
||||
className="timekeep-folder-icon"
|
||||
/>
|
||||
)}
|
||||
|
||||
<TimekeepName name={entry.name} />
|
||||
|
||||
{entry.subEntries !== null && (
|
||||
|
|
|
|||
|
|
@ -219,6 +219,12 @@
|
|||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.timekeep-folder-icon {
|
||||
margin-right: 0.5rem;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.timekeep-start-note {
|
||||
font-size: 0.8rem;
|
||||
color: #777;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
import moment from "moment";
|
||||
|
||||
export const currentTime = moment();
|
||||
|
||||
export const input = {
|
||||
id: "9054dee3-8c15-493b-ad31-f070e08c2699",
|
||||
name: "Folder",
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
subEntries: [
|
||||
{
|
||||
id: "8054dee3-8c15-493b-ad31-f070e08c2699",
|
||||
name: "New Entry",
|
||||
startTime: currentTime,
|
||||
endTime: currentTime,
|
||||
subEntries: null,
|
||||
},
|
||||
],
|
||||
folder: true,
|
||||
};
|
||||
|
||||
export const expected = {
|
||||
id: "9054dee3-8c15-493b-ad31-f070e08c2699",
|
||||
name: "Folder",
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
subEntries: [
|
||||
{
|
||||
id: "8054dee3-8c15-493b-ad31-f070e08c2699",
|
||||
name: "New Entry",
|
||||
startTime: currentTime,
|
||||
endTime: currentTime,
|
||||
subEntries: null,
|
||||
},
|
||||
{
|
||||
id: "5054dee3-8c15-493b-ad31-f070e08c2699",
|
||||
name: "New Entry 2",
|
||||
startTime: currentTime,
|
||||
endTime: null,
|
||||
subEntries: null,
|
||||
},
|
||||
],
|
||||
folder: true,
|
||||
};
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import moment from "moment";
|
||||
|
||||
export const currentTime = moment();
|
||||
|
||||
export const input = {
|
||||
id: "9054dee3-8c15-493b-ad31-f070e08c2699",
|
||||
name: "Folder",
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
subEntries: null,
|
||||
folder: true,
|
||||
};
|
||||
|
||||
export const expected = {
|
||||
id: "9054dee3-8c15-493b-ad31-f070e08c2699",
|
||||
name: "Folder",
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
subEntries: [
|
||||
{
|
||||
id: "8054dee3-8c15-493b-ad31-f070e08c2699",
|
||||
name: "New Entry",
|
||||
startTime: currentTime,
|
||||
endTime: null,
|
||||
subEntries: null,
|
||||
},
|
||||
],
|
||||
folder: true,
|
||||
};
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import moment from "moment";
|
||||
|
||||
export const currentTime = moment();
|
||||
export const entryToRemove = {
|
||||
id: "9054dee3-8c15-493b-ad31-f070e08c2699",
|
||||
name: "Block 1",
|
||||
startTime: currentTime,
|
||||
endTime: currentTime,
|
||||
subEntries: null,
|
||||
};
|
||||
|
||||
export const entries = [
|
||||
{
|
||||
id: "8054dee3-8c15-493b-ad31-f070e08c2699",
|
||||
name: "Block 3",
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
subEntries: [entryToRemove],
|
||||
folder: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const expectedEntries = [
|
||||
{
|
||||
id: "8054dee3-8c15-493b-ad31-f070e08c2699",
|
||||
name: "Block 3",
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
subEntries: [],
|
||||
folder: true,
|
||||
},
|
||||
];
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import moment from "moment";
|
||||
|
||||
export const currentTime = moment();
|
||||
|
||||
export const targetEntry = {
|
||||
id: "9054dee3-8c15-493b-ad31-f070e08c2699",
|
||||
name: "Folder Entry",
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
subEntries: null,
|
||||
folder: true,
|
||||
};
|
||||
|
||||
export const input = [targetEntry];
|
||||
|
||||
export const expected = [
|
||||
{
|
||||
id: "9054dee3-8c15-493b-ad31-f070e08c2699",
|
||||
name: "Folder Entry",
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
subEntries: [
|
||||
{
|
||||
id: "9054dee3-8c15-493b-ad31-f070e08c2699",
|
||||
name: "Part 1",
|
||||
startTime: currentTime,
|
||||
endTime: null,
|
||||
subEntries: null,
|
||||
},
|
||||
],
|
||||
folder: true,
|
||||
},
|
||||
];
|
||||
|
|
@ -51,6 +51,26 @@ describe("withSubEntry", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("adding first entry for folder should populate subentries", async () => {
|
||||
const { input, currentTime, expected } = await import(
|
||||
"./__fixtures__/manipulating/adding_sub_entry/addFolderPopulateSubEntries"
|
||||
);
|
||||
const output = withSubEntry(input, "New Entry", currentTime);
|
||||
expect(stripEntryRuntimeData(output)).toEqual(
|
||||
stripEntryRuntimeData(expected)
|
||||
);
|
||||
});
|
||||
|
||||
it("adding first entry for folder should extend subentries", async () => {
|
||||
const { input, currentTime, expected } = await import(
|
||||
"./__fixtures__/manipulating/adding_sub_entry/addFolderExtendSubEntries"
|
||||
);
|
||||
const output = withSubEntry(input, "New Entry 2", currentTime);
|
||||
expect(stripEntryRuntimeData(output)).toEqual(
|
||||
stripEntryRuntimeData(expected)
|
||||
);
|
||||
});
|
||||
|
||||
it("adding to group should extend sub entries", async () => {
|
||||
const { input, currentTime, expected } = await import(
|
||||
"./__fixtures__/manipulating/adding_sub_entry/addToGroupExtendSubEntries"
|
||||
|
|
|
|||
|
|
@ -112,6 +112,15 @@ function getSubEntryName(name: string, groupEntry: TimeEntryGroup) {
|
|||
* @returns The group entry
|
||||
*/
|
||||
function makeGroupEntry(entry: TimeEntry): TimeEntryGroup {
|
||||
if (entry.folder) {
|
||||
return {
|
||||
...entry,
|
||||
subEntries: entry.subEntries ?? [],
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (entry.subEntries !== null) {
|
||||
return entry;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ const TIME_ENTRY_SINGLE = z
|
|||
.transform((value) => (value === null ? null : moment(value))),
|
||||
// Single entries have no children
|
||||
subEntries: z.null(),
|
||||
// Optional field to indicate the entry should stay as a group when non-started and should only create
|
||||
// sub entries when starting
|
||||
folder: z.boolean().optional(),
|
||||
})
|
||||
// At runtime a unique ID is inserted
|
||||
.transform((entry) => ({
|
||||
|
|
@ -49,7 +52,11 @@ const TIME_ENTRY_GROUP_BASE = z.object({
|
|||
endTime: z.null(),
|
||||
// Optional field to indicate the entry is collapsed
|
||||
collapsed: z.boolean().optional(),
|
||||
// Optional field to indicate the entry should stay as a group when non-started and should only create
|
||||
// sub entries when starting
|
||||
folder: z.boolean().optional(),
|
||||
});
|
||||
|
||||
// Schema for a time entry group
|
||||
const TIME_ENTRY_GROUP: z.ZodType<
|
||||
TimeEntryGroup,
|
||||
|
|
|
|||
|
|
@ -42,6 +42,17 @@ describe("startNewNestedEntry", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("starting a new entry within a folder should create a subentry", async () => {
|
||||
const { currentTime, targetEntry, input, expected } = await import(
|
||||
"./__fixtures__/manipulating/start_entry/startNestedFolderEntry"
|
||||
);
|
||||
|
||||
const output = startNewNestedEntry(currentTime, targetEntry.id, input);
|
||||
expect(stripEntriesRuntimeData(output)).toEqual(
|
||||
stripEntriesRuntimeData(expected)
|
||||
);
|
||||
});
|
||||
|
||||
it("starting a new entry should stop any running entries", async () => {
|
||||
const { currentTime, targetEntry, input, expected } = await import(
|
||||
"./__fixtures__/manipulating/start_entry/startNotStartedEntry"
|
||||
|
|
|
|||
|
|
@ -49,8 +49,12 @@ export function startNewNestedEntry(
|
|||
return entries;
|
||||
}
|
||||
|
||||
// If the entry has been started or is a group create a new child entry
|
||||
if (currentEntry.subEntries !== null || currentEntry.startTime !== null) {
|
||||
// If the entry has been started, is a group, or is a folder group create a new child entry
|
||||
if (
|
||||
currentEntry.subEntries !== null ||
|
||||
currentEntry.startTime !== null ||
|
||||
currentEntry.folder
|
||||
) {
|
||||
return updateEntry(
|
||||
entries,
|
||||
// Ensure the current entry is ended
|
||||
|
|
|
|||
|
|
@ -106,6 +106,14 @@ describe("removeEntry", () => {
|
|||
const updated = removeEntry(entries, entryToRemove);
|
||||
expect(updated).toEqual(expectedEntries);
|
||||
});
|
||||
|
||||
it("should not collapse folder on empty entries", async () => {
|
||||
const { entries, entryToRemove, expectedEntries } = await import(
|
||||
"./__fixtures__/manipulating/remove_entry/removeEntryFolder"
|
||||
);
|
||||
const updated = removeEntry(entries, entryToRemove);
|
||||
expect(updated).toEqual(expectedEntries);
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeSubEntry", () => {
|
||||
|
|
|
|||
|
|
@ -119,7 +119,8 @@ export function removeEntry(
|
|||
// Add non-empty entries to the accumulator
|
||||
if (
|
||||
collapsedEntry.subEntries === null ||
|
||||
collapsedEntry.subEntries.length > 0
|
||||
collapsedEntry.subEntries.length > 0 ||
|
||||
collapsedEntry.folder
|
||||
) {
|
||||
acc.push(collapsedEntry);
|
||||
}
|
||||
|
|
@ -157,6 +158,11 @@ export function removeSubEntry(entry: TimeEntry, target: TimeEntry): TimeEntry {
|
|||
* @returns The collapsed entry
|
||||
*/
|
||||
function makeEntrySingle(target: TimeEntry): TimeEntry {
|
||||
// Cannot collapse folders
|
||||
if (target.folder) {
|
||||
return target;
|
||||
}
|
||||
|
||||
// Target has no entries to collapse
|
||||
if (target.subEntries === null) {
|
||||
return target;
|
||||
|
|
|
|||
Loading…
Reference in a new issue