@ltht-react/timeline 2.0.3 → 2.0.4
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/package.json +11 -10
- package/src/atoms/timeline-author.tsx +88 -0
- package/src/atoms/timeline-button.tsx +44 -0
- package/src/atoms/timeline-description.tsx +52 -0
- package/src/atoms/timeline-time.tsx +49 -0
- package/src/atoms/timeline-title-redacted.tsx +13 -0
- package/src/atoms/timeline-title.tsx +41 -0
- package/src/constants.ts +3 -0
- package/src/index.tsx +6 -0
- package/src/molecules/timeline-item-redacted.tsx +47 -0
- package/src/molecules/timeline-item.tsx +129 -0
- package/src/organisms/timeline.tsx +415 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ltht-react/timeline",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.4",
|
|
4
4
|
"description": "> TODO: description",
|
|
5
5
|
"author": "Jonny Dyson",
|
|
6
6
|
"homepage": "",
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"main": "lib/index.js",
|
|
9
9
|
"typings": "lib/index.d.ts",
|
|
10
10
|
"files": [
|
|
11
|
-
"lib"
|
|
11
|
+
"lib",
|
|
12
|
+
"src"
|
|
12
13
|
],
|
|
13
14
|
"directories": {
|
|
14
15
|
"lib": "lib",
|
|
@@ -23,15 +24,15 @@
|
|
|
23
24
|
},
|
|
24
25
|
"dependencies": {
|
|
25
26
|
"@emotion/styled": "^11.0.0",
|
|
26
|
-
"@ltht-react/banner": "^2.0.
|
|
27
|
-
"@ltht-react/hooks": "^2.0.
|
|
28
|
-
"@ltht-react/icon": "^2.0.
|
|
29
|
-
"@ltht-react/select": "^2.0.
|
|
30
|
-
"@ltht-react/styles": "^2.0.
|
|
31
|
-
"@ltht-react/types": "^2.0.
|
|
32
|
-
"@ltht-react/utils": "^2.0.
|
|
27
|
+
"@ltht-react/banner": "^2.0.4",
|
|
28
|
+
"@ltht-react/hooks": "^2.0.4",
|
|
29
|
+
"@ltht-react/icon": "^2.0.4",
|
|
30
|
+
"@ltht-react/select": "^2.0.4",
|
|
31
|
+
"@ltht-react/styles": "^2.0.4",
|
|
32
|
+
"@ltht-react/types": "^2.0.4",
|
|
33
|
+
"@ltht-react/utils": "^2.0.4",
|
|
33
34
|
"html-react-parser": "^5.0.6",
|
|
34
35
|
"react": "^18.2.0"
|
|
35
36
|
},
|
|
36
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "b8fe243323a0f1b9ab345475db38c73fd3b35312"
|
|
37
38
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { FC, HTMLAttributes } from 'react'
|
|
2
|
+
import Icon from '@ltht-react/icon'
|
|
3
|
+
import styled from '@emotion/styled'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
AuditEvent,
|
|
7
|
+
DocumentReference,
|
|
8
|
+
Maybe,
|
|
9
|
+
QuestionnaireResponse,
|
|
10
|
+
TimelineDomainResourceType,
|
|
11
|
+
} from '@ltht-react/types'
|
|
12
|
+
import PRIMARY_AUTHOR from '../constants'
|
|
13
|
+
|
|
14
|
+
const StyledTimelineItemLeft = styled.div`
|
|
15
|
+
flex-grow: 1;
|
|
16
|
+
`
|
|
17
|
+
|
|
18
|
+
const TimelineAuthor: FC<Props> = ({ domainResource, domainResourceType, ...rest }) => {
|
|
19
|
+
if (!domainResource) return <></>
|
|
20
|
+
|
|
21
|
+
switch (domainResourceType) {
|
|
22
|
+
case TimelineDomainResourceType.QuestionnaireResponse: {
|
|
23
|
+
const qr = domainResource as QuestionnaireResponse
|
|
24
|
+
if (!qr?.author) {
|
|
25
|
+
return <></>
|
|
26
|
+
}
|
|
27
|
+
return (
|
|
28
|
+
<StyledTimelineItemLeft {...rest}>
|
|
29
|
+
<Icon type="user" size="medium" color="grey" /> by {qr?.author?.display}
|
|
30
|
+
</StyledTimelineItemLeft>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
case TimelineDomainResourceType.DocumentReference: {
|
|
34
|
+
const docRef = domainResource as DocumentReference
|
|
35
|
+
const authorList: string[] = []
|
|
36
|
+
|
|
37
|
+
if (docRef?.author == null) {
|
|
38
|
+
return <></>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
docRef.author.forEach((auth) => {
|
|
42
|
+
if (auth) {
|
|
43
|
+
authorList.push(auth.specialty ? `${auth.fullName} (${auth.specialty})` : `${auth.fullName}`)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
if (authorList.length === 0) {
|
|
48
|
+
return <></>
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<StyledTimelineItemLeft {...rest}>
|
|
53
|
+
<Icon type="user" size="medium" color="grey" /> by {authorList.join(', ')}
|
|
54
|
+
</StyledTimelineItemLeft>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
case TimelineDomainResourceType.AuditEvent: {
|
|
58
|
+
const audit = domainResource as AuditEvent
|
|
59
|
+
let authorName = ''
|
|
60
|
+
|
|
61
|
+
audit.agent.forEach((agent) => {
|
|
62
|
+
agent?.role?.forEach((role) => {
|
|
63
|
+
const isPrimaryAuthor = !!role?.coding?.find((x) => x?.code === PRIMARY_AUTHOR)
|
|
64
|
+
!authorName && isPrimaryAuthor && (authorName = agent.who?.display || '')
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
if (!authorName) {
|
|
69
|
+
return <></>
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<StyledTimelineItemLeft {...rest}>
|
|
74
|
+
<Icon type="user" size="medium" color="grey" /> by {authorName}
|
|
75
|
+
</StyledTimelineItemLeft>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return <></>
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
84
|
+
domainResource?: Maybe<AuditEvent | QuestionnaireResponse | DocumentReference>
|
|
85
|
+
domainResourceType: TimelineDomainResourceType
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export default TimelineAuthor
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ButtonBanner } from '@ltht-react/banner'
|
|
2
|
+
import Icon from '@ltht-react/icon'
|
|
3
|
+
import { FC, HTMLAttributes } from 'react'
|
|
4
|
+
import { ITimelineItem } from '../molecules/timeline-item'
|
|
5
|
+
|
|
6
|
+
const TimelineButton: FC<Props> = ({ timelineItem, className }) => {
|
|
7
|
+
const { clickHandler, buttonState, buttonText } = timelineItem
|
|
8
|
+
|
|
9
|
+
switch (buttonState) {
|
|
10
|
+
case 'no-button':
|
|
11
|
+
return <></>
|
|
12
|
+
case 'permission-denied-button':
|
|
13
|
+
return (
|
|
14
|
+
<ButtonBanner className={className} type="warning" disabled>
|
|
15
|
+
{buttonText ?? 'Insufficient privileges to view this item'}
|
|
16
|
+
</ButtonBanner>
|
|
17
|
+
)
|
|
18
|
+
case 'selectable-button':
|
|
19
|
+
return (
|
|
20
|
+
<ButtonBanner className={className} type="info" onClick={clickHandler}>
|
|
21
|
+
{buttonText ?? ''}
|
|
22
|
+
</ButtonBanner>
|
|
23
|
+
)
|
|
24
|
+
case 'selected-button':
|
|
25
|
+
return (
|
|
26
|
+
<ButtonBanner
|
|
27
|
+
className={className}
|
|
28
|
+
type="highlight"
|
|
29
|
+
icon={<Icon type="info-circle" color="info-blue" size="medium" />}
|
|
30
|
+
onClick={clickHandler}
|
|
31
|
+
>
|
|
32
|
+
{buttonText ?? ''}
|
|
33
|
+
</ButtonBanner>
|
|
34
|
+
)
|
|
35
|
+
default:
|
|
36
|
+
throw new Error('ButtonState must be a valid value.')
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
41
|
+
timelineItem: ITimelineItem
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default TimelineButton
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { FC, HTMLAttributes } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
AuditEvent,
|
|
4
|
+
DocumentReference,
|
|
5
|
+
Maybe,
|
|
6
|
+
QuestionnaireResponse,
|
|
7
|
+
TimelineDomainResourceType,
|
|
8
|
+
} from '@ltht-react/types'
|
|
9
|
+
import parseHtml from 'html-react-parser'
|
|
10
|
+
import styled from '@emotion/styled'
|
|
11
|
+
|
|
12
|
+
const StyledDescription = styled.div`
|
|
13
|
+
p {
|
|
14
|
+
margin: 0 0.2rem;
|
|
15
|
+
display: inline;
|
|
16
|
+
}
|
|
17
|
+
`
|
|
18
|
+
|
|
19
|
+
const TimelineDescription: FC<Props> = ({ domainResource, domainResourceType, ...rest }) => {
|
|
20
|
+
if (!domainResource) return <></>
|
|
21
|
+
|
|
22
|
+
switch (domainResourceType) {
|
|
23
|
+
case TimelineDomainResourceType.QuestionnaireResponse: {
|
|
24
|
+
const qr = domainResource as QuestionnaireResponse
|
|
25
|
+
if (!qr.questionnaire?.description) {
|
|
26
|
+
return <></>
|
|
27
|
+
}
|
|
28
|
+
return <StyledDescription {...rest}>{parseHtml(qr.questionnaire?.description)}</StyledDescription>
|
|
29
|
+
}
|
|
30
|
+
case TimelineDomainResourceType.DocumentReference: {
|
|
31
|
+
const docRef = domainResource as DocumentReference
|
|
32
|
+
return docRef.description ? <>{docRef.description}</> : <></>
|
|
33
|
+
}
|
|
34
|
+
case TimelineDomainResourceType.AuditEvent: {
|
|
35
|
+
const audit = domainResource as AuditEvent
|
|
36
|
+
if (!audit.outcomeDesc) {
|
|
37
|
+
return <></>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return <StyledDescription {...rest}>{parseHtml(audit.outcomeDesc)}</StyledDescription>
|
|
41
|
+
}
|
|
42
|
+
default:
|
|
43
|
+
return <></>
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
48
|
+
domainResource?: Maybe<AuditEvent | QuestionnaireResponse | DocumentReference>
|
|
49
|
+
domainResourceType: TimelineDomainResourceType
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default TimelineDescription
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { FC, HTMLAttributes } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
AuditEvent,
|
|
4
|
+
DocumentReference,
|
|
5
|
+
Maybe,
|
|
6
|
+
QuestionnaireResponse,
|
|
7
|
+
TimelineDomainResourceType,
|
|
8
|
+
} from '@ltht-react/types'
|
|
9
|
+
import { formatTime } from '@ltht-react/utils'
|
|
10
|
+
|
|
11
|
+
const TimelineTime: FC<Props> = ({ domainResource, domainResourceType, ...rest }) => {
|
|
12
|
+
if (!domainResource) return <></>
|
|
13
|
+
|
|
14
|
+
switch (domainResourceType) {
|
|
15
|
+
case TimelineDomainResourceType.QuestionnaireResponse: {
|
|
16
|
+
const qr = domainResource as QuestionnaireResponse
|
|
17
|
+
if (!qr?.authored?.value) {
|
|
18
|
+
return <></>
|
|
19
|
+
}
|
|
20
|
+
const time = formatTime(new Date(qr?.authored.value))
|
|
21
|
+
return <div {...rest}>{time}</div>
|
|
22
|
+
}
|
|
23
|
+
case TimelineDomainResourceType.DocumentReference: {
|
|
24
|
+
const docRef = domainResource as DocumentReference
|
|
25
|
+
if (docRef && docRef?.created?.value) {
|
|
26
|
+
const time = formatTime(new Date(docRef.created.value))
|
|
27
|
+
return <div {...rest}>{time}</div>
|
|
28
|
+
}
|
|
29
|
+
return <></>
|
|
30
|
+
}
|
|
31
|
+
case TimelineDomainResourceType.AuditEvent: {
|
|
32
|
+
const audit = domainResource as AuditEvent
|
|
33
|
+
if (audit && audit?.recorded?.value) {
|
|
34
|
+
const time = formatTime(new Date(audit.recorded.value))
|
|
35
|
+
return <div {...rest}>{time}</div>
|
|
36
|
+
}
|
|
37
|
+
return <></>
|
|
38
|
+
}
|
|
39
|
+
default:
|
|
40
|
+
return <></>
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
45
|
+
domainResource?: Maybe<AuditEvent | QuestionnaireResponse | DocumentReference>
|
|
46
|
+
domainResourceType: TimelineDomainResourceType
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default TimelineTime
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { FC, HTMLAttributes } from 'react'
|
|
2
|
+
import { TEXT_COLOURS } from '@ltht-react/styles'
|
|
3
|
+
import styled from '@emotion/styled'
|
|
4
|
+
|
|
5
|
+
const StyledRedactedMessage = styled.div`
|
|
6
|
+
color: ${TEXT_COLOURS.SECONDARY.VALUE};
|
|
7
|
+
`
|
|
8
|
+
|
|
9
|
+
const TimelineTitleRedacted: FC<HTMLAttributes<HTMLDivElement>> = (props) => (
|
|
10
|
+
<StyledRedactedMessage {...props}>Insufficient Privileges</StyledRedactedMessage>
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
export default TimelineTitleRedacted
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { FC, HTMLAttributes } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
AuditEvent,
|
|
4
|
+
DocumentReference,
|
|
5
|
+
Maybe,
|
|
6
|
+
QuestionnaireResponse,
|
|
7
|
+
TimelineDomainResourceType,
|
|
8
|
+
} from '@ltht-react/types'
|
|
9
|
+
|
|
10
|
+
const TimelineTitle: FC<Props> = ({ domainResource, domainResourceType, ...rest }) => {
|
|
11
|
+
if (!domainResource) return <></>
|
|
12
|
+
|
|
13
|
+
switch (domainResourceType) {
|
|
14
|
+
case TimelineDomainResourceType.QuestionnaireResponse: {
|
|
15
|
+
const qr = domainResource as QuestionnaireResponse
|
|
16
|
+
const questionnaireTitle = qr.metadata.isRedacted
|
|
17
|
+
? 'Insufficient privileges'
|
|
18
|
+
: qr.text?.text ?? qr.questionnaire?.title
|
|
19
|
+
return <div {...rest}>{questionnaireTitle}</div>
|
|
20
|
+
}
|
|
21
|
+
case TimelineDomainResourceType.DocumentReference: {
|
|
22
|
+
const docRef = domainResource as DocumentReference
|
|
23
|
+
const docTitle = docRef.metadata.isRedacted ? 'Insufficient privileges' : docRef.text?.text
|
|
24
|
+
return <div {...rest}>{docTitle}</div>
|
|
25
|
+
}
|
|
26
|
+
case TimelineDomainResourceType.AuditEvent: {
|
|
27
|
+
const audit = domainResource as AuditEvent
|
|
28
|
+
const auditTitle = audit.metadata.isRedacted ? 'Insufficient privileges' : audit.text?.text
|
|
29
|
+
return <div {...rest}>{auditTitle}</div>
|
|
30
|
+
}
|
|
31
|
+
default:
|
|
32
|
+
return <></>
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
37
|
+
domainResource?: Maybe<AuditEvent | QuestionnaireResponse | DocumentReference>
|
|
38
|
+
domainResourceType: TimelineDomainResourceType
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default TimelineTitle
|
package/src/constants.ts
ADDED
package/src/index.tsx
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import Timeline, { ITimelineFilter, ITimelineFilterOption } from './organisms/timeline'
|
|
2
|
+
import { ITimelineItem } from './molecules/timeline-item'
|
|
3
|
+
import PRIMARY_AUTHOR from './constants'
|
|
4
|
+
|
|
5
|
+
export default Timeline
|
|
6
|
+
export { ITimelineItem, ITimelineFilter, ITimelineFilterOption, PRIMARY_AUTHOR }
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { FC } from 'react'
|
|
2
|
+
import styled from '@emotion/styled'
|
|
3
|
+
import { HIGHLIGHT_GREEN, TRANSLUCENT_DARK_BLUE } from '@ltht-react/styles'
|
|
4
|
+
import { useWindowSize } from '@ltht-react/hooks'
|
|
5
|
+
import { isMobileView } from '@ltht-react/utils'
|
|
6
|
+
import TimelineTitleRedacted from '../atoms/timeline-title-redacted'
|
|
7
|
+
|
|
8
|
+
const StyledTimelineItem = styled.div<IStyledTimelineItem>`
|
|
9
|
+
background-color: ${({ isSelected }) => (isSelected ? HIGHLIGHT_GREEN.VALUE : TRANSLUCENT_DARK_BLUE)};
|
|
10
|
+
padding: 0.5rem;
|
|
11
|
+
`
|
|
12
|
+
const StyledTimelineItemTop = styled.div`
|
|
13
|
+
display: flex;
|
|
14
|
+
color: black;
|
|
15
|
+
padding-bottom: 0.25rem;
|
|
16
|
+
`
|
|
17
|
+
const StyledTitle = styled.div<IStyledMobile>`
|
|
18
|
+
flex-grow: 1;
|
|
19
|
+
color: black;
|
|
20
|
+
font-size: ${({ isMobile }) => (isMobile ? 'medium' : 'large')};
|
|
21
|
+
font-weight: bold;
|
|
22
|
+
`
|
|
23
|
+
|
|
24
|
+
const TimelineItemRedacted: FC = () => {
|
|
25
|
+
const { width } = useWindowSize()
|
|
26
|
+
const isMobile = isMobileView(width)
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<StyledTimelineItem isSelected={false}>
|
|
30
|
+
<StyledTimelineItemTop>
|
|
31
|
+
<StyledTitle isMobile={isMobile}>
|
|
32
|
+
<TimelineTitleRedacted />
|
|
33
|
+
</StyledTitle>
|
|
34
|
+
</StyledTimelineItemTop>
|
|
35
|
+
</StyledTimelineItem>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface IStyledTimelineItem {
|
|
40
|
+
isSelected: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface IStyledMobile {
|
|
44
|
+
isMobile: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default TimelineItemRedacted
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { FC } from 'react'
|
|
2
|
+
import styled from '@emotion/styled'
|
|
3
|
+
import { HIGHLIGHT_GREEN, TRANSLUCENT_DARK_BLUE } from '@ltht-react/styles'
|
|
4
|
+
import {
|
|
5
|
+
AuditEvent,
|
|
6
|
+
DocumentReference,
|
|
7
|
+
Maybe,
|
|
8
|
+
QuestionnaireResponse,
|
|
9
|
+
TimelineDomainResourceType,
|
|
10
|
+
} from '@ltht-react/types'
|
|
11
|
+
import { useWindowSize } from '@ltht-react/hooks'
|
|
12
|
+
import { isMobileView } from '@ltht-react/utils'
|
|
13
|
+
import TimelineDescription from '../atoms/timeline-description'
|
|
14
|
+
import TimelineAuthor from '../atoms/timeline-author'
|
|
15
|
+
import TimelineTitle from '../atoms/timeline-title'
|
|
16
|
+
import TimelineTime from '../atoms/timeline-time'
|
|
17
|
+
import TimelineButton from '../atoms/timeline-button'
|
|
18
|
+
|
|
19
|
+
const StyledTimelineItem = styled.div<IStyledTimelineItem>`
|
|
20
|
+
background-color: ${({ isSelected }) => (isSelected ? HIGHLIGHT_GREEN.VALUE : TRANSLUCENT_DARK_BLUE)};
|
|
21
|
+
padding-top: 0.5rem;
|
|
22
|
+
`
|
|
23
|
+
|
|
24
|
+
const StyledTimelineAuthor = styled(TimelineAuthor)`
|
|
25
|
+
flex-grow: 1;
|
|
26
|
+
margin-bottom: 0.5rem;
|
|
27
|
+
`
|
|
28
|
+
|
|
29
|
+
const StyledTimelineTime = styled(TimelineTime)`
|
|
30
|
+
color: black;
|
|
31
|
+
`
|
|
32
|
+
|
|
33
|
+
const StyledTimelineItemTop = styled.div`
|
|
34
|
+
display: flex;
|
|
35
|
+
color: black;
|
|
36
|
+
padding-bottom: 0.25rem;
|
|
37
|
+
margin: 0.5rem;
|
|
38
|
+
`
|
|
39
|
+
|
|
40
|
+
const StyledTimelineItemMiddle = styled.div`
|
|
41
|
+
color: black;
|
|
42
|
+
padding-bottom: 1rem;
|
|
43
|
+
margin: 0.5rem;
|
|
44
|
+
`
|
|
45
|
+
|
|
46
|
+
const StyledTimelineItemBottom = styled.div`
|
|
47
|
+
color: grey;
|
|
48
|
+
display: flex;
|
|
49
|
+
margin: 0.5rem;
|
|
50
|
+
`
|
|
51
|
+
|
|
52
|
+
const StyledTimelineTitle = styled(TimelineTitle, {
|
|
53
|
+
shouldForwardProp: (prop) => prop !== 'isMobile',
|
|
54
|
+
})<IStyledMobile>`
|
|
55
|
+
flex-grow: 1;
|
|
56
|
+
color: black;
|
|
57
|
+
font-size: ${({ isMobile }) => (isMobile ? 'medium' : 'large')};
|
|
58
|
+
font-weight: bold;
|
|
59
|
+
`
|
|
60
|
+
|
|
61
|
+
const StyledTimelineDescription = styled(TimelineDescription)`
|
|
62
|
+
color: black;
|
|
63
|
+
font-size: small;
|
|
64
|
+
`
|
|
65
|
+
|
|
66
|
+
const TimelineItem: FC<IProps> = ({ timelineItem, domainResourceType }) => {
|
|
67
|
+
const { width } = useWindowSize()
|
|
68
|
+
const isMobile = isMobileView(width)
|
|
69
|
+
|
|
70
|
+
if (!timelineItem?.domainResource) {
|
|
71
|
+
return <></>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const { domainResource, buttonState } = timelineItem
|
|
75
|
+
|
|
76
|
+
const itemKey = `timelineItem_${domainResource.id}`
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<StyledTimelineItem isSelected={buttonState === 'selected-button'} key={itemKey}>
|
|
80
|
+
<StyledTimelineItemTop>
|
|
81
|
+
<StyledTimelineTitle
|
|
82
|
+
isMobile={isMobile}
|
|
83
|
+
domainResource={domainResource}
|
|
84
|
+
domainResourceType={domainResourceType}
|
|
85
|
+
/>
|
|
86
|
+
{isMobile && (
|
|
87
|
+
<StyledTimelineTime domainResource={timelineItem.domainResource} domainResourceType={domainResourceType} />
|
|
88
|
+
)}
|
|
89
|
+
</StyledTimelineItemTop>
|
|
90
|
+
|
|
91
|
+
<StyledTimelineItemMiddle>
|
|
92
|
+
<StyledTimelineDescription
|
|
93
|
+
domainResource={timelineItem.domainResource}
|
|
94
|
+
domainResourceType={domainResourceType}
|
|
95
|
+
/>
|
|
96
|
+
</StyledTimelineItemMiddle>
|
|
97
|
+
|
|
98
|
+
<StyledTimelineItemBottom>
|
|
99
|
+
<StyledTimelineAuthor domainResource={timelineItem.domainResource} domainResourceType={domainResourceType} />
|
|
100
|
+
</StyledTimelineItemBottom>
|
|
101
|
+
|
|
102
|
+
<TimelineButton timelineItem={timelineItem} />
|
|
103
|
+
</StyledTimelineItem>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface IProps {
|
|
108
|
+
timelineItem: Maybe<ITimelineItem>
|
|
109
|
+
domainResourceType: TimelineDomainResourceType
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface ITimelineItem {
|
|
113
|
+
domainResource?: Maybe<AuditEvent | QuestionnaireResponse | DocumentReference>
|
|
114
|
+
buttonState: TimeLineItemButtonState
|
|
115
|
+
clickHandler?(): void
|
|
116
|
+
buttonText?: string
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
type TimeLineItemButtonState = 'no-button' | 'selectable-button' | 'selected-button' | 'permission-denied-button'
|
|
120
|
+
|
|
121
|
+
interface IStyledTimelineItem {
|
|
122
|
+
isSelected: boolean
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface IStyledMobile {
|
|
126
|
+
isMobile: boolean
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export default TimelineItem
|
|
@@ -0,0 +1,415 @@
|
|
|
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
|
+
/>
|
|
352
|
+
</StyledTimelineDayTimeRight>
|
|
353
|
+
</div>
|
|
354
|
+
)
|
|
355
|
+
} else if (position % 2 === 0) {
|
|
356
|
+
const contentKey = `content_right_${itemKey}`
|
|
357
|
+
content = (
|
|
358
|
+
<div key={contentKey} data-testid={contentKey}>
|
|
359
|
+
<StyledTimelineDayTimeLeft>
|
|
360
|
+
<TimelineTime
|
|
361
|
+
domainResource={timelineItem?.domainResource}
|
|
362
|
+
domainResourceType={domainResourceType}
|
|
363
|
+
/>
|
|
364
|
+
</StyledTimelineDayTimeLeft>
|
|
365
|
+
<StyledTimelineDayLine>
|
|
366
|
+
<StyledOuterCircle type="circle" color="info-blue" size="medium" />
|
|
367
|
+
<StyledInnerCircle type="circle" color="info-blue" size="medium" />
|
|
368
|
+
</StyledTimelineDayLine>
|
|
369
|
+
<StyledTimelineDayContent isMobile={false}>
|
|
370
|
+
{isRedacted ? (
|
|
371
|
+
<TimelineItemRedacted />
|
|
372
|
+
) : (
|
|
373
|
+
<TimelineItem timelineItem={timelineItem} domainResourceType={domainResourceType} />
|
|
374
|
+
)}
|
|
375
|
+
</StyledTimelineDayContent>
|
|
376
|
+
</div>
|
|
377
|
+
)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return (
|
|
381
|
+
<StyledTimelineDayItem isMobile={isMobile} key={itemKey} data-testid={itemKey}>
|
|
382
|
+
{content}
|
|
383
|
+
</StyledTimelineDayItem>
|
|
384
|
+
)
|
|
385
|
+
})}
|
|
386
|
+
</StyledTimelineDayBody>
|
|
387
|
+
</div>
|
|
388
|
+
)
|
|
389
|
+
})}
|
|
390
|
+
</StyledTimeline>
|
|
391
|
+
)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
interface IProps {
|
|
395
|
+
timelineItems: Maybe<ITimelineItem>[]
|
|
396
|
+
domainResourceType: TimelineDomainResourceType
|
|
397
|
+
filters?: ITimelineFilter[]
|
|
398
|
+
onFilterChange?: (value: string[]) => void
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
interface IStyledMobile {
|
|
402
|
+
isMobile: boolean
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export interface ITimelineFilter {
|
|
406
|
+
label: string
|
|
407
|
+
options: ITimelineFilterOption[]
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export interface ITimelineFilterOption {
|
|
411
|
+
value?: string
|
|
412
|
+
label: string
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export default Timeline
|