Skip to content

Commit

Permalink
Merge pull request #110 from melfore/105-timeline-time-related-types
Browse files Browse the repository at this point in the history
[Timeline] Time related types
  • Loading branch information
luciob authored Sep 29, 2023
2 parents 239dcc5 + 2091f0a commit 290f9d0
Show file tree
Hide file tree
Showing 13 changed files with 147 additions and 62 deletions.
6 changes: 2 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
# [1.8.0](https://github.com/melfore/konva-timeline/compare/v1.7.7...v1.8.0) (2023-09-28)


### Bug Fixes

* 🐛 [Timeline] Removed useless init warn log ([34d6e6c](https://github.com/melfore/konva-timeline/commit/34d6e6c1aff688bee9ae1ca5a35cf03cd7de8c8b)), closes [#108](https://github.com/melfore/konva-timeline/issues/108)

- 🐛 [Timeline] Removed useless init warn log ([34d6e6c](https://github.com/melfore/konva-timeline/commit/34d6e6c1aff688bee9ae1ca5a35cf03cd7de8c8b)), closes [#108](https://github.com/melfore/konva-timeline/issues/108)

### Features

* 🎸 [Timeline] Added onErrors callback ([15b4a54](https://github.com/melfore/konva-timeline/commit/15b4a54b4697abffbfb05a041aa6b8b9a75f0e53)), closes [#95](https://github.com/melfore/konva-timeline/issues/95)
- 🎸 [Timeline] Added onErrors callback ([15b4a54](https://github.com/melfore/konva-timeline/commit/15b4a54b4697abffbfb05a041aa6b8b9a75f0e53)), closes [#95](https://github.com/melfore/konva-timeline/issues/95)

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

Expand Down
27 changes: 25 additions & 2 deletions src/@stories/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,33 @@ interface TimeRange {
/**
* Start of time range interval
*/
start: number;
start: number | string;
/**
* End of time range interval
*/
end: number;
end: number | string;
}
```

#### KonvaTimelineError

```
interface KonvaTimelineError {
/**
* The entity that thrown the error
*/
entity: DataEntity;
/**
* The error level (error or warn)
*/
level: ErrorLevel;
/**
* The error message
*/
message: string;
/**
* The refId for entity item
*/
refId?: string;
}
```
12 changes: 12 additions & 0 deletions src/KonvaTimeline/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";
import { DateTime } from "luxon";

import { generateStoryData } from "./stories-data";
import KonvaTimeline from ".";
Expand Down Expand Up @@ -64,3 +65,14 @@ export const HiddenResources: Story = {
hideResources: true,
},
};

export const MixedDateTimeFormats: Story = {
args: {
...Primary.args,
onErrors: (errors) => errors.forEach((error) => console.log({ error })),
range: {
start: DateTime.fromMillis(range.start).toUTC().toISO()!,
end: DateTime.fromMillis(range.end).toUTC().toISO()!,
},
},
};
14 changes: 9 additions & 5 deletions src/KonvaTimeline/stories-data.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Resource } from "../resources/utils/resources";
import { TaskData } from "../tasks/utils/tasks";
import { TimeRange } from "../utils/time-range";
import { InternalTimeRange } from "../utils/time";

interface StoryDataInput {
resourcesCount: number;
Expand All @@ -12,7 +12,7 @@ interface StoryDataInput {
export interface StoryData {
resources: Resource[];
tasks: TaskData[];
range: TimeRange;
range: InternalTimeRange;
}

export const HOUR_IN_MILLISECONDS = 1000 * 60 * 60;
Expand Down Expand Up @@ -42,8 +42,12 @@ const generateResources = (count: number): Resource[] => {
return resources;
};

const generateTasks = (count: number, avgDurationInMinutes: number, resourcesCount: number): TaskData[] => {
const tasks: TaskData[] = [];
const generateTasks = (
count: number,
avgDurationInMinutes: number,
resourcesCount: number
): TaskData<InternalTimeRange>[] => {
const tasks: TaskData<InternalTimeRange>[] = [];

for (let i = 1; i <= count; i++) {
const resourceId = `${Math.floor(Math.random() * resourcesCount) + 1}`;
Expand All @@ -70,7 +74,7 @@ const generateTasks = (count: number, avgDurationInMinutes: number, resourcesCou
return tasks;
};

const generateTimeRange = (durationInDays: number): TimeRange => {
const generateTimeRange = (durationInDays: number): InternalTimeRange => {
const start = TIME_RANGE_START_DATE.valueOf();
const end = TIME_RANGE_START_DATE.setDate(TIME_RANGE_START_DATE.getDate() + durationInDays).valueOf();

Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export { Resource } from "./resources/utils/resources";
export { TaskData } from "./tasks/utils/tasks";
export { KonvaTimelineError } from "./utils/operations";
export { TimeRange } from "./utils/time-range";
export { TimeRange } from "./utils/time";
export { RESOLUTIONS, Resolution } from "./utils/time-resolution";

export { default as KonvaTimeline } from "./KonvaTimeline";
4 changes: 2 additions & 2 deletions src/tasks/components/Layer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { DateTime } from "luxon";

import { useTimelineContext } from "../../../timeline/TimelineContext";
import { KonvaPoint } from "../../../utils/konva";
import { TimeRange } from "../../../utils/time-range";
import { InternalTimeRange } from "../../../utils/time";
import Task from "../Task";
import TaskTooltip, { TaskTooltipProps } from "../Tooltip";

Expand Down Expand Up @@ -69,7 +69,7 @@ const TasksLayer: FC<TasksLayerProps> = ({ setTaskTooltip, taskTooltip }) => {
);

const getTaskWidth = useCallback(
({ start, end }: TimeRange) => {
({ start, end }: InternalTimeRange) => {
const timeStart = DateTime.fromMillis(start);
const timeEnd = DateTime.fromMillis(end);
const widthOffsetInUnit = timeEnd.diff(timeStart).as(resolution.unit);
Expand Down
4 changes: 2 additions & 2 deletions src/tasks/utils/tasks.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { TimeRange } from "../..";
import { generateStoryData } from "../../KonvaTimeline/stories-data";
import { InternalTimeRange } from "../../utils/time";

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 range: InternalTimeRange = { start: 1672527600000, end: 1672614000000 };

describe("validateTasks", () => {
it("empty", () => {
Expand Down
47 changes: 30 additions & 17 deletions src/tasks/utils/tasks.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { KonvaTimelineError, Operation } from "../../utils/operations";
import { TimeRange } from "../../utils/time-range";
import { getValidTime, InternalTimeRange, TimeRange } from "../../utils/time";

export interface TaskData {
export interface TaskData<T extends TimeRange = TimeRange> {
/**
* Unique identifier of the task
*/
Expand All @@ -17,17 +17,17 @@ export interface TaskData {
/**
* Task time range
*/
time: TimeRange;
time: T;
}

type FilteredTasks = Operation<TaskData>;
type FilteredTasks = Operation<TaskData<InternalTimeRange>>;

/**
* 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 validateTasks = (tasks: TaskData[], range: TimeRange | null): FilteredTasks => {
export const validateTasks = (tasks: TaskData[], range: InternalTimeRange | null): FilteredTasks => {
if (!range || !range.start || !range.end) {
return { items: [], errors: [{ entity: "task", level: "warn", message: "Invalid range" }] };
}
Expand All @@ -37,19 +37,29 @@ export const validateTasks = (tasks: TaskData[], range: TimeRange | null): Filte
}

const errors: KonvaTimelineError[] = [];
const items = tasks.filter(({ id: taskId, time: { start: taskStart, end: taskEnd } }) => {
if (taskStart >= taskEnd) {
errors.push({ entity: "task", level: "error", message: "Invalid time", refId: taskId });
return false;
}
const items = tasks
.map(
(task): TaskData<InternalTimeRange> => ({
...task,
time: {
start: getValidTime(task.time.start),
end: getValidTime(task.time.end),
},
})
)
.filter(({ id: taskId, time: { start: taskStart, end: taskEnd } }) => {
if (taskStart >= taskEnd) {
errors.push({ entity: "task", level: "error", message: "Invalid time", refId: taskId });
return false;
}

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

return true;
});
return true;
});

return { items, errors };
};
Expand All @@ -59,7 +69,10 @@ export const validateTasks = (tasks: TaskData[], range: TimeRange | null): Filte
* @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[] => {
export const filterTasks = (
tasks: TaskData<InternalTimeRange>[],
range: InternalTimeRange | null
): TaskData<InternalTimeRange>[] => {
if (!range || !range.start || !range.end || !tasks || !tasks.length) {
return [];
}
Expand Down
25 changes: 18 additions & 7 deletions src/timeline/TimelineContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { addHeaderResource } from "../resources/utils/resources";
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 { getValidTime, InternalTimeRange } from "../utils/time";
import { getIntervalFromInternalTimeRange } from "../utils/time";
import { getResolutionData, Resolution, ResolutionData } from "../utils/time-resolution";
import { TimelineInput } from "../utils/timeline";
import { executeWithPerfomanceCheck } from "../utils/utils";
Expand Down Expand Up @@ -47,27 +48,29 @@ type TimelineTheme = {
};

type TimelineContextType = Required<
Pick<TimelineInput, "columnWidth" | "displayTasksLabel" | "hideResources" | "resources" | "rowHeight" | "tasks">
Pick<TimelineInput, "columnWidth" | "displayTasksLabel" | "hideResources" | "resources" | "rowHeight">
> & {
blocksOffset: number;
dragResolution: ResolutionData;
drawRange: TimeRange;
drawRange: InternalTimeRange;
interval: Interval;
onErrors?: (errors: KonvaTimelineError[]) => void;
onTaskClick?: (task: TaskData) => void;
onTaskDrag?: (task: TaskData) => void;
resolution: ResolutionData;
resolutionKey: Resolution;
resourcesContentHeight: number;
setDrawRange: (range: TimeRange) => void;
setDrawRange: (range: InternalTimeRange) => void;
tasks: TaskData<InternalTimeRange>[];
theme: TimelineTheme;
timeBlocks: Interval[];
visibleTimeBlocks: Interval[];
};

const TimelineContext = createContext<TimelineContextType | undefined>(undefined);

const DEFAULT_DRAW_RANGE: TimeRange = { start: 0, end: 0 };
// TODO#lb: this should be another data type, specific to drawing
const DEFAULT_DRAW_RANGE: InternalTimeRange = { start: 0, end: 0 };

const TIME_BLOCKS_PRELOAD = 5;

Expand All @@ -82,7 +85,7 @@ export const TimelineProvider = ({
onTaskClick,
onTaskDrag,
tasks: externalTasks,
range,
range: externalRange,
resolution: externalResolution,
resources: externalResources,
rowHeight: externalRowHeight,
Expand All @@ -98,10 +101,18 @@ export const TimelineProvider = ({
window.__MELFORE_KONVA_TIMELINE_DEBUG__ = debug;
}, [debug]);

const range = useMemo((): InternalTimeRange => {
const { start: externalStart, end: externalEnd } = externalRange;
const start = getValidTime(externalStart);
const end = getValidTime(externalEnd);

return { start, end };
}, [externalRange]);

const validTasks = useMemo(() => validateTasks(externalTasks, range), [externalTasks, range]);

const interval = useMemo(
() => executeWithPerfomanceCheck("TimelineProvider", "interval", () => toInterval(range)),
() => executeWithPerfomanceCheck("TimelineProvider", "interval", () => getIntervalFromInternalTimeRange(range)),
[range]
);

Expand Down
20 changes: 0 additions & 20 deletions src/utils/time-range.ts

This file was deleted.

44 changes: 44 additions & 0 deletions src/utils/time.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { DateTime, Interval } from "luxon";

export interface TimeRange {
/**
* Start of time range interval
*/
start: number | string;
/**
* End of time range interval
*/
end: number | string;
}

export interface InternalTimeRange {
start: number;
end: number;
}

/**
* Returns valid date based on input, otherwise now
* @param date the input date (number or string formats)
*/
export const getValidTime = (date: number | string): number => {
if (typeof date === "number" && !Number.isNaN(date)) {
return date;
}

if (typeof date === "string") {
const dateTime = DateTime.fromISO(date, { zone: "utc" });
if (dateTime.toISO() === date) {
return dateTime.toMillis();
}
}

return DateTime.now().toMillis();
};

/**
* Converts a TimeRange to a luxon Interval
* @param range TimeRange to convert
*/
export const getIntervalFromInternalTimeRange = ({ start, end }: InternalTimeRange): Interval => {
return Interval.fromDateTimes(DateTime.fromMillis(start), DateTime.fromMillis(end));
};
2 changes: 1 addition & 1 deletion src/utils/timeline.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Resource } from "../resources/utils/resources";
import { TaskData } from "../tasks/utils/tasks";

import { TimeRange } from "./time-range";
import { TimeRange } from "./time";
import { Resolution } from "./time-resolution";

export type TimelineInput = {
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@
"target": "ES2016"
},
"include": ["src"],
"exclude": ["dist", "node_modules", "src/**/*.test.tsx", "src/**/*.stories.tsx"]
"exclude": ["dist", "node_modules", "src/**/*.test.*", "src/**/*.stories.*"]
}

0 comments on commit 290f9d0

Please sign in to comment.