@ltht-react/timeline 2.0.189 → 2.0.191
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/README.md +19 -19
- package/package.json +10 -10
- package/src/atoms/time-element.tsx +52 -52
- package/src/atoms/timeline-author.tsx +88 -88
- package/src/atoms/timeline-button.tsx +44 -44
- package/src/atoms/timeline-description.tsx +52 -52
- package/src/atoms/timeline-time.tsx +69 -69
- package/src/atoms/timeline-title-redacted.tsx +13 -13
- package/src/atoms/timeline-title.tsx +41 -41
- package/src/constants.ts +3 -3
- package/src/index.tsx +6 -6
- package/src/molecules/timeline-item-redacted.tsx +47 -47
- package/src/molecules/timeline-item.tsx +130 -130
- package/src/organisms/timeline.tsx +419 -419
|
@@ -1,419 +1,419 @@
|
|
|
1
|
-
import { FC, useEffect, useState } from 'react'
|
|
2
|
-
import styled from '@emotion/styled'
|
|
3
|
-
import Icon from '@ltht-react/icon'
|
|
4
|
-
import { TEXT_COLOURS, BANNER_COLOURS, TABLET_MINIMUM_MEDIA_QUERY } from '@ltht-react/styles'
|
|
5
|
-
import {
|
|
6
|
-
AuditEvent,
|
|
7
|
-
DocumentReference,
|
|
8
|
-
Maybe,
|
|
9
|
-
QuestionnaireResponse,
|
|
10
|
-
TimelineDomainResourceType,
|
|
11
|
-
} from '@ltht-react/types'
|
|
12
|
-
import { formatDate, formatDateExplicitMonth, formatTime, isMobileView } from '@ltht-react/utils'
|
|
13
|
-
import { useWindowSize } from '@ltht-react/hooks'
|
|
14
|
-
import Select from '@ltht-react/select'
|
|
15
|
-
|
|
16
|
-
import TimelineTime from '../atoms/timeline-time'
|
|
17
|
-
import TimelineItem, { ITimelineItem } from '../molecules/timeline-item'
|
|
18
|
-
import TimelineItemRedacted from '../molecules/timeline-item-redacted'
|
|
19
|
-
|
|
20
|
-
const StyledTimeline = styled.div`
|
|
21
|
-
position: relative;
|
|
22
|
-
margin: -0.75rem;
|
|
23
|
-
`
|
|
24
|
-
|
|
25
|
-
const StyledTimelineDayBody = styled.div<IStyledMobile>`
|
|
26
|
-
background-color: white;
|
|
27
|
-
position: relative;
|
|
28
|
-
|
|
29
|
-
&:before {
|
|
30
|
-
content: '';
|
|
31
|
-
position: absolute;
|
|
32
|
-
z-index: 1;
|
|
33
|
-
height: ${({ isMobile }) => (isMobile ? '0%' : '100%')};
|
|
34
|
-
left: calc(50% - 1px);
|
|
35
|
-
border-width: 0 0 0 2px;
|
|
36
|
-
border-color: ${TEXT_COLOURS.INFO};
|
|
37
|
-
border-style: solid;
|
|
38
|
-
}
|
|
39
|
-
`
|
|
40
|
-
|
|
41
|
-
const StyledTimelineDayHeader = styled.div`
|
|
42
|
-
background-color: ${BANNER_COLOURS.DEFAULT.BACKGROUND};
|
|
43
|
-
padding: 0.5rem;
|
|
44
|
-
text-align: center;
|
|
45
|
-
font-weight: bold;
|
|
46
|
-
`
|
|
47
|
-
|
|
48
|
-
const StyledTimelineDayItem = styled.div<IStyledMobile>`
|
|
49
|
-
display: inline-block;
|
|
50
|
-
width: 100%;
|
|
51
|
-
justify-content: center;
|
|
52
|
-
padding: ${({ isMobile }) => (isMobile ? '' : '0 0.5rem')};
|
|
53
|
-
margin: ${({ isMobile }) => (isMobile ? '0.5rem 0' : '1rem 0')};
|
|
54
|
-
`
|
|
55
|
-
|
|
56
|
-
const StyledTimelineDayContent = styled.div<IStyledMobile>`
|
|
57
|
-
width: ${({ isMobile }) => (isMobile ? '100%' : '49%')};
|
|
58
|
-
padding: 0 0.5rem;
|
|
59
|
-
display: inline-block;
|
|
60
|
-
vertical-align: top;
|
|
61
|
-
`
|
|
62
|
-
|
|
63
|
-
const StyledTimelineDayLine = styled.div`
|
|
64
|
-
width: 2%;
|
|
65
|
-
vertical-align: top;
|
|
66
|
-
margin-top: 0.125rem;
|
|
67
|
-
display: inline-block;
|
|
68
|
-
text-align: center;
|
|
69
|
-
position: relative;
|
|
70
|
-
height: 100%;
|
|
71
|
-
`
|
|
72
|
-
|
|
73
|
-
const StyledTimelineDayTimeLeft = styled.div`
|
|
74
|
-
width: 49%;
|
|
75
|
-
padding: 0 0.5rem;
|
|
76
|
-
display: inline-block;
|
|
77
|
-
vertical-align: top;
|
|
78
|
-
text-align: right;
|
|
79
|
-
font-weight: bold;
|
|
80
|
-
`
|
|
81
|
-
|
|
82
|
-
const StyledTimelineDayTimeRight = styled.div`
|
|
83
|
-
width: 49%;
|
|
84
|
-
padding: 0 0.5rem;
|
|
85
|
-
display: inline-block;
|
|
86
|
-
vertical-align: top;
|
|
87
|
-
text-align: left;
|
|
88
|
-
font-weight: bold;
|
|
89
|
-
`
|
|
90
|
-
|
|
91
|
-
const StyledOuterCircle = styled(Icon)`
|
|
92
|
-
position: absolute;
|
|
93
|
-
z-index: 1;
|
|
94
|
-
transform: translate(-50%);
|
|
95
|
-
-webkit-transform: translate(-50%);
|
|
96
|
-
-ms-transform: translate(-50%);
|
|
97
|
-
left: 50%;
|
|
98
|
-
color: ${TEXT_COLOURS.INFO};
|
|
99
|
-
font-size: 0.75rem;
|
|
100
|
-
`
|
|
101
|
-
|
|
102
|
-
const StyledInnerCircle = styled(Icon)`
|
|
103
|
-
position: absolute;
|
|
104
|
-
z-index: 2;
|
|
105
|
-
top: 0.125rem;
|
|
106
|
-
transform: translate(-50%);
|
|
107
|
-
-webkit-transform: translate(-50%);
|
|
108
|
-
-ms-transform: translate(-50%);
|
|
109
|
-
left: 50%;
|
|
110
|
-
color: white;
|
|
111
|
-
font-size: 0.5rem;
|
|
112
|
-
`
|
|
113
|
-
|
|
114
|
-
const StyledFilters = styled.div`
|
|
115
|
-
position: sticky;
|
|
116
|
-
margin-bottom: 1rem;
|
|
117
|
-
top: 0;
|
|
118
|
-
background-color: ${BANNER_COLOURS.DEFAULT.BACKGROUND};
|
|
119
|
-
z-index: 1000;
|
|
120
|
-
padding: 0.5em;
|
|
121
|
-
display: block;
|
|
122
|
-
|
|
123
|
-
${TABLET_MINIMUM_MEDIA_QUERY} {
|
|
124
|
-
display: flex;
|
|
125
|
-
padding: 1em;
|
|
126
|
-
}
|
|
127
|
-
`
|
|
128
|
-
|
|
129
|
-
const StyledFilter = styled.div`
|
|
130
|
-
display: flex;
|
|
131
|
-
align-items: center;
|
|
132
|
-
padding: 0.5rem;
|
|
133
|
-
|
|
134
|
-
${TABLET_MINIMUM_MEDIA_QUERY} {
|
|
135
|
-
max-width: 200px;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
> select {
|
|
139
|
-
min-width: 100px;
|
|
140
|
-
flex: 2;
|
|
141
|
-
|
|
142
|
-
${TABLET_MINIMUM_MEDIA_QUERY} {
|
|
143
|
-
flex: unset;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
> label {
|
|
148
|
-
flex: 1;
|
|
149
|
-
white-space: pre;
|
|
150
|
-
padding-right: 0.5em;
|
|
151
|
-
|
|
152
|
-
${TABLET_MINIMUM_MEDIA_QUERY} {
|
|
153
|
-
flex: unset;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
`
|
|
157
|
-
|
|
158
|
-
const Timeline: FC<IProps> = ({ timelineItems, domainResourceType, filters, onFilterChange }) => {
|
|
159
|
-
const { width } = useWindowSize()
|
|
160
|
-
const isMobile = isMobileView(width)
|
|
161
|
-
const timelineDates: { [date: string]: { item: Maybe<ITimelineItem>[]; formattedDate: string } } = {}
|
|
162
|
-
const [activeFilters, setActiveFilters] = useState<Record<number, string>>({})
|
|
163
|
-
useEffect(() => setActiveFilters([]), [filters])
|
|
164
|
-
|
|
165
|
-
timelineItems?.forEach((timelineItem) => {
|
|
166
|
-
if (!timelineItem?.domainResource) {
|
|
167
|
-
return
|
|
168
|
-
}
|
|
169
|
-
let date = ''
|
|
170
|
-
let displayDate = ''
|
|
171
|
-
|
|
172
|
-
switch (domainResourceType) {
|
|
173
|
-
case TimelineDomainResourceType.QuestionnaireResponse: {
|
|
174
|
-
const qr = timelineItem?.domainResource as QuestionnaireResponse
|
|
175
|
-
if (!qr.authored?.value) {
|
|
176
|
-
return
|
|
177
|
-
}
|
|
178
|
-
date = formatDate(new Date(qr.authored?.value))
|
|
179
|
-
displayDate = formatDateExplicitMonth(new Date(qr.authored?.value))
|
|
180
|
-
break
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
case TimelineDomainResourceType.DocumentReference: {
|
|
184
|
-
const docRef = timelineItem?.domainResource as DocumentReference
|
|
185
|
-
if (!docRef.created?.value) {
|
|
186
|
-
return
|
|
187
|
-
}
|
|
188
|
-
date = formatDate(new Date(docRef.created.value))
|
|
189
|
-
displayDate = formatDateExplicitMonth(new Date(docRef.created.value))
|
|
190
|
-
break
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
case TimelineDomainResourceType.AuditEvent: {
|
|
194
|
-
const audit = timelineItem?.domainResource as AuditEvent
|
|
195
|
-
if (!audit.recorded?.value) {
|
|
196
|
-
return
|
|
197
|
-
}
|
|
198
|
-
date = formatDate(new Date(audit.recorded?.value))
|
|
199
|
-
displayDate = formatDateExplicitMonth(new Date(audit.recorded?.value))
|
|
200
|
-
break
|
|
201
|
-
}
|
|
202
|
-
default:
|
|
203
|
-
throw Error('Unrecognised resource type')
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const lookup = timelineDates[date]
|
|
207
|
-
if (!lookup) {
|
|
208
|
-
timelineDates[date] = { item: [timelineItem], formattedDate: displayDate }
|
|
209
|
-
} else {
|
|
210
|
-
lookup.item.push(timelineItem)
|
|
211
|
-
timelineDates[date] = lookup
|
|
212
|
-
}
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
let position = 0
|
|
216
|
-
|
|
217
|
-
const handleFilterChange = (key: number, filter: ITimelineFilter, value?: string) => {
|
|
218
|
-
const newActiveFilters = { ...activeFilters }
|
|
219
|
-
|
|
220
|
-
if (value && value.length > 0 && filter.options.some((x) => x.value === value)) {
|
|
221
|
-
newActiveFilters[key] = value
|
|
222
|
-
} else {
|
|
223
|
-
delete newActiveFilters[key]
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
setActiveFilters(newActiveFilters)
|
|
227
|
-
onFilterChange && onFilterChange(Object.values(newActiveFilters).filter((x) => x && x.length > 0))
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
return (
|
|
231
|
-
<StyledTimeline key="timeline" data-testid="timeline">
|
|
232
|
-
{filters && (
|
|
233
|
-
<StyledFilters>
|
|
234
|
-
{filters.map((filter, key) => (
|
|
235
|
-
<StyledFilter key={key}>
|
|
236
|
-
<label htmlFor={`${filter.label}-${key}`}>{filter.label}:</label>
|
|
237
|
-
<Select
|
|
238
|
-
id={`${filter.label}-${key}`}
|
|
239
|
-
options={filter.options}
|
|
240
|
-
onChange={(e) => handleFilterChange(key, filter, e.target.value)}
|
|
241
|
-
/>
|
|
242
|
-
</StyledFilter>
|
|
243
|
-
))}
|
|
244
|
-
</StyledFilters>
|
|
245
|
-
)}
|
|
246
|
-
{Object.entries(timelineDates).map(([dateKey, value]) => {
|
|
247
|
-
position += 1
|
|
248
|
-
return (
|
|
249
|
-
<div key={dateKey} data-testid={dateKey}>
|
|
250
|
-
<StyledTimelineDayHeader>{value.formattedDate}</StyledTimelineDayHeader>
|
|
251
|
-
<StyledTimelineDayBody isMobile={isMobile}>
|
|
252
|
-
{value.item?.map((timelineItem, idx) => {
|
|
253
|
-
let content: JSX.Element = <></>
|
|
254
|
-
if (!timelineItem?.domainResource) {
|
|
255
|
-
return <></>
|
|
256
|
-
}
|
|
257
|
-
let currentTime = ''
|
|
258
|
-
let previousTime = ''
|
|
259
|
-
|
|
260
|
-
switch (domainResourceType) {
|
|
261
|
-
case TimelineDomainResourceType.QuestionnaireResponse: {
|
|
262
|
-
const qr = timelineItem?.domainResource as QuestionnaireResponse
|
|
263
|
-
if (!qr.authored?.value) {
|
|
264
|
-
return <></>
|
|
265
|
-
}
|
|
266
|
-
currentTime = formatTime(new Date(qr.authored.value))
|
|
267
|
-
previousTime = currentTime
|
|
268
|
-
|
|
269
|
-
if (idx > 0) {
|
|
270
|
-
const previousItem = value.item[idx - 1]?.domainResource as QuestionnaireResponse
|
|
271
|
-
if (!previousItem?.authored?.value) {
|
|
272
|
-
return <></>
|
|
273
|
-
}
|
|
274
|
-
previousTime = formatTime(new Date(previousItem?.authored.value))
|
|
275
|
-
}
|
|
276
|
-
break
|
|
277
|
-
}
|
|
278
|
-
case TimelineDomainResourceType.DocumentReference: {
|
|
279
|
-
const docRef = timelineItem?.domainResource as DocumentReference
|
|
280
|
-
if (!docRef.created?.value) {
|
|
281
|
-
return <></>
|
|
282
|
-
}
|
|
283
|
-
currentTime = formatTime(new Date(docRef.created.value))
|
|
284
|
-
previousTime = currentTime
|
|
285
|
-
|
|
286
|
-
if (idx > 0) {
|
|
287
|
-
const previousItem = value.item[idx - 1]?.domainResource as DocumentReference
|
|
288
|
-
if (!previousItem?.created?.value) {
|
|
289
|
-
return <></>
|
|
290
|
-
}
|
|
291
|
-
previousTime = formatTime(new Date(previousItem?.created?.value))
|
|
292
|
-
}
|
|
293
|
-
break
|
|
294
|
-
}
|
|
295
|
-
case TimelineDomainResourceType.AuditEvent: {
|
|
296
|
-
const audit = timelineItem?.domainResource as AuditEvent
|
|
297
|
-
if (!audit.recorded?.value) {
|
|
298
|
-
return <></>
|
|
299
|
-
}
|
|
300
|
-
currentTime = formatTime(new Date(audit.recorded?.value))
|
|
301
|
-
previousTime = currentTime
|
|
302
|
-
|
|
303
|
-
if (idx > 0) {
|
|
304
|
-
const previousItem = value.item[idx - 1]?.domainResource as AuditEvent
|
|
305
|
-
if (!previousItem?.recorded?.value) {
|
|
306
|
-
return <></>
|
|
307
|
-
}
|
|
308
|
-
previousTime = formatTime(new Date(previousItem?.recorded?.value))
|
|
309
|
-
}
|
|
310
|
-
break
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
if (currentTime !== previousTime) {
|
|
315
|
-
position += 1
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const itemKey = `timeline_day_item_${dateKey}_${currentTime}_${idx}`
|
|
319
|
-
|
|
320
|
-
const isRedacted = timelineItem.domainResource.metadata.isRedacted ?? false
|
|
321
|
-
|
|
322
|
-
if (isMobile) {
|
|
323
|
-
content = (
|
|
324
|
-
<StyledTimelineDayContent isMobile>
|
|
325
|
-
{isRedacted ? (
|
|
326
|
-
<TimelineItemRedacted />
|
|
327
|
-
) : (
|
|
328
|
-
<TimelineItem timelineItem={timelineItem} domainResourceType={domainResourceType} />
|
|
329
|
-
)}
|
|
330
|
-
</StyledTimelineDayContent>
|
|
331
|
-
)
|
|
332
|
-
} else if (position % 2 === 1) {
|
|
333
|
-
const contentKey = `content_left_${itemKey}`
|
|
334
|
-
content = (
|
|
335
|
-
<div key={contentKey} data-testid={contentKey}>
|
|
336
|
-
<StyledTimelineDayContent isMobile={false}>
|
|
337
|
-
{isRedacted ? (
|
|
338
|
-
<TimelineItemRedacted />
|
|
339
|
-
) : (
|
|
340
|
-
<TimelineItem timelineItem={timelineItem} domainResourceType={domainResourceType} />
|
|
341
|
-
)}
|
|
342
|
-
</StyledTimelineDayContent>
|
|
343
|
-
<StyledTimelineDayLine>
|
|
344
|
-
<StyledOuterCircle type="circle" color="info-blue" size="medium" />
|
|
345
|
-
<StyledInnerCircle type="circle" color="info-blue" size="medium" />
|
|
346
|
-
</StyledTimelineDayLine>
|
|
347
|
-
<StyledTimelineDayTimeRight>
|
|
348
|
-
<TimelineTime
|
|
349
|
-
domainResource={timelineItem?.domainResource}
|
|
350
|
-
domainResourceType={domainResourceType}
|
|
351
|
-
pointInTimeClickHandler={timelineItem?.pointInTimeClickHandler}
|
|
352
|
-
orientation="right"
|
|
353
|
-
/>
|
|
354
|
-
</StyledTimelineDayTimeRight>
|
|
355
|
-
</div>
|
|
356
|
-
)
|
|
357
|
-
} else if (position % 2 === 0) {
|
|
358
|
-
const contentKey = `content_right_${itemKey}`
|
|
359
|
-
content = (
|
|
360
|
-
<div key={contentKey} data-testid={contentKey}>
|
|
361
|
-
<StyledTimelineDayTimeLeft>
|
|
362
|
-
<TimelineTime
|
|
363
|
-
domainResource={timelineItem?.domainResource}
|
|
364
|
-
domainResourceType={domainResourceType}
|
|
365
|
-
pointInTimeClickHandler={timelineItem?.pointInTimeClickHandler}
|
|
366
|
-
orientation="left"
|
|
367
|
-
/>
|
|
368
|
-
</StyledTimelineDayTimeLeft>
|
|
369
|
-
<StyledTimelineDayLine>
|
|
370
|
-
<StyledOuterCircle type="circle" color="info-blue" size="medium" />
|
|
371
|
-
<StyledInnerCircle type="circle" color="info-blue" size="medium" />
|
|
372
|
-
</StyledTimelineDayLine>
|
|
373
|
-
<StyledTimelineDayContent isMobile={false}>
|
|
374
|
-
{isRedacted ? (
|
|
375
|
-
<TimelineItemRedacted />
|
|
376
|
-
) : (
|
|
377
|
-
<TimelineItem timelineItem={timelineItem} domainResourceType={domainResourceType} />
|
|
378
|
-
)}
|
|
379
|
-
</StyledTimelineDayContent>
|
|
380
|
-
</div>
|
|
381
|
-
)
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
return (
|
|
385
|
-
<StyledTimelineDayItem isMobile={isMobile} key={itemKey} data-testid={itemKey}>
|
|
386
|
-
{content}
|
|
387
|
-
</StyledTimelineDayItem>
|
|
388
|
-
)
|
|
389
|
-
})}
|
|
390
|
-
</StyledTimelineDayBody>
|
|
391
|
-
</div>
|
|
392
|
-
)
|
|
393
|
-
})}
|
|
394
|
-
</StyledTimeline>
|
|
395
|
-
)
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
interface IProps {
|
|
399
|
-
timelineItems: Maybe<ITimelineItem>[]
|
|
400
|
-
domainResourceType: TimelineDomainResourceType
|
|
401
|
-
filters?: ITimelineFilter[]
|
|
402
|
-
onFilterChange?: (value: string[]) => void
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
interface IStyledMobile {
|
|
406
|
-
isMobile: boolean
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
export interface ITimelineFilter {
|
|
410
|
-
label: string
|
|
411
|
-
options: ITimelineFilterOption[]
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
export interface ITimelineFilterOption {
|
|
415
|
-
value?: string
|
|
416
|
-
label: string
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
export default Timeline
|
|
1
|
+
import { FC, useEffect, useState } from 'react'
|
|
2
|
+
import styled from '@emotion/styled'
|
|
3
|
+
import Icon from '@ltht-react/icon'
|
|
4
|
+
import { TEXT_COLOURS, BANNER_COLOURS, TABLET_MINIMUM_MEDIA_QUERY } from '@ltht-react/styles'
|
|
5
|
+
import {
|
|
6
|
+
AuditEvent,
|
|
7
|
+
DocumentReference,
|
|
8
|
+
Maybe,
|
|
9
|
+
QuestionnaireResponse,
|
|
10
|
+
TimelineDomainResourceType,
|
|
11
|
+
} from '@ltht-react/types'
|
|
12
|
+
import { formatDate, formatDateExplicitMonth, formatTime, isMobileView } from '@ltht-react/utils'
|
|
13
|
+
import { useWindowSize } from '@ltht-react/hooks'
|
|
14
|
+
import Select from '@ltht-react/select'
|
|
15
|
+
|
|
16
|
+
import TimelineTime from '../atoms/timeline-time'
|
|
17
|
+
import TimelineItem, { ITimelineItem } from '../molecules/timeline-item'
|
|
18
|
+
import TimelineItemRedacted from '../molecules/timeline-item-redacted'
|
|
19
|
+
|
|
20
|
+
const StyledTimeline = styled.div`
|
|
21
|
+
position: relative;
|
|
22
|
+
margin: -0.75rem;
|
|
23
|
+
`
|
|
24
|
+
|
|
25
|
+
const StyledTimelineDayBody = styled.div<IStyledMobile>`
|
|
26
|
+
background-color: white;
|
|
27
|
+
position: relative;
|
|
28
|
+
|
|
29
|
+
&:before {
|
|
30
|
+
content: '';
|
|
31
|
+
position: absolute;
|
|
32
|
+
z-index: 1;
|
|
33
|
+
height: ${({ isMobile }) => (isMobile ? '0%' : '100%')};
|
|
34
|
+
left: calc(50% - 1px);
|
|
35
|
+
border-width: 0 0 0 2px;
|
|
36
|
+
border-color: ${TEXT_COLOURS.INFO};
|
|
37
|
+
border-style: solid;
|
|
38
|
+
}
|
|
39
|
+
`
|
|
40
|
+
|
|
41
|
+
const StyledTimelineDayHeader = styled.div`
|
|
42
|
+
background-color: ${BANNER_COLOURS.DEFAULT.BACKGROUND};
|
|
43
|
+
padding: 0.5rem;
|
|
44
|
+
text-align: center;
|
|
45
|
+
font-weight: bold;
|
|
46
|
+
`
|
|
47
|
+
|
|
48
|
+
const StyledTimelineDayItem = styled.div<IStyledMobile>`
|
|
49
|
+
display: inline-block;
|
|
50
|
+
width: 100%;
|
|
51
|
+
justify-content: center;
|
|
52
|
+
padding: ${({ isMobile }) => (isMobile ? '' : '0 0.5rem')};
|
|
53
|
+
margin: ${({ isMobile }) => (isMobile ? '0.5rem 0' : '1rem 0')};
|
|
54
|
+
`
|
|
55
|
+
|
|
56
|
+
const StyledTimelineDayContent = styled.div<IStyledMobile>`
|
|
57
|
+
width: ${({ isMobile }) => (isMobile ? '100%' : '49%')};
|
|
58
|
+
padding: 0 0.5rem;
|
|
59
|
+
display: inline-block;
|
|
60
|
+
vertical-align: top;
|
|
61
|
+
`
|
|
62
|
+
|
|
63
|
+
const StyledTimelineDayLine = styled.div`
|
|
64
|
+
width: 2%;
|
|
65
|
+
vertical-align: top;
|
|
66
|
+
margin-top: 0.125rem;
|
|
67
|
+
display: inline-block;
|
|
68
|
+
text-align: center;
|
|
69
|
+
position: relative;
|
|
70
|
+
height: 100%;
|
|
71
|
+
`
|
|
72
|
+
|
|
73
|
+
const StyledTimelineDayTimeLeft = styled.div`
|
|
74
|
+
width: 49%;
|
|
75
|
+
padding: 0 0.5rem;
|
|
76
|
+
display: inline-block;
|
|
77
|
+
vertical-align: top;
|
|
78
|
+
text-align: right;
|
|
79
|
+
font-weight: bold;
|
|
80
|
+
`
|
|
81
|
+
|
|
82
|
+
const StyledTimelineDayTimeRight = styled.div`
|
|
83
|
+
width: 49%;
|
|
84
|
+
padding: 0 0.5rem;
|
|
85
|
+
display: inline-block;
|
|
86
|
+
vertical-align: top;
|
|
87
|
+
text-align: left;
|
|
88
|
+
font-weight: bold;
|
|
89
|
+
`
|
|
90
|
+
|
|
91
|
+
const StyledOuterCircle = styled(Icon)`
|
|
92
|
+
position: absolute;
|
|
93
|
+
z-index: 1;
|
|
94
|
+
transform: translate(-50%);
|
|
95
|
+
-webkit-transform: translate(-50%);
|
|
96
|
+
-ms-transform: translate(-50%);
|
|
97
|
+
left: 50%;
|
|
98
|
+
color: ${TEXT_COLOURS.INFO};
|
|
99
|
+
font-size: 0.75rem;
|
|
100
|
+
`
|
|
101
|
+
|
|
102
|
+
const StyledInnerCircle = styled(Icon)`
|
|
103
|
+
position: absolute;
|
|
104
|
+
z-index: 2;
|
|
105
|
+
top: 0.125rem;
|
|
106
|
+
transform: translate(-50%);
|
|
107
|
+
-webkit-transform: translate(-50%);
|
|
108
|
+
-ms-transform: translate(-50%);
|
|
109
|
+
left: 50%;
|
|
110
|
+
color: white;
|
|
111
|
+
font-size: 0.5rem;
|
|
112
|
+
`
|
|
113
|
+
|
|
114
|
+
const StyledFilters = styled.div`
|
|
115
|
+
position: sticky;
|
|
116
|
+
margin-bottom: 1rem;
|
|
117
|
+
top: 0;
|
|
118
|
+
background-color: ${BANNER_COLOURS.DEFAULT.BACKGROUND};
|
|
119
|
+
z-index: 1000;
|
|
120
|
+
padding: 0.5em;
|
|
121
|
+
display: block;
|
|
122
|
+
|
|
123
|
+
${TABLET_MINIMUM_MEDIA_QUERY} {
|
|
124
|
+
display: flex;
|
|
125
|
+
padding: 1em;
|
|
126
|
+
}
|
|
127
|
+
`
|
|
128
|
+
|
|
129
|
+
const StyledFilter = styled.div`
|
|
130
|
+
display: flex;
|
|
131
|
+
align-items: center;
|
|
132
|
+
padding: 0.5rem;
|
|
133
|
+
|
|
134
|
+
${TABLET_MINIMUM_MEDIA_QUERY} {
|
|
135
|
+
max-width: 200px;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
> select {
|
|
139
|
+
min-width: 100px;
|
|
140
|
+
flex: 2;
|
|
141
|
+
|
|
142
|
+
${TABLET_MINIMUM_MEDIA_QUERY} {
|
|
143
|
+
flex: unset;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
> label {
|
|
148
|
+
flex: 1;
|
|
149
|
+
white-space: pre;
|
|
150
|
+
padding-right: 0.5em;
|
|
151
|
+
|
|
152
|
+
${TABLET_MINIMUM_MEDIA_QUERY} {
|
|
153
|
+
flex: unset;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
`
|
|
157
|
+
|
|
158
|
+
const Timeline: FC<IProps> = ({ timelineItems, domainResourceType, filters, onFilterChange }) => {
|
|
159
|
+
const { width } = useWindowSize()
|
|
160
|
+
const isMobile = isMobileView(width)
|
|
161
|
+
const timelineDates: { [date: string]: { item: Maybe<ITimelineItem>[]; formattedDate: string } } = {}
|
|
162
|
+
const [activeFilters, setActiveFilters] = useState<Record<number, string>>({})
|
|
163
|
+
useEffect(() => setActiveFilters([]), [filters])
|
|
164
|
+
|
|
165
|
+
timelineItems?.forEach((timelineItem) => {
|
|
166
|
+
if (!timelineItem?.domainResource) {
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
let date = ''
|
|
170
|
+
let displayDate = ''
|
|
171
|
+
|
|
172
|
+
switch (domainResourceType) {
|
|
173
|
+
case TimelineDomainResourceType.QuestionnaireResponse: {
|
|
174
|
+
const qr = timelineItem?.domainResource as QuestionnaireResponse
|
|
175
|
+
if (!qr.authored?.value) {
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
date = formatDate(new Date(qr.authored?.value))
|
|
179
|
+
displayDate = formatDateExplicitMonth(new Date(qr.authored?.value))
|
|
180
|
+
break
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
case TimelineDomainResourceType.DocumentReference: {
|
|
184
|
+
const docRef = timelineItem?.domainResource as DocumentReference
|
|
185
|
+
if (!docRef.created?.value) {
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
date = formatDate(new Date(docRef.created.value))
|
|
189
|
+
displayDate = formatDateExplicitMonth(new Date(docRef.created.value))
|
|
190
|
+
break
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
case TimelineDomainResourceType.AuditEvent: {
|
|
194
|
+
const audit = timelineItem?.domainResource as AuditEvent
|
|
195
|
+
if (!audit.recorded?.value) {
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
date = formatDate(new Date(audit.recorded?.value))
|
|
199
|
+
displayDate = formatDateExplicitMonth(new Date(audit.recorded?.value))
|
|
200
|
+
break
|
|
201
|
+
}
|
|
202
|
+
default:
|
|
203
|
+
throw Error('Unrecognised resource type')
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const lookup = timelineDates[date]
|
|
207
|
+
if (!lookup) {
|
|
208
|
+
timelineDates[date] = { item: [timelineItem], formattedDate: displayDate }
|
|
209
|
+
} else {
|
|
210
|
+
lookup.item.push(timelineItem)
|
|
211
|
+
timelineDates[date] = lookup
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
let position = 0
|
|
216
|
+
|
|
217
|
+
const handleFilterChange = (key: number, filter: ITimelineFilter, value?: string) => {
|
|
218
|
+
const newActiveFilters = { ...activeFilters }
|
|
219
|
+
|
|
220
|
+
if (value && value.length > 0 && filter.options.some((x) => x.value === value)) {
|
|
221
|
+
newActiveFilters[key] = value
|
|
222
|
+
} else {
|
|
223
|
+
delete newActiveFilters[key]
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
setActiveFilters(newActiveFilters)
|
|
227
|
+
onFilterChange && onFilterChange(Object.values(newActiveFilters).filter((x) => x && x.length > 0))
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<StyledTimeline key="timeline" data-testid="timeline">
|
|
232
|
+
{filters && (
|
|
233
|
+
<StyledFilters>
|
|
234
|
+
{filters.map((filter, key) => (
|
|
235
|
+
<StyledFilter key={key}>
|
|
236
|
+
<label htmlFor={`${filter.label}-${key}`}>{filter.label}:</label>
|
|
237
|
+
<Select
|
|
238
|
+
id={`${filter.label}-${key}`}
|
|
239
|
+
options={filter.options}
|
|
240
|
+
onChange={(e) => handleFilterChange(key, filter, e.target.value)}
|
|
241
|
+
/>
|
|
242
|
+
</StyledFilter>
|
|
243
|
+
))}
|
|
244
|
+
</StyledFilters>
|
|
245
|
+
)}
|
|
246
|
+
{Object.entries(timelineDates).map(([dateKey, value]) => {
|
|
247
|
+
position += 1
|
|
248
|
+
return (
|
|
249
|
+
<div key={dateKey} data-testid={dateKey}>
|
|
250
|
+
<StyledTimelineDayHeader>{value.formattedDate}</StyledTimelineDayHeader>
|
|
251
|
+
<StyledTimelineDayBody isMobile={isMobile}>
|
|
252
|
+
{value.item?.map((timelineItem, idx) => {
|
|
253
|
+
let content: JSX.Element = <></>
|
|
254
|
+
if (!timelineItem?.domainResource) {
|
|
255
|
+
return <></>
|
|
256
|
+
}
|
|
257
|
+
let currentTime = ''
|
|
258
|
+
let previousTime = ''
|
|
259
|
+
|
|
260
|
+
switch (domainResourceType) {
|
|
261
|
+
case TimelineDomainResourceType.QuestionnaireResponse: {
|
|
262
|
+
const qr = timelineItem?.domainResource as QuestionnaireResponse
|
|
263
|
+
if (!qr.authored?.value) {
|
|
264
|
+
return <></>
|
|
265
|
+
}
|
|
266
|
+
currentTime = formatTime(new Date(qr.authored.value))
|
|
267
|
+
previousTime = currentTime
|
|
268
|
+
|
|
269
|
+
if (idx > 0) {
|
|
270
|
+
const previousItem = value.item[idx - 1]?.domainResource as QuestionnaireResponse
|
|
271
|
+
if (!previousItem?.authored?.value) {
|
|
272
|
+
return <></>
|
|
273
|
+
}
|
|
274
|
+
previousTime = formatTime(new Date(previousItem?.authored.value))
|
|
275
|
+
}
|
|
276
|
+
break
|
|
277
|
+
}
|
|
278
|
+
case TimelineDomainResourceType.DocumentReference: {
|
|
279
|
+
const docRef = timelineItem?.domainResource as DocumentReference
|
|
280
|
+
if (!docRef.created?.value) {
|
|
281
|
+
return <></>
|
|
282
|
+
}
|
|
283
|
+
currentTime = formatTime(new Date(docRef.created.value))
|
|
284
|
+
previousTime = currentTime
|
|
285
|
+
|
|
286
|
+
if (idx > 0) {
|
|
287
|
+
const previousItem = value.item[idx - 1]?.domainResource as DocumentReference
|
|
288
|
+
if (!previousItem?.created?.value) {
|
|
289
|
+
return <></>
|
|
290
|
+
}
|
|
291
|
+
previousTime = formatTime(new Date(previousItem?.created?.value))
|
|
292
|
+
}
|
|
293
|
+
break
|
|
294
|
+
}
|
|
295
|
+
case TimelineDomainResourceType.AuditEvent: {
|
|
296
|
+
const audit = timelineItem?.domainResource as AuditEvent
|
|
297
|
+
if (!audit.recorded?.value) {
|
|
298
|
+
return <></>
|
|
299
|
+
}
|
|
300
|
+
currentTime = formatTime(new Date(audit.recorded?.value))
|
|
301
|
+
previousTime = currentTime
|
|
302
|
+
|
|
303
|
+
if (idx > 0) {
|
|
304
|
+
const previousItem = value.item[idx - 1]?.domainResource as AuditEvent
|
|
305
|
+
if (!previousItem?.recorded?.value) {
|
|
306
|
+
return <></>
|
|
307
|
+
}
|
|
308
|
+
previousTime = formatTime(new Date(previousItem?.recorded?.value))
|
|
309
|
+
}
|
|
310
|
+
break
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (currentTime !== previousTime) {
|
|
315
|
+
position += 1
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const itemKey = `timeline_day_item_${dateKey}_${currentTime}_${idx}`
|
|
319
|
+
|
|
320
|
+
const isRedacted = timelineItem.domainResource.metadata.isRedacted ?? false
|
|
321
|
+
|
|
322
|
+
if (isMobile) {
|
|
323
|
+
content = (
|
|
324
|
+
<StyledTimelineDayContent isMobile>
|
|
325
|
+
{isRedacted ? (
|
|
326
|
+
<TimelineItemRedacted />
|
|
327
|
+
) : (
|
|
328
|
+
<TimelineItem timelineItem={timelineItem} domainResourceType={domainResourceType} />
|
|
329
|
+
)}
|
|
330
|
+
</StyledTimelineDayContent>
|
|
331
|
+
)
|
|
332
|
+
} else if (position % 2 === 1) {
|
|
333
|
+
const contentKey = `content_left_${itemKey}`
|
|
334
|
+
content = (
|
|
335
|
+
<div key={contentKey} data-testid={contentKey}>
|
|
336
|
+
<StyledTimelineDayContent isMobile={false}>
|
|
337
|
+
{isRedacted ? (
|
|
338
|
+
<TimelineItemRedacted />
|
|
339
|
+
) : (
|
|
340
|
+
<TimelineItem timelineItem={timelineItem} domainResourceType={domainResourceType} />
|
|
341
|
+
)}
|
|
342
|
+
</StyledTimelineDayContent>
|
|
343
|
+
<StyledTimelineDayLine>
|
|
344
|
+
<StyledOuterCircle type="circle" color="info-blue" size="medium" />
|
|
345
|
+
<StyledInnerCircle type="circle" color="info-blue" size="medium" />
|
|
346
|
+
</StyledTimelineDayLine>
|
|
347
|
+
<StyledTimelineDayTimeRight>
|
|
348
|
+
<TimelineTime
|
|
349
|
+
domainResource={timelineItem?.domainResource}
|
|
350
|
+
domainResourceType={domainResourceType}
|
|
351
|
+
pointInTimeClickHandler={timelineItem?.pointInTimeClickHandler}
|
|
352
|
+
orientation="right"
|
|
353
|
+
/>
|
|
354
|
+
</StyledTimelineDayTimeRight>
|
|
355
|
+
</div>
|
|
356
|
+
)
|
|
357
|
+
} else if (position % 2 === 0) {
|
|
358
|
+
const contentKey = `content_right_${itemKey}`
|
|
359
|
+
content = (
|
|
360
|
+
<div key={contentKey} data-testid={contentKey}>
|
|
361
|
+
<StyledTimelineDayTimeLeft>
|
|
362
|
+
<TimelineTime
|
|
363
|
+
domainResource={timelineItem?.domainResource}
|
|
364
|
+
domainResourceType={domainResourceType}
|
|
365
|
+
pointInTimeClickHandler={timelineItem?.pointInTimeClickHandler}
|
|
366
|
+
orientation="left"
|
|
367
|
+
/>
|
|
368
|
+
</StyledTimelineDayTimeLeft>
|
|
369
|
+
<StyledTimelineDayLine>
|
|
370
|
+
<StyledOuterCircle type="circle" color="info-blue" size="medium" />
|
|
371
|
+
<StyledInnerCircle type="circle" color="info-blue" size="medium" />
|
|
372
|
+
</StyledTimelineDayLine>
|
|
373
|
+
<StyledTimelineDayContent isMobile={false}>
|
|
374
|
+
{isRedacted ? (
|
|
375
|
+
<TimelineItemRedacted />
|
|
376
|
+
) : (
|
|
377
|
+
<TimelineItem timelineItem={timelineItem} domainResourceType={domainResourceType} />
|
|
378
|
+
)}
|
|
379
|
+
</StyledTimelineDayContent>
|
|
380
|
+
</div>
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return (
|
|
385
|
+
<StyledTimelineDayItem isMobile={isMobile} key={itemKey} data-testid={itemKey}>
|
|
386
|
+
{content}
|
|
387
|
+
</StyledTimelineDayItem>
|
|
388
|
+
)
|
|
389
|
+
})}
|
|
390
|
+
</StyledTimelineDayBody>
|
|
391
|
+
</div>
|
|
392
|
+
)
|
|
393
|
+
})}
|
|
394
|
+
</StyledTimeline>
|
|
395
|
+
)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
interface IProps {
|
|
399
|
+
timelineItems: Maybe<ITimelineItem>[]
|
|
400
|
+
domainResourceType: TimelineDomainResourceType
|
|
401
|
+
filters?: ITimelineFilter[]
|
|
402
|
+
onFilterChange?: (value: string[]) => void
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
interface IStyledMobile {
|
|
406
|
+
isMobile: boolean
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export interface ITimelineFilter {
|
|
410
|
+
label: string
|
|
411
|
+
options: ITimelineFilterOption[]
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export interface ITimelineFilterOption {
|
|
415
|
+
value?: string
|
|
416
|
+
label: string
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export default Timeline
|