-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #201 from melfore/198-connection-between-tasks
[TimeLine] connection between tasks
- Loading branch information
Showing
13 changed files
with
1,318 additions
and
33 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import type { Meta, StoryObj } from "@storybook/react"; | ||
|
||
import DecoratoGantt from "../utils/decorator"; | ||
|
||
import { generateStoryData } from "./stories-data"; | ||
import KonvaTimeline from "."; | ||
|
||
const meta = { | ||
title: "Scenario/Gantt", | ||
component: KonvaTimeline, | ||
decorators: [DecoratoGantt], | ||
tags: ["autodocs"], | ||
argTypes: { | ||
onTaskClick: { | ||
type: "function", | ||
}, | ||
onTaskChange: { | ||
type: "function", | ||
}, | ||
}, | ||
} satisfies Meta<typeof KonvaTimeline>; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof meta>; | ||
|
||
const { resources } = generateStoryData({ | ||
averageTaskDurationInMinutes: 200, | ||
resourcesCount: 3, | ||
tasksCount: 5, | ||
timeRangeInDays: 1, | ||
}); | ||
|
||
export const Line: Story = { | ||
args: { | ||
onAreaSelect: undefined, | ||
resources, | ||
resolution: "2weeks", | ||
enableLines: true, | ||
toolTip: false, | ||
onTaskClick: (task) => task, | ||
initialDateTime: 1698516000000, | ||
range: { | ||
start: 1698357600000, | ||
end: 1702095200000, | ||
}, | ||
tasks: [ | ||
{ | ||
id: "4", | ||
label: "Task4", | ||
resourceId: "2", | ||
time: { | ||
start: 1698411600000, | ||
end: 1698613200000, | ||
}, | ||
relatedTasks: ["1"], | ||
}, | ||
{ | ||
id: "1", | ||
label: "Task1", | ||
resourceId: "1", | ||
time: { | ||
start: 1698793200000, | ||
end: 1699434800000, | ||
}, | ||
relatedTasks: ["3", "2"], | ||
}, | ||
{ | ||
id: "3", | ||
label: "Task3", | ||
resourceId: "3", | ||
time: { | ||
start: 1699734800000, | ||
end: 1700234800000, | ||
}, | ||
}, | ||
{ | ||
id: "2", | ||
label: "Task2", | ||
resourceId: "2", | ||
time: { | ||
start: 1700434800000, | ||
end: 1700934800000, | ||
}, | ||
relatedTasks: ["5"], | ||
}, | ||
{ | ||
id: "5", | ||
label: "Task5", | ||
resourceId: "1", | ||
time: { | ||
start: 1701505200000, | ||
end: 1702105200000, | ||
}, | ||
}, | ||
], | ||
onTaskChange: (task, opts) => { | ||
task.id, opts; | ||
}, | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
import React, { FC, useCallback, useState } from "react"; | ||
import { Layer } from "react-konva"; | ||
import { DateTime } from "luxon"; | ||
|
||
import { useTimelineContext } from "../../../timeline/TimelineContext"; | ||
import { KonvaPoint } from "../../../utils/konva"; | ||
import { InternalTimeRange } from "../../../utils/time"; | ||
import { LINE_OFFSET } from "../../utils/line"; | ||
import { getTaskYCoordinate, TASK_HEIGHT_OFFSET } from "../../utils/tasks"; | ||
import LineKonva from "../Line"; | ||
import TaskLine from "../TaskLine"; | ||
import TaskTooltip, { TaskTooltipProps } from "../Tooltip"; | ||
|
||
interface TasksLayerProps { | ||
taskTooltip: TaskTooltipProps | null; | ||
setTaskTooltip: (tooltip: TaskTooltipProps | null) => void; | ||
create?: boolean; | ||
onTaskEvent: (value: boolean) => void; | ||
} | ||
|
||
/** | ||
* This component renders a set of tasks as a Konva Layer. | ||
* Tasks are displayed accordingly to their assigned resource (different vertical / row position) and their timing (different horizontal / column position) | ||
* `TasksLayer` is also responsible of handling callback for task components offering base implementation for click, leave and over. | ||
* | ||
* The playground has a canvas that simulates 1 day of data with 1 hour resolution. | ||
* Depending on your screen size you might be able to test also the horizontal scrolling behaviour. | ||
*/ | ||
const LayerLine: FC<TasksLayerProps> = ({ setTaskTooltip, taskTooltip, create, onTaskEvent }) => { | ||
const { columnWidth, drawRange, interval, resolution, resources, rowHeight, tasks, toolTip, validLine } = | ||
useTimelineContext(); | ||
|
||
const [workLine, setWorkLine] = useState([""]); | ||
const { start: intervalStart, end: intervalEnd } = interval; | ||
|
||
const getResourceById = useCallback( | ||
(resourceId: string) => resources.findIndex(({ id }) => resourceId === id), | ||
[resources] | ||
); | ||
|
||
const getTaskById = useCallback((taskId: string) => tasks.find(({ id }) => taskId === id), [tasks]); | ||
|
||
const onTaskLeave = useCallback(() => setTaskTooltip(null), [setTaskTooltip]); | ||
|
||
const onTaskOver = useCallback( | ||
(taskId: string, point: KonvaPoint) => { | ||
const task = getTaskById(taskId); | ||
if (!task) { | ||
return setTaskTooltip(null); | ||
} | ||
|
||
const { x, y } = point; | ||
setTaskTooltip({ task, x, y }); | ||
}, | ||
[getTaskById, setTaskTooltip] | ||
); | ||
|
||
const getXCoordinate = useCallback( | ||
(offset: number) => (offset * columnWidth) / resolution.sizeInUnits, | ||
[columnWidth, resolution.sizeInUnits] | ||
); | ||
|
||
const getTaskXCoordinate = useCallback( | ||
(startTime: number) => { | ||
const timeStart = DateTime.fromMillis(startTime); | ||
const startOffsetInUnit = timeStart.diff(intervalStart!).as(resolution.unit); | ||
return getXCoordinate(startOffsetInUnit); | ||
}, | ||
[getXCoordinate, intervalStart, resolution.unit] | ||
); | ||
|
||
const getTaskWidth = useCallback( | ||
({ start, end }: InternalTimeRange) => { | ||
const timeStart = DateTime.fromMillis(start); | ||
const timeEnd = DateTime.fromMillis(end); | ||
const widthOffsetInUnit = timeEnd.diff(timeStart).as(resolution.unit); | ||
return getXCoordinate(widthOffsetInUnit); | ||
}, | ||
[getXCoordinate, resolution] | ||
); | ||
|
||
if (!intervalStart || !intervalEnd) { | ||
return null; | ||
} | ||
|
||
if (drawRange.end - drawRange.start <= 0) { | ||
return null; | ||
} | ||
return ( | ||
<Layer> | ||
{validLine && | ||
validLine.map((data) => { | ||
const startResourceIndex = getResourceById(data.startResId); | ||
if (startResourceIndex < 0 || workLine.includes(data.id)) { | ||
return null; | ||
} | ||
//line | ||
const yCoordinate = getTaskYCoordinate(startResourceIndex, rowHeight) + (rowHeight * TASK_HEIGHT_OFFSET) / 2; | ||
const endResourceIndex = getResourceById(data.endResId); | ||
const endY = getTaskYCoordinate(endResourceIndex, rowHeight) + (rowHeight * TASK_HEIGHT_OFFSET) / 2; | ||
const startLine = getTaskXCoordinate(data.start); | ||
const endLine = getTaskXCoordinate(data.end); | ||
|
||
return ( | ||
<LineKonva | ||
key={`Layer${data.id}`} | ||
points={[ | ||
startLine, | ||
yCoordinate, | ||
startLine + LINE_OFFSET, | ||
yCoordinate, | ||
endLine - LINE_OFFSET, | ||
endY, | ||
endLine, | ||
endY, | ||
]} | ||
/> | ||
); | ||
})} | ||
{tasks.map((taskData) => { | ||
const { resourceId, time } = taskData; | ||
const resourceIndex = getResourceById(resourceId); | ||
if (resourceIndex < 0) { | ||
return null; | ||
} | ||
|
||
const { color: resourceColor, toCompleteColor } = resources[resourceIndex]; | ||
const xCoordinate = getTaskXCoordinate(time.start); | ||
const yCoordinate = getTaskYCoordinate(resourceIndex, rowHeight); | ||
const width = getTaskWidth(time); | ||
if (xCoordinate > drawRange.end || xCoordinate + width < drawRange.start) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<TaskLine | ||
key={`task-${taskData.id}`} | ||
data={taskData} | ||
fill={resourceColor} | ||
fillToComplete={toCompleteColor} | ||
onLeave={onTaskLeave} | ||
onOver={onTaskOver} | ||
x={xCoordinate} | ||
y={yCoordinate} | ||
width={width} | ||
disabled={create} | ||
onTaskEvent={onTaskEvent} | ||
workLine={setWorkLine} | ||
/> | ||
); | ||
})} | ||
{toolTip && taskTooltip && <TaskTooltip {...taskTooltip} />} | ||
</Layer> | ||
); | ||
}; | ||
|
||
export default LayerLine; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import React from "react"; | ||
import { Circle, Group, Line } from "react-konva"; | ||
|
||
import { | ||
CIRCLE_POINT_COLOR, | ||
CIRCLE_POINT_OFFSET, | ||
CIRCLE_POINT_STROKE, | ||
LINE_COLOR, | ||
LINE_TENSION, | ||
LINE_WIDTH, | ||
LineType, | ||
} from "../../utils/line"; | ||
|
||
const LineKonva = ({ points }: LineType) => { | ||
return ( | ||
<Group> | ||
<Line strokeWidth={LINE_WIDTH} lineJoin="round" stroke={LINE_COLOR} points={points} tension={LINE_TENSION} /> | ||
<Circle | ||
x={points[0] + CIRCLE_POINT_OFFSET} | ||
y={points[1]} | ||
radius={4} | ||
stroke={CIRCLE_POINT_STROKE} | ||
fill={CIRCLE_POINT_COLOR} | ||
strokeWidth={1} | ||
/> | ||
<Circle | ||
x={points[6] - CIRCLE_POINT_OFFSET} | ||
y={points[7]} | ||
radius={4} | ||
stroke={CIRCLE_POINT_STROKE} | ||
fill={CIRCLE_POINT_COLOR} | ||
strokeWidth={1} | ||
/> | ||
</Group> | ||
); | ||
}; | ||
|
||
export default LineKonva; |
Oops, something went wrong.