Skip to content

Commit

Permalink
Merge pull request #109 from melfore/develop
Browse files Browse the repository at this point in the history
[Timeline] Tasks validation and onErrors cb
  • Loading branch information
luciob committed Sep 28, 2023
2 parents 2764447 + 15b4a54 commit e8f2ec3
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 98 deletions.
3 changes: 1 addition & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
## [1.7.7](https://github.com/melfore/konva-timeline/compare/v1.7.6...v1.7.7) (2023-09-26)


### Bug Fixes

* 🐛 [Task] Fixed foreground color ([d037b67](https://github.com/melfore/konva-timeline/commit/d037b675f905ddf31be390fe9f10ff0dcc5c02c5)), closes [#96](https://github.com/melfore/konva-timeline/issues/96)
- 🐛 [Task] Fixed foreground color ([d037b67](https://github.com/melfore/konva-timeline/commit/d037b675f905ddf31be390fe9f10ff0dcc5c02c5)), closes [#96](https://github.com/melfore/konva-timeline/issues/96)

## [1.7.6](https://github.com/melfore/konva-timeline/compare/v1.7.5...v1.7.6) (2023-09-26)

Expand Down
34 changes: 16 additions & 18 deletions src/tasks/utils/tasks.test.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,25 @@
import { DateTime, Interval } from "luxon";

import { TimeRange } from "../..";
import { generateStoryData } from "../../KonvaTimeline/stories-data";

import { filterTasks } from "./tasks";
import { validateTasks } from "./tasks";

// From: Sunday, 1 January 2023 00:00:00 GMT+01:00
// To: Monday, 2 January 2023 00:00:00 GMT+01:00
const range: TimeRange = { start: 1672527600000, end: 1672614000000 };

const interval = Interval.fromDateTimes(DateTime.fromMillis(range.start), DateTime.fromMillis(range.end));

describe("filterTasks", () => {
describe("validateTasks", () => {
it("empty", () => {
const tasks = filterTasks([], interval);
const tasks = validateTasks([], range);
expect(tasks).toEqual({
errors: [{ entity: "task", level: "warn", message: "No data" }],
items: [],
});
});

it("task invalid", () => {
const tasks = filterTasks(
const tasks = validateTasks(
[{ id: "1", label: "Task #1", resourceId: "1", time: { start: 1672578000000, end: 1672563600000 } }],
interval
range
);

expect(tasks).toEqual({
Expand All @@ -31,21 +29,21 @@ describe("filterTasks", () => {
});

it("task out of interval", () => {
const tasks = filterTasks(
const tasks = validateTasks(
[{ id: "1", label: "Task #1", resourceId: "1", time: { start: 1672470000000, end: 1672477200000 } }],
interval
range
);

expect(tasks).toEqual({
errors: [{ entity: "task", level: "warn", message: "Outside interval", refId: "1" }],
errors: [{ entity: "task", level: "warn", message: "Outside range", refId: "1" }],
items: [],
});
});

it("valid", () => {
const tasks = filterTasks(
const tasks = validateTasks(
[{ id: "1", label: "Task #1", resourceId: "1", time: { start: 1672556400000, end: 1672578000000 } }],
interval
range
);

expect(tasks).toEqual({
Expand All @@ -55,18 +53,18 @@ describe("filterTasks", () => {
});

it("mixed", () => {
const tasks = filterTasks(
const tasks = validateTasks(
[
{ id: "1", label: "Task #1", resourceId: "1", time: { start: 1672556400000, end: 1672578000000 } },
{ id: "2", label: "Task #2", resourceId: "1", time: { start: 1672470000000, end: 1672477200000 } },
{ id: "3", label: "Task #3", resourceId: "1", time: { start: 1672578000000, end: 1672563600000 } },
],
interval
range
);

expect(tasks).toEqual({
errors: [
{ entity: "task", level: "warn", message: "Outside interval", refId: "2" },
{ entity: "task", level: "warn", message: "Outside range", refId: "2" },
{ entity: "task", level: "error", message: "Invalid time", refId: "3" },
],
items: [{ id: "1", label: "Task #1", resourceId: "1", time: { start: 1672556400000, end: 1672578000000 } }],
Expand All @@ -82,7 +80,7 @@ describe("filterTasks", () => {
});

const start = new Date().valueOf();
filterTasks(allTasks, Interval.fromDateTimes(DateTime.fromMillis(range.start), DateTime.fromMillis(range.end)));
validateTasks(allTasks, range);
const end = new Date().valueOf();
const operationLength = end - start;
console.log(`Filter tasks: ${operationLength} ms`);
Expand Down
42 changes: 31 additions & 11 deletions src/tasks/utils/tasks.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { Interval } from "luxon";

import { KonvaTimelineError, Operation } from "../../utils/operations";
import { TimeRange } from "../../utils/time-range";

Expand Down Expand Up @@ -27,16 +25,15 @@ type FilteredTasks = Operation<TaskData>;
/**
* Filters valid tasks to be shown in the chart
* @param tasks list of tasks as passed to the component
* @param interval interval as passed to the component
* @param intervals intervals as passed to the component
*/
export const filterTasks = (tasks: TaskData[], interval: Interval): FilteredTasks => {
if (!tasks || !tasks.length) {
return { items: [], errors: [{ entity: "task", level: "warn", message: "No data" }] };
export const validateTasks = (tasks: TaskData[], range: TimeRange | null): FilteredTasks => {
if (!range || !range.start || !range.end) {
return { items: [], errors: [{ entity: "task", level: "warn", message: "Invalid range" }] };
}

const { end: intervalEnd, start: intervalStart } = interval;
if (!intervalEnd || !intervalStart) {
return { items: [], errors: [{ entity: "interval", level: "warn", message: "Incomplete" }] };
if (!tasks || !tasks.length) {
return { items: [], errors: [{ entity: "task", level: "warn", message: "No data" }] };
}

const errors: KonvaTimelineError[] = [];
Expand All @@ -46,8 +43,8 @@ export const filterTasks = (tasks: TaskData[], interval: Interval): FilteredTask
return false;
}

if (taskEnd < intervalStart.toMillis() || taskStart > intervalEnd.toMillis()) {
errors.push({ entity: "task", level: "warn", message: "Outside interval", refId: taskId });
if (taskEnd < range.start || taskStart > range.end) {
errors.push({ entity: "task", level: "warn", message: "Outside range", refId: taskId });
return false;
}

Expand All @@ -56,3 +53,26 @@ export const filterTasks = (tasks: TaskData[], interval: Interval): FilteredTask

return { items, errors };
};

/**
* Filters valid tasks to be shown in the chart
* @param tasks list of tasks as passed to the component
* @param intervals intervals as passed to the component
*/
export const filterTasks = (tasks: TaskData[], range: TimeRange | null): TaskData[] => {
if (!range || !range.start || !range.end || !tasks || !tasks.length) {
return [];
}

return tasks.filter(({ id: taskId, time: { start: taskStart, end: taskEnd } }) => {
if (taskStart >= taskEnd) {
return false;
}

if (taskEnd < range.start || taskStart > range.end) {
return false;
}

return true;
});
};
140 changes: 73 additions & 67 deletions src/timeline/TimelineContext.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import React, { createContext, PropsWithChildren, useContext, useMemo, useState } from "react";
import React, { createContext, PropsWithChildren, useContext, useEffect, useMemo, useState } from "react";
import { DateTime, Interval } from "luxon";

import { addHeaderResource } from "../resources/utils/resources";
import { filterTasks, TaskData } from "../tasks/utils/tasks";
import { filterTasks, TaskData, validateTasks } from "../tasks/utils/tasks";
import { DEFAULT_GRID_COLUMN_WIDTH, DEFAULT_GRID_ROW_HEIGHT, MINIMUM_GRID_ROW_HEIGHT } from "../utils/dimensions";
import { logDebug, logWarn } from "../utils/logger";
import { TimeRange, toInterval } from "../utils/time-range";
import { getResolutionData, Resolution, ResolutionData } from "../utils/time-resolution";
import { TimelineInput } from "../utils/timeline";
import { executeWithPerfomanceCheck } from "../utils/utils";
import { KonvaTimelineError } from "..";

declare global {
interface Window {
Expand All @@ -22,6 +24,10 @@ export type TimelineProviderProps = PropsWithChildren<TimelineInput> & {
* Enables debug logging in browser console
*/
debug?: boolean;
/**
* Callback invoked when errors are thrown
*/
onErrors?: (errors: KonvaTimelineError[]) => void;
/**
* Event handler for task click
*/
Expand All @@ -47,6 +53,7 @@ type TimelineContextType = Required<
dragResolution: ResolutionData;
drawRange: TimeRange;
interval: Interval;
onErrors?: (errors: KonvaTimelineError[]) => void;
onTaskClick?: (task: TaskData) => void;
onTaskDrag?: (task: TaskData) => void;
resolution: ResolutionData;
Expand All @@ -71,6 +78,7 @@ export const TimelineProvider = ({
displayTasksLabel = false,
dragResolution: externalDragResolution,
hideResources = false,
onErrors,
onTaskClick,
onTaskDrag,
tasks: externalTasks,
Expand All @@ -80,42 +88,35 @@ export const TimelineProvider = ({
rowHeight: externalRowHeight,
theme: externalTheme = "light",
}: TimelineProviderProps) => {
logWarn("TimelineProvider", `Debug ${debug ? "ON" : "OFF"}`);
window.__MELFORE_KONVA_TIMELINE_DEBUG__ = debug;
// logWarn("TimelineProvider", `Debug ${debug ? "ON" : "OFF"}`);
// window.__MELFORE_KONVA_TIMELINE_DEBUG__ = debug;

const [drawRange, setDrawRange] = useState(DEFAULT_DRAW_RANGE);

// useEffect(() => {
// logWarn("TimelineProvider", `Debug ${debug ? "ON" : "OFF"}`);
// window.__MELFORE_KONVA_TIMELINE_DEBUG__ = debug;
// }, [debug]);
useEffect(() => {
logWarn("TimelineProvider", `Debug ${debug ? "ON" : "OFF"}`);
window.__MELFORE_KONVA_TIMELINE_DEBUG__ = debug;
}, [debug]);

const interval = useMemo(() => {
logDebug("TimelineProvider", "Calculating interval...");
const start = DateTime.now().toMillis();
const itv = toInterval(range);
const end = DateTime.now().toMillis();
logDebug("TimelineProvider", `Interval calculation took ${end - start} ms`);
return itv;
}, [range]);
const validTasks = useMemo(() => validateTasks(externalTasks, range), [externalTasks, range]);

const resolution = useMemo(() => {
logDebug("TimelineProvider", "Calculating resolution...");
const start = DateTime.now().toMillis();
const resData = getResolutionData(externalResolution);
const end = DateTime.now().toMillis();
logDebug("TimelineProvider", `Resolution calculation took ${end - start} ms`);
return resData;
}, [externalResolution]);
const interval = useMemo(
() => executeWithPerfomanceCheck("TimelineProvider", "interval", () => toInterval(range)),
[range]
);

const dragResolution = useMemo(() => {
logDebug("TimelineProvider", "Calculating drag resolution...");
const start = DateTime.now().toMillis();
const resData = getResolutionData(externalDragResolution || externalResolution);
const end = DateTime.now().toMillis();
logDebug("TimelineProvider", `Drag resolution calculation took ${end - start} ms`);
return resData;
}, [externalDragResolution, externalResolution]);
const resolution = useMemo(
() => executeWithPerfomanceCheck("TimelineProvider", "resolution", () => getResolutionData(externalResolution)),
[externalResolution]
);

const timeBlocks = useMemo(
() =>
executeWithPerfomanceCheck("TimelineProvider", "timeBlocks", () =>
interval.splitBy({ [resolution.unit]: resolution.sizeInUnits })
),
[interval, resolution]
);

const columnWidth = useMemo(() => {
logDebug("TimelineProvider", "Calculating columnWidth...");
Expand All @@ -124,28 +125,6 @@ export const TimelineProvider = ({
: externalColumnWidth;
}, [externalColumnWidth, resolution]);

const resources = useMemo(() => addHeaderResource(externalResources), [externalResources]);

const rowHeight = useMemo(() => {
logDebug("TimelineProvider", "Calculating rowHeight...");
const rowHeight = externalRowHeight || DEFAULT_GRID_ROW_HEIGHT;
return rowHeight < MINIMUM_GRID_ROW_HEIGHT ? MINIMUM_GRID_ROW_HEIGHT : rowHeight;
}, [externalRowHeight]);

const resourcesContentHeight = useMemo(() => {
logDebug("TimelineProvider", "Calculating resources content height...");
return rowHeight * resources.length;
}, [resources, rowHeight]);

const timeBlocks = useMemo(() => {
logDebug("TimelineProvider", "Calculating time blocks...");
const start = DateTime.now().toMillis();
const itvs = interval.splitBy({ [resolution.unit]: resolution.sizeInUnits });
const end = DateTime.now().toMillis();
logDebug("TimelineProvider", `Time blocks calculation took ${end - start} ms`);
return itvs;
}, [interval, resolution]);

const timeblocksOffset = useMemo(() => Math.floor(drawRange.start / columnWidth), [drawRange, columnWidth]);

const visibleTimeBlocks = useMemo(() => {
Expand All @@ -172,31 +151,57 @@ export const TimelineProvider = ({
return vtbs;
}, [timeblocksOffset, columnWidth, drawRange, timeBlocks]);

const tasks = useMemo(() => {
logDebug("TimelineProvider", "Preparing tasks...");
if (!visibleTimeBlocks || !visibleTimeBlocks.length) {
return [];
const visibleRange = useMemo(() => {
let range = null;
if (visibleTimeBlocks && visibleTimeBlocks.length) {
range = {
start: visibleTimeBlocks[0].start!.toMillis(),
end: visibleTimeBlocks[visibleTimeBlocks.length - 1].end!.toMillis(),
};
}

const start = DateTime.now().toMillis();
const interval = Interval.fromDateTimes(
DateTime.fromMillis(visibleTimeBlocks[0].start!.toMillis()),
DateTime.fromMillis(visibleTimeBlocks[visibleTimeBlocks.length - 1].start!.toMillis())
);
return range;
}, [visibleTimeBlocks]);

const { items } = filterTasks(externalTasks, interval);
const tasks = useMemo(
() => executeWithPerfomanceCheck("TimelineProvider", "tasks", () => filterTasks(validTasks.items, visibleRange)),
[validTasks, visibleRange]
);

const dragResolution = useMemo(() => {
logDebug("TimelineProvider", "Calculating drag resolution...");
const start = DateTime.now().toMillis();
const resData = getResolutionData(externalDragResolution || externalResolution);
const end = DateTime.now().toMillis();
logDebug("TimelineProvider", `Tasks preparation took ${end - start} ms`);
return items;
}, [externalTasks, visibleTimeBlocks]);
logDebug("TimelineProvider", `Drag resolution calculation took ${end - start} ms`);
return resData;
}, [externalDragResolution, externalResolution]);

const resources = useMemo(() => addHeaderResource(externalResources), [externalResources]);

const rowHeight = useMemo(() => {
logDebug("TimelineProvider", "Calculating rowHeight...");
const rowHeight = externalRowHeight || DEFAULT_GRID_ROW_HEIGHT;
return rowHeight < MINIMUM_GRID_ROW_HEIGHT ? MINIMUM_GRID_ROW_HEIGHT : rowHeight;
}, [externalRowHeight]);

const resourcesContentHeight = useMemo(() => {
logDebug("TimelineProvider", "Calculating resources content height...");
return rowHeight * resources.length;
}, [resources, rowHeight]);

const theme = useMemo((): TimelineTheme => {
return {
color: externalTheme === "dark" ? "white" : "black",
};
}, [externalTheme]);

useEffect(() => {
if (onErrors) {
onErrors(validTasks.errors);
}
}, [onErrors, validTasks]);

return (
<TimelineContext.Provider
value={{
Expand All @@ -206,6 +211,7 @@ export const TimelineProvider = ({
drawRange,
hideResources,
interval,
onErrors,
onTaskClick,
onTaskDrag,
resolution,
Expand Down
Loading

0 comments on commit e8f2ec3

Please sign in to comment.