@colisweb/rescript-toolkit 5.47.1 → 5.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.secure_files/{ci-functions-v19.0.1 → ci-functions-v20.6.0} +440 -140
- package/.secure_files/{ci-functions-v19.0.0 → ci-functions-v20.6.1} +679 -83
- package/locale/fr.json +10 -0
- package/package.json +1 -1
- package/src/ui/Toolkit__Ui.res +1 -0
- package/src/ui/Toolkit__Ui_Sheet.res +4 -2
- package/src/ui/Toolkit__Ui_Timeline.res +593 -0
package/locale/fr.json
CHANGED
|
@@ -164,6 +164,11 @@
|
|
|
164
164
|
"defaultMessage": "Doit être un entier positif",
|
|
165
165
|
"message": "Doit être un entier positif"
|
|
166
166
|
},
|
|
167
|
+
{
|
|
168
|
+
"id": "_87223ce4",
|
|
169
|
+
"defaultMessage": "Réinitialiser l'échelle",
|
|
170
|
+
"message": "Réinitialiser l'échelle"
|
|
171
|
+
},
|
|
167
172
|
{
|
|
168
173
|
"id": "_8b33a5ae",
|
|
169
174
|
"defaultMessage": "Votre identifiant",
|
|
@@ -199,6 +204,11 @@
|
|
|
199
204
|
"defaultMessage": "Lundi",
|
|
200
205
|
"message": "Lundi"
|
|
201
206
|
},
|
|
207
|
+
{
|
|
208
|
+
"id": "_a37c0ff0",
|
|
209
|
+
"defaultMessage": "Augmenter l'échelle",
|
|
210
|
+
"message": "Augmenter l'échelle"
|
|
211
|
+
},
|
|
202
212
|
{
|
|
203
213
|
"id": "_a7e73ea4",
|
|
204
214
|
"defaultMessage": "Déconnexion",
|
package/package.json
CHANGED
package/src/ui/Toolkit__Ui.res
CHANGED
|
@@ -23,11 +23,13 @@ let make = (
|
|
|
23
23
|
<Dialog.Content
|
|
24
24
|
side={Right}
|
|
25
25
|
className={cx([
|
|
26
|
-
"bg-white shadow-lg w-3/4 xl:w-2/3 2xl:w-1/2 h-full relative data-[state=open]:animate-slideInLeft data-[state=close]:animate-slideOutRight overflow-auto",
|
|
26
|
+
"bg-white shadow-lg w-3/4 xl:w-2/3 max-w-[1000px] 2xl:w-1/2 h-full relative data-[state=open]:animate-slideInLeft data-[state=close]:animate-slideOutRight overflow-auto",
|
|
27
27
|
className,
|
|
28
28
|
])}>
|
|
29
29
|
<header
|
|
30
|
-
className={cx([
|
|
30
|
+
className={cx([
|
|
31
|
+
"flex items-center justify-between gap-4 sticky bg-white top-0 p-4 z-50",
|
|
32
|
+
])}>
|
|
31
33
|
{title->Option.mapWithDefault(React.null, title =>
|
|
32
34
|
<Dialog.Title
|
|
33
35
|
className={cx([
|
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
open ReactIntl
|
|
2
|
+
|
|
3
|
+
let formatToPx = (number: float) => `${number->Float.toString}px`
|
|
4
|
+
|
|
5
|
+
let markerWidth = 24.
|
|
6
|
+
let padding = 24.
|
|
7
|
+
type measuringScale = {
|
|
8
|
+
startHour: float,
|
|
9
|
+
endHour: float,
|
|
10
|
+
displayedHours: float,
|
|
11
|
+
hours: array<int>,
|
|
12
|
+
containerWidth: float,
|
|
13
|
+
// INFO:
|
|
14
|
+
// This is calculated with the container width and the number of displayed hours
|
|
15
|
+
hourSizePx: float,
|
|
16
|
+
minuteSizePx: float,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let calculateMeasuringScale = (~containerWidth, ~startHour: float, ~endHour): measuringScale => {
|
|
20
|
+
// INFO: We must add 1 to the displayedHours value in order to take into account the last hour visibility.
|
|
21
|
+
let displayedHours = endHour -. startHour +. 1.
|
|
22
|
+
let hourSizePx = (containerWidth -. padding *. 2.) /. displayedHours
|
|
23
|
+
// Based on the hour size, we can calculte the width of 1 minute duration
|
|
24
|
+
let minuteSizePx = hourSizePx /. 60.
|
|
25
|
+
let hours = []
|
|
26
|
+
|
|
27
|
+
for h in startHour->Float.toInt to endHour->Float.toInt {
|
|
28
|
+
hours->Js.Array2.push(h)->ignore
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
{
|
|
32
|
+
startHour,
|
|
33
|
+
endHour,
|
|
34
|
+
hours,
|
|
35
|
+
displayedHours,
|
|
36
|
+
containerWidth,
|
|
37
|
+
hourSizePx,
|
|
38
|
+
minuteSizePx,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
type state = {
|
|
42
|
+
// Required to reset the value
|
|
43
|
+
initialContainerWidth: float,
|
|
44
|
+
measuringScale: measuringScale,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type timelinePointExtras = {
|
|
48
|
+
className?: string,
|
|
49
|
+
content?: React.element,
|
|
50
|
+
popoverContent?: React.element,
|
|
51
|
+
popoverContentOptions?: Radix.Popover.Content.options,
|
|
52
|
+
}
|
|
53
|
+
type timelinePoint = {
|
|
54
|
+
start: Js.Date.t,
|
|
55
|
+
end: Js.Date.t,
|
|
56
|
+
extras?: timelinePointExtras,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type marker =
|
|
60
|
+
| Cluster(array<timelinePoint>)
|
|
61
|
+
| Point(timelinePoint)
|
|
62
|
+
|
|
63
|
+
type timlineInfos = {content: React.element, className?: string}
|
|
64
|
+
|
|
65
|
+
type timeline<'data> = {
|
|
66
|
+
points: array<timelinePoint>,
|
|
67
|
+
id: string,
|
|
68
|
+
infos?: timlineInfos,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
type markerOffset = {
|
|
72
|
+
offsetStart: float,
|
|
73
|
+
offsetEnd: float,
|
|
74
|
+
width: float,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let getMarkerOffset = (~marker: marker, ~measuringScale: measuringScale) => {
|
|
78
|
+
let {startHour} = measuringScale
|
|
79
|
+
// INFO:
|
|
80
|
+
// The task is at the beginning of the day, so the index will be 0
|
|
81
|
+
// This index will be multiplied by the hourSize to give it the right position
|
|
82
|
+
// Ex: taskHour is 10h00, startHour is 8h, and the hourSize is 100px
|
|
83
|
+
// (10-8) * 100 = 200 -> the marker will have 200px of offset
|
|
84
|
+
|
|
85
|
+
let calculateOffset = (date: Js.Date.t) => {
|
|
86
|
+
let (startTaskHour, startTaskMinutes) = (date->Js.Date.getHours, date->Js.Date.getMinutes)
|
|
87
|
+
let startHourIndex = startTaskHour -. startHour
|
|
88
|
+
let hourPx = (startHourIndex *. measuringScale.hourSizePx)->Js.Math.floor_float
|
|
89
|
+
let minutesPx = startTaskMinutes *. measuringScale.minuteSizePx
|
|
90
|
+
|
|
91
|
+
hourPx +. minutesPx +. padding
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
switch marker {
|
|
95
|
+
| Cluster(tasks) => {
|
|
96
|
+
let firstTask = tasks[0]->Option.getExn
|
|
97
|
+
let lastTask = tasks->Array.tailExn
|
|
98
|
+
|
|
99
|
+
let offsetStart = calculateOffset(firstTask.start)
|
|
100
|
+
let offsetEnd = calculateOffset(lastTask.end)
|
|
101
|
+
|
|
102
|
+
{
|
|
103
|
+
offsetStart,
|
|
104
|
+
offsetEnd,
|
|
105
|
+
width: offsetEnd -. offsetStart,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
| Point(task) => {
|
|
109
|
+
let offsetStart = calculateOffset(task.start)
|
|
110
|
+
let offsetEnd = calculateOffset(task.end)
|
|
111
|
+
|
|
112
|
+
// Can be drawn like this:
|
|
113
|
+
// ----[offsetStart][offsetWidth][offsetEnd]----
|
|
114
|
+
{
|
|
115
|
+
offsetStart,
|
|
116
|
+
offsetEnd,
|
|
117
|
+
width: Js.Math.max_float(markerWidth, offsetEnd -. offsetStart),
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module HoursRuler = {
|
|
124
|
+
@react.component
|
|
125
|
+
let make = (~setMeasuringScale, ~state: state) => {
|
|
126
|
+
let currentHour = Js.Date.make()->DateFns.getHours
|
|
127
|
+
let {initialContainerWidth, measuringScale: {containerWidth, startHour, endHour}} = state
|
|
128
|
+
let scroll = Toolkit__Hooks.useScrollPosition()
|
|
129
|
+
|
|
130
|
+
let handleClick = (i, event) => {
|
|
131
|
+
event->JsxEventC.Mouse.preventDefault
|
|
132
|
+
|
|
133
|
+
let newScaling = calculateMeasuringScale(
|
|
134
|
+
~containerWidth=containerWidth +. 1500.,
|
|
135
|
+
~startHour,
|
|
136
|
+
~endHour,
|
|
137
|
+
)
|
|
138
|
+
setMeasuringScale(_ => {
|
|
139
|
+
Some({
|
|
140
|
+
initialContainerWidth,
|
|
141
|
+
measuringScale: newScaling,
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
Js.Global.setTimeout(() => {
|
|
145
|
+
ReactDOM.querySelector("#timeline-container")->Option.forEach(div => {
|
|
146
|
+
(div->Obj.magic)["scroll"](. {
|
|
147
|
+
"left": i->Int.toFloat *. newScaling.hourSizePx -. initialContainerWidth /. 2.,
|
|
148
|
+
"behavior": "smooth",
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
}, 16)->ignore
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
<section
|
|
155
|
+
className={cx([
|
|
156
|
+
"flex flex-row bg-neutral-100 pb-3 h-8 px-[--padding] absolute top-[--top] z-30",
|
|
157
|
+
])}
|
|
158
|
+
style={Obj.magic({
|
|
159
|
+
"--padding": padding->formatToPx,
|
|
160
|
+
"--top": (scroll.y < 230. ? 0. : scroll.y -. 230.)->formatToPx,
|
|
161
|
+
})}>
|
|
162
|
+
{state.measuringScale.hours
|
|
163
|
+
->Array.mapWithIndex((i, h) => {
|
|
164
|
+
<React.Fragment key={`timline-hour-${h->Int.toString}`}>
|
|
165
|
+
<div
|
|
166
|
+
className={"h-8 flex-shrink-0 text-sm text-neutral-700 font-mono w-[--size] transition-all duration-250 ease-linear relative"}
|
|
167
|
+
style={Obj.magic({
|
|
168
|
+
"--size": state.measuringScale.hourSizePx->formatToPx,
|
|
169
|
+
})}>
|
|
170
|
+
<span
|
|
171
|
+
onClick={handleClick(i)}
|
|
172
|
+
onContextMenu={handleClick(i)}
|
|
173
|
+
className={cx([
|
|
174
|
+
"absolute right-full transform translate-x-1/2 z-20 rounded py-0.5 cursor-pointer",
|
|
175
|
+
currentHour === h
|
|
176
|
+
? "bg-primary-600 font-semibold text-white px-1"
|
|
177
|
+
: "bg-neutral-100 !rounded-none",
|
|
178
|
+
])}>
|
|
179
|
+
{`${(h >= 24 ? h - 24 : h)->Int.toString}h`->React.string}
|
|
180
|
+
</span>
|
|
181
|
+
</div>
|
|
182
|
+
<span
|
|
183
|
+
className="absolute top-0 w-px h-full border-l border-dashed border-neutral-400 ml-[--offsetX]"
|
|
184
|
+
style={Obj.magic({
|
|
185
|
+
"--offsetX": (state.measuringScale.hourSizePx *.
|
|
186
|
+
(h->Int.toFloat -. state.measuringScale.startHour))->formatToPx,
|
|
187
|
+
})}
|
|
188
|
+
/>
|
|
189
|
+
</React.Fragment>
|
|
190
|
+
})
|
|
191
|
+
->React.array}
|
|
192
|
+
</section>
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module CurrentTimeIndicator = {
|
|
197
|
+
@react.component
|
|
198
|
+
let make = (~measuringScale: measuringScale) => {
|
|
199
|
+
let now = Js.Date.make()
|
|
200
|
+
|
|
201
|
+
let offset = {
|
|
202
|
+
let size =
|
|
203
|
+
(now->DateFns.getHours->Int.toFloat -. measuringScale.startHour) *.
|
|
204
|
+
measuringScale.hourSizePx +. padding
|
|
205
|
+
size +. 2. +. now->Js.Date.getMinutes *. measuringScale.minuteSizePx
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
<section
|
|
209
|
+
className={"w-1 h-full bg-primary-600 absolute left-[--offset-x] z-10 opacity-70"}
|
|
210
|
+
style={Obj.magic({
|
|
211
|
+
"--offset-x": offset->formatToPx,
|
|
212
|
+
})}
|
|
213
|
+
/>
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
module Timeline = {
|
|
218
|
+
module Marker = {
|
|
219
|
+
@react.component
|
|
220
|
+
let make = (
|
|
221
|
+
~marker: marker,
|
|
222
|
+
~clusterClassName="",
|
|
223
|
+
~markerClassName="",
|
|
224
|
+
~measuringScale,
|
|
225
|
+
~setMeasuringScale,
|
|
226
|
+
) => {
|
|
227
|
+
let markerOffset = getMarkerOffset(~marker, ~measuringScale)
|
|
228
|
+
|
|
229
|
+
<div
|
|
230
|
+
onClick={_ => {
|
|
231
|
+
switch marker {
|
|
232
|
+
| Cluster(tasks) =>
|
|
233
|
+
setMeasuringScale(m => {
|
|
234
|
+
switch m {
|
|
235
|
+
| Some({
|
|
236
|
+
initialContainerWidth,
|
|
237
|
+
measuringScale: {containerWidth, startHour, endHour},
|
|
238
|
+
}) => {
|
|
239
|
+
let newScaling =
|
|
240
|
+
tasks->Array.length->Int.toFloat *.
|
|
241
|
+
measuringScale.hourSizePx *.
|
|
242
|
+
measuringScale.displayedHours
|
|
243
|
+
|
|
244
|
+
let newMeasuringScale = calculateMeasuringScale(
|
|
245
|
+
~containerWidth=containerWidth +. newScaling,
|
|
246
|
+
~startHour,
|
|
247
|
+
~endHour,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
let markerOffset = getMarkerOffset(~marker, ~measuringScale=newMeasuringScale)
|
|
251
|
+
|
|
252
|
+
Js.Global.setTimeout(() => {
|
|
253
|
+
ReactDOM.querySelector("#timeline-container")->Option.forEach(
|
|
254
|
+
div => {
|
|
255
|
+
(div->Obj.magic)["scroll"](. {
|
|
256
|
+
"left": markerOffset.offsetStart -. initialContainerWidth /. 2.,
|
|
257
|
+
"behavior": "smooth",
|
|
258
|
+
})
|
|
259
|
+
},
|
|
260
|
+
)
|
|
261
|
+
}, 150)->ignore
|
|
262
|
+
|
|
263
|
+
Some({
|
|
264
|
+
initialContainerWidth,
|
|
265
|
+
measuringScale: newMeasuringScale,
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
| _ => m
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
| Point(_) => ()
|
|
273
|
+
}
|
|
274
|
+
}}
|
|
275
|
+
style={{
|
|
276
|
+
"--x-offset": `${(markerOffset.offsetStart +. padding)->Float.toString}px`,
|
|
277
|
+
"--markerWidth": (markerOffset.width < 1. ? markerWidth : markerOffset.width)->formatToPx,
|
|
278
|
+
"--markerHeight": switch marker {
|
|
279
|
+
| Cluster(_) => markerWidth +. 10.
|
|
280
|
+
| Point(_) => markerWidth
|
|
281
|
+
}->formatToPx,
|
|
282
|
+
}->Obj.magic}
|
|
283
|
+
className={cx([
|
|
284
|
+
"flex-shrink-0 w-[--markerWidth] h-[--markerHeight] inline-flex justify-center items-center rounded-full absolute ml-[--x-offset] transition-all duration-150 ease-linear bg-white border-2 border-neutral-600",
|
|
285
|
+
switch marker {
|
|
286
|
+
| Cluster(_) => `${clusterClassName}`
|
|
287
|
+
| Point(p) =>
|
|
288
|
+
`${markerClassName} ${p.extras
|
|
289
|
+
->Option.flatMap(v => v.className)
|
|
290
|
+
->Option.getWithDefault("")}`
|
|
291
|
+
},
|
|
292
|
+
])}>
|
|
293
|
+
{switch marker {
|
|
294
|
+
| Cluster(p) => p->Array.length->React.int
|
|
295
|
+
| Point({extras}) =>
|
|
296
|
+
switch extras.popoverContent {
|
|
297
|
+
| None =>
|
|
298
|
+
<div
|
|
299
|
+
className={"w-full h-[--markerHeight] cursor-pointer"}
|
|
300
|
+
style={{
|
|
301
|
+
"--markerHeight": switch marker {
|
|
302
|
+
| Cluster(_) => markerWidth +. 10.
|
|
303
|
+
| Point(_) => markerWidth
|
|
304
|
+
}->formatToPx,
|
|
305
|
+
}->Obj.magic}>
|
|
306
|
+
{extras.content->Option.getWithDefault(React.null)}
|
|
307
|
+
</div>
|
|
308
|
+
| Some(c) =>
|
|
309
|
+
<Toolkit__Ui_Popover
|
|
310
|
+
triggerAsChild={true}
|
|
311
|
+
trigger={<div
|
|
312
|
+
className={"w-full h-[--markerHeight] cursor-pointer"}
|
|
313
|
+
style={{
|
|
314
|
+
"--markerHeight": switch marker {
|
|
315
|
+
| Cluster(_) => markerWidth +. 10.
|
|
316
|
+
| Point(_) => markerWidth
|
|
317
|
+
}->formatToPx,
|
|
318
|
+
}->Obj.magic}>
|
|
319
|
+
{extras.content->Option.getWithDefault(React.null)}
|
|
320
|
+
</div>}
|
|
321
|
+
contentOptions=?{extras.popoverContentOptions}
|
|
322
|
+
content={c}
|
|
323
|
+
/>
|
|
324
|
+
}
|
|
325
|
+
| Point(_) => React.null
|
|
326
|
+
}}
|
|
327
|
+
</div>
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
@react.component
|
|
331
|
+
let make = (~timeline: timeline<'a>, ~measuringScale, ~setMeasuringScale) => {
|
|
332
|
+
// INFO: handle the need of cluster if there are too many tasks in 20 min
|
|
333
|
+
let markers =
|
|
334
|
+
timeline.points
|
|
335
|
+
->Array.reduce(Js.Dict.empty(), (acc, task) => {
|
|
336
|
+
let taskHour = task.start->DateFns.getHours
|
|
337
|
+
let taskMinutes = task.start->DateFns.getMinutes
|
|
338
|
+
let taskMinutes = switch taskMinutes {
|
|
339
|
+
| taskMinutes if taskMinutes >= 40 => 40
|
|
340
|
+
| taskMinutes if taskMinutes >= 20 => 20
|
|
341
|
+
| _ => 0
|
|
342
|
+
}
|
|
343
|
+
// INFO: We group tasks per 20 minutes
|
|
344
|
+
let key = `${taskHour->Int.toString}-${taskMinutes->Int.toString}`
|
|
345
|
+
acc->Js.Dict.set(
|
|
346
|
+
key,
|
|
347
|
+
acc
|
|
348
|
+
->Js.Dict.get(key)
|
|
349
|
+
->Option.getWithDefault([])
|
|
350
|
+
->Array.concat([task]),
|
|
351
|
+
)
|
|
352
|
+
acc
|
|
353
|
+
})
|
|
354
|
+
->Js.Dict.values
|
|
355
|
+
->Array.map(tasksByPeriod => {
|
|
356
|
+
let maximumTasksAllowed = measuringScale.hourSizePx /. 3. /. markerWidth
|
|
357
|
+
|
|
358
|
+
if tasksByPeriod->Array.length->Int.toFloat > maximumTasksAllowed {
|
|
359
|
+
[Cluster(tasksByPeriod->Lodash.sortBy(v => v.start))]
|
|
360
|
+
} else {
|
|
361
|
+
tasksByPeriod->Array.map(t => Point(t))
|
|
362
|
+
}
|
|
363
|
+
})
|
|
364
|
+
->Lodash.flatten
|
|
365
|
+
->Lodash.sortBy(marker => {
|
|
366
|
+
switch marker {
|
|
367
|
+
| Point({start}) => start
|
|
368
|
+
| Cluster(tasks) => (tasks->Array.getUnsafe(0)).start
|
|
369
|
+
}
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
let lastMarkerOffset = getMarkerOffset(
|
|
373
|
+
~marker=markers->Toolkit__Primitives.Array.tailExn,
|
|
374
|
+
~measuringScale,
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
<div className="h-[44px]">
|
|
378
|
+
<div
|
|
379
|
+
className={"bg-white rounded z-20 w-[--width] @container"}
|
|
380
|
+
style={Obj.magic({
|
|
381
|
+
"--width": (lastMarkerOffset.offsetEnd +. padding *. 2.)->formatToPx,
|
|
382
|
+
})}>
|
|
383
|
+
<div
|
|
384
|
+
className={cx([
|
|
385
|
+
"flex flex-row items-center py-2 h-10 rounded border-2 border-neutral-400",
|
|
386
|
+
])}>
|
|
387
|
+
<div
|
|
388
|
+
className={"h-0.5 bg-neutral-300 absolute w-[--width] ml-[--offsetX] "}
|
|
389
|
+
style={Obj.magic({
|
|
390
|
+
"--width": lastMarkerOffset.offsetEnd->formatToPx,
|
|
391
|
+
"--offsetX": (padding /. 2.)->formatToPx,
|
|
392
|
+
})}
|
|
393
|
+
/>
|
|
394
|
+
{markers
|
|
395
|
+
->Array.mapWithIndex((i, marker) => {
|
|
396
|
+
<Marker
|
|
397
|
+
key={`${timeline.id}-task-${i->Int.toString}`} marker measuringScale setMeasuringScale
|
|
398
|
+
/>
|
|
399
|
+
})
|
|
400
|
+
->React.array}
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
@react.component
|
|
408
|
+
let make = (
|
|
409
|
+
~timelines: array<timeline<'a>>,
|
|
410
|
+
~infosContainerClassName="",
|
|
411
|
+
~displayCurrentTime=false,
|
|
412
|
+
~centerOnMount=false,
|
|
413
|
+
) => {
|
|
414
|
+
let containerRef = React.useRef(Js.Nullable.null)
|
|
415
|
+
let (measuringScale, setMeasuringScale) = React.useState((): option<state> => None)
|
|
416
|
+
|
|
417
|
+
React.useEffect(() => {
|
|
418
|
+
let getContainerWidth = _ => {
|
|
419
|
+
containerRef.current
|
|
420
|
+
->Js.Nullable.toOption
|
|
421
|
+
->Option.forEach(element => {
|
|
422
|
+
let containerWidth = (element->Browser.DomElement.getBoundingClientRect).width
|
|
423
|
+
let startHour = ref(8.)
|
|
424
|
+
let endHour = ref(23.)
|
|
425
|
+
|
|
426
|
+
timelines->Array.forEachWithIndex(
|
|
427
|
+
(i, timeline) => {
|
|
428
|
+
let sortedTasks = timeline.points->Lodash.sortBy(p => p.start)
|
|
429
|
+
|
|
430
|
+
switch sortedTasks[0] {
|
|
431
|
+
| Some({start}) => {
|
|
432
|
+
let h = start->DateFns.getHours->Int.toFloat
|
|
433
|
+
|
|
434
|
+
// INFO: We have to set the first route to be truely content based
|
|
435
|
+
if i === 0 {
|
|
436
|
+
startHour.contents = h
|
|
437
|
+
} else if h < startHour.contents {
|
|
438
|
+
startHour.contents = h
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
| _ => ()
|
|
442
|
+
}
|
|
443
|
+
switch sortedTasks->Toolkit__Primitives.Array.tail {
|
|
444
|
+
| Some({end}) => {
|
|
445
|
+
let h = end->DateFns.getHours->Int.toFloat
|
|
446
|
+
|
|
447
|
+
// INFO: We have to set the first route to be truely content based
|
|
448
|
+
if i === 0 {
|
|
449
|
+
endHour.contents = h
|
|
450
|
+
} else if h > endHour.contents {
|
|
451
|
+
endHour.contents = h
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
| _ => ()
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
let measuringScale = calculateMeasuringScale(
|
|
460
|
+
~containerWidth=containerWidth +. 600.,
|
|
461
|
+
~startHour=startHour.contents,
|
|
462
|
+
~endHour=endHour.contents,
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
setMeasuringScale(
|
|
466
|
+
_ => Some({
|
|
467
|
+
initialContainerWidth: containerWidth,
|
|
468
|
+
measuringScale,
|
|
469
|
+
}),
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
if centerOnMount {
|
|
473
|
+
Js.Global.setTimeout(
|
|
474
|
+
() => {
|
|
475
|
+
let hourIndex = (endHour.contents -. startHour.contents) /. 2. +. 1.
|
|
476
|
+
let middleHourSection = hourIndex *. measuringScale.hourSizePx -. containerWidth /. 2.
|
|
477
|
+
|
|
478
|
+
ReactDOM.querySelector("#timeline-container")->Option.forEach(
|
|
479
|
+
div => {
|
|
480
|
+
(div->Obj.magic)["scroll"](. {
|
|
481
|
+
"left": middleHourSection,
|
|
482
|
+
"behavior": "smooth",
|
|
483
|
+
})
|
|
484
|
+
},
|
|
485
|
+
)
|
|
486
|
+
},
|
|
487
|
+
100,
|
|
488
|
+
)->ignore
|
|
489
|
+
}
|
|
490
|
+
})
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if !(containerRef.current->Js.Nullable.isNullable) {
|
|
494
|
+
getContainerWidth()
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
Browser.Window.addEventListener("resize", getContainerWidth)
|
|
498
|
+
|
|
499
|
+
Some(
|
|
500
|
+
() => {
|
|
501
|
+
Browser.Window.removeEventListener("resize", getContainerWidth)
|
|
502
|
+
},
|
|
503
|
+
)
|
|
504
|
+
}, (containerRef.current->Js.Nullable.isNullable, timelines->Array.length, centerOnMount))
|
|
505
|
+
|
|
506
|
+
<section className={"flex flex-row"}>
|
|
507
|
+
<div>
|
|
508
|
+
{measuringScale->Option.mapWithDefault(React.null, state => {
|
|
509
|
+
<section className={"flex items-center justify-end gap-2 mr-2"}>
|
|
510
|
+
<Toolkit__Ui_Tooltip
|
|
511
|
+
color={Black}
|
|
512
|
+
side={Top}
|
|
513
|
+
tooltipContent={<FormattedMessage defaultMessage={"Augmenter l'échelle"} />}
|
|
514
|
+
triggerContent={<Toolkit__Ui_Button
|
|
515
|
+
className={"!px-2"}
|
|
516
|
+
onClick={_ =>
|
|
517
|
+
setMeasuringScale(_ => {
|
|
518
|
+
Some({
|
|
519
|
+
initialContainerWidth: state.initialContainerWidth,
|
|
520
|
+
measuringScale: calculateMeasuringScale(
|
|
521
|
+
~containerWidth=state.measuringScale.containerWidth +. 1500.,
|
|
522
|
+
~startHour=state.measuringScale.startHour,
|
|
523
|
+
~endHour=state.measuringScale.endHour,
|
|
524
|
+
),
|
|
525
|
+
})
|
|
526
|
+
})}>
|
|
527
|
+
<ReactIcons.FaSearchPlus />
|
|
528
|
+
</Toolkit__Ui_Button>}
|
|
529
|
+
/>
|
|
530
|
+
{state.measuringScale.containerWidth !== state.initialContainerWidth
|
|
531
|
+
? <Toolkit__Ui_Tooltip
|
|
532
|
+
color={Black}
|
|
533
|
+
side={Top}
|
|
534
|
+
tooltipContent={<FormattedMessage defaultMessage={"Réinitialiser l'échelle"} />}
|
|
535
|
+
triggerContent={<Toolkit__Ui_Button
|
|
536
|
+
className={"!px-2"}
|
|
537
|
+
onClick={_ =>
|
|
538
|
+
setMeasuringScale(_ => {
|
|
539
|
+
Some({
|
|
540
|
+
initialContainerWidth: state.initialContainerWidth,
|
|
541
|
+
measuringScale: calculateMeasuringScale(
|
|
542
|
+
~containerWidth=state.initialContainerWidth,
|
|
543
|
+
~startHour=state.measuringScale.startHour,
|
|
544
|
+
~endHour=state.measuringScale.endHour,
|
|
545
|
+
),
|
|
546
|
+
})
|
|
547
|
+
})}>
|
|
548
|
+
<ReactIcons.MdUndo />
|
|
549
|
+
</Toolkit__Ui_Button>}
|
|
550
|
+
/>
|
|
551
|
+
: React.null}
|
|
552
|
+
</section>
|
|
553
|
+
})}
|
|
554
|
+
<div className={cx(["min-w-32 pt-1 flex flex-col gap-1", infosContainerClassName])}>
|
|
555
|
+
{timelines
|
|
556
|
+
->Array.map(timeline =>
|
|
557
|
+
timeline.infos->Option.mapWithDefault(
|
|
558
|
+
<div key={`${timeline.id}info`} className="h-10" />,
|
|
559
|
+
infos => {
|
|
560
|
+
<div
|
|
561
|
+
key={`${timeline.id}info`}
|
|
562
|
+
className={cx([
|
|
563
|
+
"bg-white border-2 h-10 flex flex-row items-center justify-between px-1",
|
|
564
|
+
infos.className->Option.getWithDefault(""),
|
|
565
|
+
])}>
|
|
566
|
+
{infos.content}
|
|
567
|
+
</div>
|
|
568
|
+
},
|
|
569
|
+
)
|
|
570
|
+
)
|
|
571
|
+
->React.array}
|
|
572
|
+
</div>
|
|
573
|
+
</div>
|
|
574
|
+
<section
|
|
575
|
+
id={"timeline-container"}
|
|
576
|
+
className={"flex flex-col gap-4 relative bg-neutral-100 pb-4 overflow-y-scroll z-10 w-full"}
|
|
577
|
+
ref={ReactDOM.Ref.domRef(containerRef)}>
|
|
578
|
+
{measuringScale->Option.mapWithDefault(React.null, ({measuringScale} as state) => {
|
|
579
|
+
<React.Fragment>
|
|
580
|
+
<HoursRuler setMeasuringScale state />
|
|
581
|
+
{displayCurrentTime ? <CurrentTimeIndicator measuringScale /> : React.null}
|
|
582
|
+
<div className={"mt-10"}>
|
|
583
|
+
{timelines
|
|
584
|
+
->Array.map(timeline =>
|
|
585
|
+
<Timeline key={timeline.id} timeline measuringScale setMeasuringScale />
|
|
586
|
+
)
|
|
587
|
+
->React.array}
|
|
588
|
+
</div>
|
|
589
|
+
</React.Fragment>
|
|
590
|
+
})}
|
|
591
|
+
</section>
|
|
592
|
+
</section>
|
|
593
|
+
}
|