@stack-spot/portal-components 2.27.0 → 2.27.2

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.
Files changed (250) hide show
  1. package/CHANGELOG.md +635 -621
  2. package/dist/components/AnimatedHeight.d.ts +1 -1
  3. package/dist/components/AnimatedHeight.js +26 -26
  4. package/dist/components/AsyncContent.d.ts +1 -1
  5. package/dist/components/AsyncContent.js +1 -1
  6. package/dist/components/BannerWarning.d.ts +1 -1
  7. package/dist/components/BannerWarning.js +1 -1
  8. package/dist/components/Breadcrumb/index.d.ts +2 -2
  9. package/dist/components/Breadcrumb/index.js +1 -1
  10. package/dist/components/Breadcrumb/styled.js +31 -31
  11. package/dist/components/ButtonLoading.d.ts +1 -1
  12. package/dist/components/ButtonLoading.js +1 -1
  13. package/dist/components/ChatBot.d.ts +1 -1
  14. package/dist/components/ChatBot.js +1 -1
  15. package/dist/components/ContentValidateFilter.d.ts +1 -1
  16. package/dist/components/ContentValidateFilter.js +1 -1
  17. package/dist/components/FadingOverflow.d.ts +1 -1
  18. package/dist/components/FadingOverflow.js +69 -69
  19. package/dist/components/FileTreeView/More.d.ts +1 -1
  20. package/dist/components/FileTreeView/More.js +1 -1
  21. package/dist/components/FileTreeView/index.d.ts +1 -1
  22. package/dist/components/FileTreeView/index.js +1 -1
  23. package/dist/components/InfiniteScroll.d.ts +1 -1
  24. package/dist/components/InfiniteScroll.js +1 -1
  25. package/dist/components/InfoMaintenanceBanner.d.ts +1 -1
  26. package/dist/components/InfoMaintenanceBanner.js +2 -2
  27. package/dist/components/LazyMarkdown/BlockquoteMd.d.ts +1 -1
  28. package/dist/components/LazyMarkdown/BlockquoteMd.js +1 -1
  29. package/dist/components/LazyMarkdown/CodeViewer.d.ts +1 -1
  30. package/dist/components/LazyMarkdown/CodeViewer.js +76 -76
  31. package/dist/components/LazyMarkdown/Markdown.d.ts +1 -1
  32. package/dist/components/LazyMarkdown/Markdown.js +1 -1
  33. package/dist/components/LazyMarkdown/MarkdownButton.d.ts +1 -1
  34. package/dist/components/LazyMarkdown/MarkdownButton.js +1 -1
  35. package/dist/components/LazyMarkdown/Video.d.ts +1 -1
  36. package/dist/components/LazyMarkdown/Video.js +1 -1
  37. package/dist/components/LazyMarkdown/index.d.ts +1 -1
  38. package/dist/components/LazyMarkdown/index.js +1 -1
  39. package/dist/components/Placeholder.d.ts +7 -3
  40. package/dist/components/Placeholder.d.ts.map +1 -1
  41. package/dist/components/Placeholder.js +3 -3
  42. package/dist/components/Placeholder.js.map +1 -1
  43. package/dist/components/ScrollView.js +16 -16
  44. package/dist/components/Select/BadgeItem.d.ts +1 -1
  45. package/dist/components/Select/BadgeItem.js +1 -1
  46. package/dist/components/Select/ClearInput.d.ts +1 -1
  47. package/dist/components/Select/ClearInput.js +1 -1
  48. package/dist/components/Select/CloseItem.d.ts +1 -1
  49. package/dist/components/Select/CloseItem.js +1 -1
  50. package/dist/components/Select/CreatableSelect.js +1 -1
  51. package/dist/components/Select/CustomMenu.d.ts +1 -1
  52. package/dist/components/Select/CustomMenu.js +1 -1
  53. package/dist/components/Select/LabelItem.d.ts +1 -1
  54. package/dist/components/Select/LabelItem.js +1 -1
  55. package/dist/components/Select/MultiValue.d.ts +1 -1
  56. package/dist/components/Select/MultiValue.js +1 -1
  57. package/dist/components/Select/SelectInfiniteScroll.d.ts +1 -1
  58. package/dist/components/Select/SelectInfiniteScroll.js +1 -1
  59. package/dist/components/Select/SelectSearch.d.ts +1 -1
  60. package/dist/components/Select/SelectSearch.js +1 -1
  61. package/dist/components/SelectionList.d.ts +1 -1
  62. package/dist/components/SelectionList.js +61 -61
  63. package/dist/components/StatusCircle.d.ts +1 -1
  64. package/dist/components/StatusCircle.js +6 -6
  65. package/dist/components/Stepper/Navigation.js +4 -4
  66. package/dist/components/Stepper/Step.js +3 -3
  67. package/dist/components/Stepper/Stepper.js +6 -6
  68. package/dist/components/Stepper/headers.js +22 -22
  69. package/dist/components/Table/HeaderItem.js +1 -1
  70. package/dist/components/Table/SettingsVerticalMenu.d.ts +1 -1
  71. package/dist/components/Table/SettingsVerticalMenu.js +1 -1
  72. package/dist/components/Table/StyledLinkTable.d.ts +1 -1
  73. package/dist/components/Table/StyledLinkTable.js +5 -5
  74. package/dist/components/Table/TableData.d.ts +1 -1
  75. package/dist/components/Table/TableData.js +25 -25
  76. package/dist/components/TimelineSection.d.ts +1 -1
  77. package/dist/components/TimelineSection.js +14 -14
  78. package/dist/components/error/ErrorFeedback.d.ts +1 -1
  79. package/dist/components/error/ErrorFeedback.js +35 -35
  80. package/dist/components/error/NotFound.d.ts +1 -1
  81. package/dist/components/error/NotFound.js +1 -1
  82. package/dist/components/error/UnderMaintenance.d.ts +1 -1
  83. package/dist/components/error/UnderMaintenance.js +1 -1
  84. package/dist/components/form/Form/Form.d.ts +1 -1
  85. package/dist/components/form/Form/Form.js +1 -1
  86. package/dist/components/form/Form/FormGroup.d.ts +2 -2
  87. package/dist/components/form/Form/FormGroup.js +1 -1
  88. package/dist/components/form/SearchInput.d.ts +1 -1
  89. package/dist/components/form/SearchInput.js +1 -1
  90. package/dist/components/form/Select/CustomSelect.d.ts +1 -1
  91. package/dist/components/form/Select/CustomSelect.js +1 -1
  92. package/dist/components/form/Select/DetailedSelect.d.ts +1 -1
  93. package/dist/components/form/Select/DetailedSelect.js +1 -1
  94. package/dist/components/form/Select/Select.d.ts +1 -1
  95. package/dist/components/form/Select/Select.js +1 -1
  96. package/dist/components/form/Select/styled.js +161 -161
  97. package/dist/components/form/Select/utils.js +1 -1
  98. package/dist/components/notification/NotificationComponent.d.ts +1 -1
  99. package/dist/components/notification/NotificationComponent.js +54 -54
  100. package/dist/components/notification/NotificationItem.d.ts +1 -1
  101. package/dist/components/notification/NotificationItem.d.ts.map +1 -1
  102. package/dist/components/notification/NotificationItem.js +11 -5
  103. package/dist/components/notification/NotificationItem.js.map +1 -1
  104. package/dist/components/notification/NotificationList.d.ts +1 -1
  105. package/dist/components/notification/NotificationList.d.ts.map +1 -1
  106. package/dist/components/notification/NotificationList.js +44 -44
  107. package/dist/components/notification/NotificationList.js.map +1 -1
  108. package/dist/components/notification/NotificationPlaceholder.d.ts +1 -1
  109. package/dist/components/notification/NotificationPlaceholder.d.ts.map +1 -1
  110. package/dist/components/notification/NotificationPlaceholder.js +2 -2
  111. package/dist/components/notification/NotificationPlaceholder.js.map +1 -1
  112. package/dist/containers/NotificationsPage.d.ts +1 -1
  113. package/dist/containers/NotificationsPage.d.ts.map +1 -1
  114. package/dist/containers/NotificationsPage.js +24 -11
  115. package/dist/containers/NotificationsPage.js.map +1 -1
  116. package/dist/context/anchor.d.ts +1 -1
  117. package/dist/context/anchor.js +1 -1
  118. package/dist/context/loading.d.ts +1 -1
  119. package/dist/context/loading.js +1 -1
  120. package/dist/context/notification/context.d.ts +1 -1
  121. package/dist/context/notification/context.js +1 -1
  122. package/dist/context/notification/types.d.ts +1 -0
  123. package/dist/context/notification/types.d.ts.map +1 -1
  124. package/dist/hooks/date.js +1 -1
  125. package/dist/hooks/service-now.js +28 -28
  126. package/dist/svg/AI.d.ts +1 -1
  127. package/dist/svg/AI.js +1 -1
  128. package/dist/svg/CS.d.ts +1 -1
  129. package/dist/svg/CS.js +1 -1
  130. package/dist/svg/EDP.d.ts +1 -1
  131. package/dist/svg/EDP.js +1 -1
  132. package/dist/svg/Forbidden.d.ts +1 -1
  133. package/dist/svg/Forbidden.js +1 -1
  134. package/dist/svg/GenericPlaceholder.d.ts +4 -2
  135. package/dist/svg/GenericPlaceholder.d.ts.map +1 -1
  136. package/dist/svg/GenericPlaceholder.js +2 -2
  137. package/dist/svg/GenericPlaceholder.js.map +1 -1
  138. package/dist/svg/HUB.d.ts +1 -1
  139. package/dist/svg/HUB.js +1 -1
  140. package/dist/svg/Logo.d.ts +1 -1
  141. package/dist/svg/Logo.js +1 -1
  142. package/dist/svg/MiniLogo.d.ts +1 -1
  143. package/dist/svg/MiniLogo.js +1 -1
  144. package/dist/svg/NotFound.d.ts +1 -1
  145. package/dist/svg/NotFound.js +1 -1
  146. package/dist/svg/ServerError.d.ts +1 -1
  147. package/dist/svg/ServerError.js +1 -1
  148. package/dist/svg/Unauthenticated.d.ts +1 -1
  149. package/dist/svg/Unauthenticated.js +1 -1
  150. package/package.json +6 -6
  151. package/readme.md +66 -66
  152. package/src/components/AnimatedHeight.tsx +174 -174
  153. package/src/components/AsyncContent.tsx +78 -78
  154. package/src/components/BannerWarning.tsx +91 -91
  155. package/src/components/Breadcrumb/index.tsx +76 -76
  156. package/src/components/Breadcrumb/styled.ts +37 -37
  157. package/src/components/ButtonLoading.tsx +29 -29
  158. package/src/components/ChatBot.tsx +82 -82
  159. package/src/components/ContentValidateFilter.tsx +15 -15
  160. package/src/components/FadingOverflow.tsx +265 -265
  161. package/src/components/FileTreeView/More.tsx +114 -114
  162. package/src/components/FileTreeView/index.tsx +186 -186
  163. package/src/components/InfiniteScroll.tsx +24 -24
  164. package/src/components/InfoMaintenanceBanner.tsx +29 -29
  165. package/src/components/LazyMarkdown/BlockquoteMd.tsx +107 -107
  166. package/src/components/LazyMarkdown/CodeViewer.tsx +161 -161
  167. package/src/components/LazyMarkdown/Markdown.tsx +122 -122
  168. package/src/components/LazyMarkdown/MarkdownButton.tsx +24 -24
  169. package/src/components/LazyMarkdown/Video.tsx +13 -13
  170. package/src/components/LazyMarkdown/index.tsx +21 -21
  171. package/src/components/Placeholder.tsx +123 -118
  172. package/src/components/ScrollView.tsx +57 -57
  173. package/src/components/Select/BadgeItem.tsx +58 -58
  174. package/src/components/Select/ClearInput.tsx +24 -24
  175. package/src/components/Select/CloseItem.tsx +38 -38
  176. package/src/components/Select/CreatableSelect.tsx +155 -155
  177. package/src/components/Select/CustomMenu.tsx +16 -16
  178. package/src/components/Select/LabelItem.tsx +8 -8
  179. package/src/components/Select/MultiValue.tsx +49 -49
  180. package/src/components/Select/SelectInfiniteScroll.tsx +82 -82
  181. package/src/components/Select/SelectSearch.tsx +195 -195
  182. package/src/components/Select/index.tsx +7 -7
  183. package/src/components/Select/types.ts +8 -8
  184. package/src/components/SelectionList.tsx +427 -427
  185. package/src/components/StatusCircle.tsx +67 -67
  186. package/src/components/Stepper/Navigation.tsx +97 -97
  187. package/src/components/Stepper/Step.tsx +30 -30
  188. package/src/components/Stepper/Stepper.tsx +113 -113
  189. package/src/components/Stepper/headers.tsx +64 -64
  190. package/src/components/Stepper/index.ts +3 -3
  191. package/src/components/Table/HeaderItem.tsx +52 -52
  192. package/src/components/Table/SettingsVerticalMenu.tsx +50 -50
  193. package/src/components/Table/StyledLinkTable.tsx +22 -22
  194. package/src/components/Table/TableData.tsx +251 -251
  195. package/src/components/Table/index.tsx +2 -2
  196. package/src/components/TimelineSection.tsx +66 -66
  197. package/src/components/error/ErrorFeedback.tsx +217 -217
  198. package/src/components/error/NotFound.tsx +24 -24
  199. package/src/components/error/UnderMaintenance.tsx +30 -30
  200. package/src/components/error/index.ts +4 -4
  201. package/src/components/form/Form/Form.tsx +101 -101
  202. package/src/components/form/Form/FormGroup.tsx +221 -221
  203. package/src/components/form/Form/index.ts +2 -2
  204. package/src/components/form/SearchInput.tsx +69 -69
  205. package/src/components/form/Select/CustomSelect.tsx +232 -232
  206. package/src/components/form/Select/DetailedSelect.tsx +85 -85
  207. package/src/components/form/Select/Select.tsx +67 -67
  208. package/src/components/form/Select/index.ts +4 -4
  209. package/src/components/form/Select/styled.ts +165 -165
  210. package/src/components/form/Select/types.ts +112 -112
  211. package/src/components/form/Select/utils.tsx +28 -28
  212. package/src/components/notification/NotificationComponent.tsx +340 -340
  213. package/src/components/notification/NotificationItem.tsx +345 -336
  214. package/src/components/notification/NotificationList.tsx +179 -178
  215. package/src/components/notification/NotificationPlaceholder.tsx +44 -43
  216. package/src/components/notification/types.ts +72 -72
  217. package/src/containers/NotificationsPage.tsx +119 -98
  218. package/src/context/anchor.tsx +37 -37
  219. package/src/context/loading.tsx +36 -36
  220. package/src/context/notification/LazyNotificationList.ts +103 -103
  221. package/src/context/notification/NotificationController.ts +104 -104
  222. package/src/context/notification/context.tsx +23 -23
  223. package/src/context/notification/hooks.ts +98 -98
  224. package/src/context/notification/types.ts +66 -65
  225. package/src/hooks/date.ts +31 -31
  226. package/src/hooks/keyboard.tsx +128 -128
  227. package/src/hooks/manual-render.tsx +10 -10
  228. package/src/hooks/service-now.tsx +233 -233
  229. package/src/hooks/text.tsx +30 -30
  230. package/src/hooks/title.tsx +28 -28
  231. package/src/hooks/use-effect-once.tsx +43 -43
  232. package/src/index.ts +19 -19
  233. package/src/notifications.ts +11 -11
  234. package/src/svg/AI.tsx +41 -41
  235. package/src/svg/CS.tsx +48 -48
  236. package/src/svg/EDP.tsx +31 -31
  237. package/src/svg/Forbidden.tsx +22 -22
  238. package/src/svg/GenericPlaceholder.tsx +20 -20
  239. package/src/svg/HUB.tsx +48 -48
  240. package/src/svg/Logo.tsx +16 -16
  241. package/src/svg/MiniLogo.tsx +12 -12
  242. package/src/svg/NotFound.tsx +16 -16
  243. package/src/svg/ServerError.tsx +33 -33
  244. package/src/svg/Unauthenticated.tsx +16 -16
  245. package/src/svg/index.ts +11 -11
  246. package/src/utils/accessibility.ts +135 -135
  247. package/src/utils/cookie.ts +73 -73
  248. package/src/utils/promise.ts +5 -5
  249. package/src/utils/read-file.ts +16 -16
  250. package/tsconfig.json +10 -10
@@ -1,340 +1,340 @@
1
- import { Box, Button, Flex, IconBox, Text } from '@citric/core'
2
- import { Bell, TimesMini } from '@citric/icons'
3
- import { IconButton } from '@citric/ui'
4
- import { listToClass, theme } from '@stack-spot/portal-theme'
5
- import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
6
- import { ReactElement, useLayoutEffect, useRef, useState } from 'react'
7
- import styled from 'styled-components'
8
- import { AsyncContent, ErrorProps } from '../AsyncContent'
9
- import { InfiniteScroll } from '../InfiniteScroll'
10
- import { ScrollView } from '../ScrollView'
11
- import { StatusCircle } from '../StatusCircle'
12
- import { NotificationItem } from './NotificationItem'
13
- import { GetTenantNotificationsResponse, NotificationTypeFilters, UnreadType } from './types'
14
-
15
- interface Props {
16
- hasUnreadNotification?: boolean,
17
- }
18
-
19
- const ANIMATION_DURATION_MS = 300
20
- const MAX_HEIGHT_TRANSITION = `max-height ease-in ${ANIMATION_DURATION_MS / 1000}s`
21
-
22
- const NotificationsComponent = styled.div<{ $scroll?: boolean }>`
23
- max-height: 0;
24
- z-index: 2;
25
- visibility: hidden;
26
- position: absolute;
27
- top: calc(var(--header-height) + 4px);
28
- right: -270%;
29
- opacity: 0;
30
- width: 400px;
31
- transition: ${MAX_HEIGHT_TRANSITION}, opacity ${ANIMATION_DURATION_MS}ms ease-in-out, visibility 0s ${ANIMATION_DURATION_MS}ms;
32
-
33
- &.visible {
34
- visibility: visible;
35
- min-height: 400px;
36
- opacity: 1;
37
- transition: ${MAX_HEIGHT_TRANSITION}, opacity ${ANIMATION_DURATION_MS}ms ease-in-out;
38
- }
39
-
40
- .content {
41
- border-radius: 4px;
42
- border: 1px solid ${theme.color.light[400]};
43
- box-shadow: 4px 4px 48px ${theme.color.danger.contrastText};
44
- background-color: ${theme.color.light[300]};
45
- overflow-y: ${({ $scroll }) => ($scroll ? 'auto' : 'hidden')};
46
- overflow-x: hidden;
47
- }
48
-
49
- &::after {
50
- content: '';
51
- position: absolute;
52
- border-width: 8px 32px;
53
- border-style: solid;
54
- border-color: transparent;
55
- bottom: 100%;
56
- left: 74%;
57
- margin-left: -5px;
58
- transform: rotate(180deg);
59
- border-top-color: ${theme.color.light[400]};
60
- }
61
- `
62
-
63
- const Overlay = styled.div`
64
- position: fixed;
65
- top: 0;
66
- left: 0;
67
- width: 100%;
68
- height: 100%;
69
- background-color: ${theme.color.primary.contrastText};
70
- opacity: 0.6;
71
- z-index: 1;
72
- `
73
-
74
- const StyledBox = styled(Box)`
75
- width: 100%;
76
- > div:first-child{
77
- width: 100%;
78
- }
79
- `
80
-
81
- interface NotificationsFilterProps {
82
- type?: NotificationTypeFilters,
83
- onChangeFilterType: (type?: NotificationTypeFilters) => void,
84
- }
85
-
86
- interface NotificationFilterButtonProps extends NotificationsFilterProps {
87
- ariaLabel: string,
88
- label: string,
89
- enumType: NotificationTypeFilters,
90
- }
91
-
92
- /**
93
- * NotificationFilterButton component that renders a button to be used in quick filters for notifications.
94
- *
95
- * @param props the component's props {@link NotificationFilterButtonProps}.
96
- */
97
- const NotificationFilterButton = (props: NotificationFilterButtonProps) => {
98
- const { type, onChangeFilterType, ariaLabel, label, enumType } = props
99
- return (<Button
100
- appearance="text"
101
- role="button"
102
- aria-label={ariaLabel}
103
- aria-pressed={type === enumType}
104
- onClick={() => onChangeFilterType(enumType)}
105
- sx={{ borderColor: type === enumType ? 'primary' : 'transparent' }}
106
- >
107
- <Text colorScheme="inverse" appearance="microtext1">
108
- {label}
109
- </Text>
110
- </Button>
111
- )
112
- }
113
-
114
- /**
115
- * NotificationsFilter component that renders the filter options for notifications.
116
- *
117
- * @param props the component's props {@link NotificationsFilterProps}.
118
- */
119
- const NotificationsFilter = ({ type, onChangeFilterType }: NotificationsFilterProps) => {
120
- const t = useTranslate(dictionary)
121
-
122
- return (<Flex alignItems="center" sx={{ gap: '4px' }} my="5">
123
- <Button
124
- aria-pressed={!type}
125
- appearance="text"
126
- role="button"
127
- aria-label={t.filterAll}
128
- onClick={() => onChangeFilterType()}
129
- sx={{ borderColor: !type ? 'primary' : 'transparent' }}
130
- >
131
- <Text colorScheme="inverse" appearance="microtext1">
132
- {t.all}
133
- </Text>
134
- </Button>
135
- <NotificationFilterButton
136
- type={type} onChangeFilterType={onChangeFilterType}
137
- ariaLabel={t.filterUnread} label={t.unread} enumType={UnreadType.Unread}
138
- />
139
- <NotificationFilterButton
140
- type={type} onChangeFilterType={onChangeFilterType}
141
- ariaLabel={t.filterHigh} label={t.high} enumType={'HIGH'}
142
- />
143
- <NotificationFilterButton
144
- type={type} onChangeFilterType={onChangeFilterType}
145
- ariaLabel={t.filterMedium} label={t.medium} enumType={'MEDIUM'}
146
- />
147
- <NotificationFilterButton
148
- type={type} onChangeFilterType={onChangeFilterType}
149
- ariaLabel={t.filterLow} label={t.low} enumType={'LOW'}
150
- />
151
- </Flex>)
152
- }
153
-
154
- interface Props {
155
- onMarkAsReadUnread: (notificationId: string, read: boolean, type: 'callToAction' | 'icon') => void,
156
- notifications?: GetTenantNotificationsResponse[],
157
- isLoading: boolean,
158
- error?: any,
159
- onClickViewNotifications: () => void,
160
- onClickViewAll: () => void,
161
- errorDetails: ErrorProps,
162
- fetchNextPage: () => void,
163
- hasNextPage: boolean,
164
- type?: NotificationTypeFilters,
165
- onUpdateType: (updatedType?: NotificationTypeFilters) => void,
166
- placeholderComponent: ReactElement,
167
- isSummary: boolean,
168
- }
169
-
170
- // fixme: remove this component in the next major
171
- /**
172
- * NotificationComponent component that renders the notifications panel.
173
- * It renders the notification icon and when clicked the notification modal is opened.
174
- *
175
- * @param props the component's props {@link Props}.
176
- * @deprecated this functionality has been moved to the Layout library. This is now a property of the Header.
177
- */
178
- export const NotificationComponent = ({
179
- hasUnreadNotification, onMarkAsReadUnread, notifications, isLoading, error,
180
- type, onUpdateType,
181
- onClickViewNotifications, onClickViewAll, errorDetails, fetchNextPage, hasNextPage,
182
- placeholderComponent, isSummary = false,
183
- }: Props) => {
184
- const t = useTranslate(dictionary)
185
- const [visible, setVisible] = useState(false)
186
- const seeAllButtonRef = useRef<HTMLButtonElement>(null)
187
-
188
- const updateType = (updatedType?: NotificationTypeFilters) => {
189
- onUpdateType(updatedType)
190
- }
191
-
192
- useLayoutEffect(() => {
193
- const handleKeyDown = (event: KeyboardEvent) => {
194
- if (event.key === 'Escape') {
195
- const focusedElement = document.activeElement
196
- const notificationItems = document.querySelectorAll('[id^="notificationItem-"]')
197
- let isFocusedOnNotificationItem = false
198
-
199
- notificationItems.forEach((item) => {
200
- if (item.contains(focusedElement)) {
201
- isFocusedOnNotificationItem = true
202
- }
203
- })
204
-
205
- if (isFocusedOnNotificationItem) {
206
- seeAllButtonRef?.current?.focus()
207
- } else {
208
- setVisible(false)
209
- }
210
- }
211
- }
212
-
213
- document.addEventListener('keydown', handleKeyDown)
214
-
215
- return () => {
216
- document.removeEventListener('keydown', handleKeyDown)
217
- }
218
- }, [])
219
-
220
- return (<Flex sx={{ position: 'relative' }}>
221
- <IconButton aria-label={t.openNotifications} onClick={() => {
222
- onClickViewNotifications()
223
- setVisible(!visible)
224
- }} sx={{ border: 'none', bg: 'transparent' }} aria-expanded={visible}>
225
- <IconBox size="md"
226
- className="notificationsTour" >
227
- <Bell />
228
- </IconBox>
229
- </IconButton>
230
- {hasUnreadNotification && <Box sx={{ position: 'absolute', right: '2px' }} aria-label={t.hasUnread}>
231
- <StatusCircle status={'danger'} />
232
- </Box>}
233
-
234
- {visible && <Overlay onClick={() => setVisible(false)} />}
235
-
236
- <NotificationsComponent
237
- className={listToClass(['notification-list', visible ? 'visible' : undefined])}
238
- $scroll={true}
239
- aria-hidden={!visible}
240
- >
241
- <Flex className="content" p={5} flexDirection="column" justifyContent="space-between">
242
- <Flex w="100%">
243
- <Flex justifyContent="space-between" w="100%" >
244
- <Text appearance="h4">
245
- {t.notifications}
246
- </Text>
247
- <IconButton onClick={() => setVisible(false)} aria-label={t.close}>
248
- <IconBox size="xs">
249
- <TimesMini />
250
- </IconBox>
251
- </IconButton>
252
- </Flex>
253
-
254
- <NotificationsFilter type={type} onChangeFilterType={updateType} />
255
- <AsyncContent error={error} errorDetails={errorDetails} loading={isLoading}>
256
- {notifications?.length ? <StyledBox>
257
- <ScrollView id="scrollableNotifications" direction="vertical" style={{ maxHeight: 'calc(100vh - 300px)' }}>
258
- <InfiniteScroll
259
- dataLength={notifications?.length || 0}
260
- next={fetchNextPage}
261
- hasMore={hasNextPage}
262
- scrollableTarget="scrollableNotifications"
263
- >
264
- <Flex sx={{ gap: '4px' }} mr="3" flexDirection="column">
265
- {notifications?.map((item, index) => (
266
- <NotificationItem
267
- key={item.id}
268
- notification={item}
269
- id={`notificationItem-${index}`}
270
- isSummary={isSummary}
271
- onClickMarkReadUnread={(read, type) => onMarkAsReadUnread(item.id, read, type)}
272
- />
273
- ))}
274
- </Flex>
275
- </InfiniteScroll>
276
- </ScrollView>
277
- </StyledBox>
278
- :
279
- <>
280
- {placeholderComponent}
281
- </>
282
- }
283
- </AsyncContent>
284
- </Flex>
285
-
286
- <Flex w="100%" pt={3}>
287
- <Button
288
- ref={seeAllButtonRef}
289
- size="sm"
290
- sx={{ width: '100%' }} colorScheme="inverse" appearance="text"
291
- onClick={() => {
292
- setVisible(false)
293
- onClickViewAll()
294
- }}>
295
- <Text appearance="microtext1">
296
- {t.viewAll}
297
- </Text>
298
- </Button>
299
- </Flex>
300
- </Flex>
301
- </NotificationsComponent>
302
- </Flex >)
303
- }
304
-
305
- const dictionary = {
306
- en: {
307
- notifications: 'Notifications',
308
- all: 'All',
309
- filterAll: 'Filter all notifications',
310
- filterUnread: 'Filter unread notifications',
311
- filterHigh: 'Filter high notifications',
312
- filterMedium: 'Filter medium notifications',
313
- filterLow: 'Filter low notifications',
314
- unread: 'Unread',
315
- high: 'High',
316
- medium: 'Medium',
317
- low: 'Low',
318
- viewAll: 'View all notifications',
319
- openNotifications: 'View notifications',
320
- hasUnread: 'Has Unread notifications',
321
- close: 'Close',
322
- },
323
- pt: {
324
- notifications: 'Notificações',
325
- all: 'Todos',
326
- filterAll: 'Filtrar todas as notificações ',
327
- filterUnread: 'Filtrar notificações não lidas',
328
- filterHigh: 'Filtrar notificações criticidade alta',
329
- filterMedium: 'Filtrar notificações criticidade média',
330
- filterLow: 'Filtrar notificações criticidade baixa',
331
- unread: 'Não lidas',
332
- high: 'Alto',
333
- medium: 'Médio',
334
- low: 'Baixo',
335
- viewAll: 'Ver todas as notificações',
336
- openNotifications: 'Visualizar notificações',
337
- hasUnread: 'Existem notificações não lidas',
338
- close: 'Fechar',
339
- },
340
- } satisfies Dictionary
1
+ import { Box, Button, Flex, IconBox, Text } from '@citric/core'
2
+ import { Bell, TimesMini } from '@citric/icons'
3
+ import { IconButton } from '@citric/ui'
4
+ import { listToClass, theme } from '@stack-spot/portal-theme'
5
+ import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
6
+ import { ReactElement, useLayoutEffect, useRef, useState } from 'react'
7
+ import styled from 'styled-components'
8
+ import { AsyncContent, ErrorProps } from '../AsyncContent'
9
+ import { InfiniteScroll } from '../InfiniteScroll'
10
+ import { ScrollView } from '../ScrollView'
11
+ import { StatusCircle } from '../StatusCircle'
12
+ import { NotificationItem } from './NotificationItem'
13
+ import { GetTenantNotificationsResponse, NotificationTypeFilters, UnreadType } from './types'
14
+
15
+ interface Props {
16
+ hasUnreadNotification?: boolean,
17
+ }
18
+
19
+ const ANIMATION_DURATION_MS = 300
20
+ const MAX_HEIGHT_TRANSITION = `max-height ease-in ${ANIMATION_DURATION_MS / 1000}s`
21
+
22
+ const NotificationsComponent = styled.div<{ $scroll?: boolean }>`
23
+ max-height: 0;
24
+ z-index: 2;
25
+ visibility: hidden;
26
+ position: absolute;
27
+ top: calc(var(--header-height) + 4px);
28
+ right: -270%;
29
+ opacity: 0;
30
+ width: 400px;
31
+ transition: ${MAX_HEIGHT_TRANSITION}, opacity ${ANIMATION_DURATION_MS}ms ease-in-out, visibility 0s ${ANIMATION_DURATION_MS}ms;
32
+
33
+ &.visible {
34
+ visibility: visible;
35
+ min-height: 400px;
36
+ opacity: 1;
37
+ transition: ${MAX_HEIGHT_TRANSITION}, opacity ${ANIMATION_DURATION_MS}ms ease-in-out;
38
+ }
39
+
40
+ .content {
41
+ border-radius: 4px;
42
+ border: 1px solid ${theme.color.light[400]};
43
+ box-shadow: 4px 4px 48px ${theme.color.danger.contrastText};
44
+ background-color: ${theme.color.light[300]};
45
+ overflow-y: ${({ $scroll }) => ($scroll ? 'auto' : 'hidden')};
46
+ overflow-x: hidden;
47
+ }
48
+
49
+ &::after {
50
+ content: '';
51
+ position: absolute;
52
+ border-width: 8px 32px;
53
+ border-style: solid;
54
+ border-color: transparent;
55
+ bottom: 100%;
56
+ left: 74%;
57
+ margin-left: -5px;
58
+ transform: rotate(180deg);
59
+ border-top-color: ${theme.color.light[400]};
60
+ }
61
+ `
62
+
63
+ const Overlay = styled.div`
64
+ position: fixed;
65
+ top: 0;
66
+ left: 0;
67
+ width: 100%;
68
+ height: 100%;
69
+ background-color: ${theme.color.primary.contrastText};
70
+ opacity: 0.6;
71
+ z-index: 1;
72
+ `
73
+
74
+ const StyledBox = styled(Box)`
75
+ width: 100%;
76
+ > div:first-child{
77
+ width: 100%;
78
+ }
79
+ `
80
+
81
+ interface NotificationsFilterProps {
82
+ type?: NotificationTypeFilters,
83
+ onChangeFilterType: (type?: NotificationTypeFilters) => void,
84
+ }
85
+
86
+ interface NotificationFilterButtonProps extends NotificationsFilterProps {
87
+ ariaLabel: string,
88
+ label: string,
89
+ enumType: NotificationTypeFilters,
90
+ }
91
+
92
+ /**
93
+ * NotificationFilterButton component that renders a button to be used in quick filters for notifications.
94
+ *
95
+ * @param props the component's props {@link NotificationFilterButtonProps}.
96
+ */
97
+ const NotificationFilterButton = (props: NotificationFilterButtonProps) => {
98
+ const { type, onChangeFilterType, ariaLabel, label, enumType } = props
99
+ return (<Button
100
+ appearance="text"
101
+ role="button"
102
+ aria-label={ariaLabel}
103
+ aria-pressed={type === enumType}
104
+ onClick={() => onChangeFilterType(enumType)}
105
+ sx={{ borderColor: type === enumType ? 'primary' : 'transparent' }}
106
+ >
107
+ <Text colorScheme="inverse" appearance="microtext1">
108
+ {label}
109
+ </Text>
110
+ </Button>
111
+ )
112
+ }
113
+
114
+ /**
115
+ * NotificationsFilter component that renders the filter options for notifications.
116
+ *
117
+ * @param props the component's props {@link NotificationsFilterProps}.
118
+ */
119
+ const NotificationsFilter = ({ type, onChangeFilterType }: NotificationsFilterProps) => {
120
+ const t = useTranslate(dictionary)
121
+
122
+ return (<Flex alignItems="center" sx={{ gap: '4px' }} my="5">
123
+ <Button
124
+ aria-pressed={!type}
125
+ appearance="text"
126
+ role="button"
127
+ aria-label={t.filterAll}
128
+ onClick={() => onChangeFilterType()}
129
+ sx={{ borderColor: !type ? 'primary' : 'transparent' }}
130
+ >
131
+ <Text colorScheme="inverse" appearance="microtext1">
132
+ {t.all}
133
+ </Text>
134
+ </Button>
135
+ <NotificationFilterButton
136
+ type={type} onChangeFilterType={onChangeFilterType}
137
+ ariaLabel={t.filterUnread} label={t.unread} enumType={UnreadType.Unread}
138
+ />
139
+ <NotificationFilterButton
140
+ type={type} onChangeFilterType={onChangeFilterType}
141
+ ariaLabel={t.filterHigh} label={t.high} enumType={'HIGH'}
142
+ />
143
+ <NotificationFilterButton
144
+ type={type} onChangeFilterType={onChangeFilterType}
145
+ ariaLabel={t.filterMedium} label={t.medium} enumType={'MEDIUM'}
146
+ />
147
+ <NotificationFilterButton
148
+ type={type} onChangeFilterType={onChangeFilterType}
149
+ ariaLabel={t.filterLow} label={t.low} enumType={'LOW'}
150
+ />
151
+ </Flex>)
152
+ }
153
+
154
+ interface Props {
155
+ onMarkAsReadUnread: (notificationId: string, read: boolean, type: 'callToAction' | 'icon') => void,
156
+ notifications?: GetTenantNotificationsResponse[],
157
+ isLoading: boolean,
158
+ error?: any,
159
+ onClickViewNotifications: () => void,
160
+ onClickViewAll: () => void,
161
+ errorDetails: ErrorProps,
162
+ fetchNextPage: () => void,
163
+ hasNextPage: boolean,
164
+ type?: NotificationTypeFilters,
165
+ onUpdateType: (updatedType?: NotificationTypeFilters) => void,
166
+ placeholderComponent: ReactElement,
167
+ isSummary: boolean,
168
+ }
169
+
170
+ // fixme: remove this component in the next major
171
+ /**
172
+ * NotificationComponent component that renders the notifications panel.
173
+ * It renders the notification icon and when clicked the notification modal is opened.
174
+ *
175
+ * @param props the component's props {@link Props}.
176
+ * @deprecated this functionality has been moved to the Layout library. This is now a property of the Header.
177
+ */
178
+ export const NotificationComponent = ({
179
+ hasUnreadNotification, onMarkAsReadUnread, notifications, isLoading, error,
180
+ type, onUpdateType,
181
+ onClickViewNotifications, onClickViewAll, errorDetails, fetchNextPage, hasNextPage,
182
+ placeholderComponent, isSummary = false,
183
+ }: Props) => {
184
+ const t = useTranslate(dictionary)
185
+ const [visible, setVisible] = useState(false)
186
+ const seeAllButtonRef = useRef<HTMLButtonElement>(null)
187
+
188
+ const updateType = (updatedType?: NotificationTypeFilters) => {
189
+ onUpdateType(updatedType)
190
+ }
191
+
192
+ useLayoutEffect(() => {
193
+ const handleKeyDown = (event: KeyboardEvent) => {
194
+ if (event.key === 'Escape') {
195
+ const focusedElement = document.activeElement
196
+ const notificationItems = document.querySelectorAll('[id^="notificationItem-"]')
197
+ let isFocusedOnNotificationItem = false
198
+
199
+ notificationItems.forEach((item) => {
200
+ if (item.contains(focusedElement)) {
201
+ isFocusedOnNotificationItem = true
202
+ }
203
+ })
204
+
205
+ if (isFocusedOnNotificationItem) {
206
+ seeAllButtonRef?.current?.focus()
207
+ } else {
208
+ setVisible(false)
209
+ }
210
+ }
211
+ }
212
+
213
+ document.addEventListener('keydown', handleKeyDown)
214
+
215
+ return () => {
216
+ document.removeEventListener('keydown', handleKeyDown)
217
+ }
218
+ }, [])
219
+
220
+ return (<Flex sx={{ position: 'relative' }}>
221
+ <IconButton aria-label={t.openNotifications} onClick={() => {
222
+ onClickViewNotifications()
223
+ setVisible(!visible)
224
+ }} sx={{ border: 'none', bg: 'transparent' }} aria-expanded={visible}>
225
+ <IconBox size="md"
226
+ className="notificationsTour" >
227
+ <Bell />
228
+ </IconBox>
229
+ </IconButton>
230
+ {hasUnreadNotification && <Box sx={{ position: 'absolute', right: '2px' }} aria-label={t.hasUnread}>
231
+ <StatusCircle status={'danger'} />
232
+ </Box>}
233
+
234
+ {visible && <Overlay onClick={() => setVisible(false)} />}
235
+
236
+ <NotificationsComponent
237
+ className={listToClass(['notification-list', visible ? 'visible' : undefined])}
238
+ $scroll={true}
239
+ aria-hidden={!visible}
240
+ >
241
+ <Flex className="content" p={5} flexDirection="column" justifyContent="space-between">
242
+ <Flex w="100%">
243
+ <Flex justifyContent="space-between" w="100%" >
244
+ <Text appearance="h4">
245
+ {t.notifications}
246
+ </Text>
247
+ <IconButton onClick={() => setVisible(false)} aria-label={t.close}>
248
+ <IconBox size="xs">
249
+ <TimesMini />
250
+ </IconBox>
251
+ </IconButton>
252
+ </Flex>
253
+
254
+ <NotificationsFilter type={type} onChangeFilterType={updateType} />
255
+ <AsyncContent error={error} errorDetails={errorDetails} loading={isLoading}>
256
+ {notifications?.length ? <StyledBox>
257
+ <ScrollView id="scrollableNotifications" direction="vertical" style={{ maxHeight: 'calc(100vh - 300px)' }}>
258
+ <InfiniteScroll
259
+ dataLength={notifications?.length || 0}
260
+ next={fetchNextPage}
261
+ hasMore={hasNextPage}
262
+ scrollableTarget="scrollableNotifications"
263
+ >
264
+ <Flex sx={{ gap: '4px' }} mr="3" flexDirection="column">
265
+ {notifications?.map((item, index) => (
266
+ <NotificationItem
267
+ key={item.id}
268
+ notification={item}
269
+ id={`notificationItem-${index}`}
270
+ isSummary={isSummary}
271
+ onClickMarkReadUnread={(read, type) => onMarkAsReadUnread(item.id, read, type)}
272
+ />
273
+ ))}
274
+ </Flex>
275
+ </InfiniteScroll>
276
+ </ScrollView>
277
+ </StyledBox>
278
+ :
279
+ <>
280
+ {placeholderComponent}
281
+ </>
282
+ }
283
+ </AsyncContent>
284
+ </Flex>
285
+
286
+ <Flex w="100%" pt={3}>
287
+ <Button
288
+ ref={seeAllButtonRef}
289
+ size="sm"
290
+ sx={{ width: '100%' }} colorScheme="inverse" appearance="text"
291
+ onClick={() => {
292
+ setVisible(false)
293
+ onClickViewAll()
294
+ }}>
295
+ <Text appearance="microtext1">
296
+ {t.viewAll}
297
+ </Text>
298
+ </Button>
299
+ </Flex>
300
+ </Flex>
301
+ </NotificationsComponent>
302
+ </Flex >)
303
+ }
304
+
305
+ const dictionary = {
306
+ en: {
307
+ notifications: 'Notifications',
308
+ all: 'All',
309
+ filterAll: 'Filter all notifications',
310
+ filterUnread: 'Filter unread notifications',
311
+ filterHigh: 'Filter high notifications',
312
+ filterMedium: 'Filter medium notifications',
313
+ filterLow: 'Filter low notifications',
314
+ unread: 'Unread',
315
+ high: 'High',
316
+ medium: 'Medium',
317
+ low: 'Low',
318
+ viewAll: 'View all notifications',
319
+ openNotifications: 'View notifications',
320
+ hasUnread: 'Has Unread notifications',
321
+ close: 'Close',
322
+ },
323
+ pt: {
324
+ notifications: 'Notificações',
325
+ all: 'Todos',
326
+ filterAll: 'Filtrar todas as notificações ',
327
+ filterUnread: 'Filtrar notificações não lidas',
328
+ filterHigh: 'Filtrar notificações criticidade alta',
329
+ filterMedium: 'Filtrar notificações criticidade média',
330
+ filterLow: 'Filtrar notificações criticidade baixa',
331
+ unread: 'Não lidas',
332
+ high: 'Alto',
333
+ medium: 'Médio',
334
+ low: 'Baixo',
335
+ viewAll: 'Ver todas as notificações',
336
+ openNotifications: 'Visualizar notificações',
337
+ hasUnread: 'Existem notificações não lidas',
338
+ close: 'Fechar',
339
+ },
340
+ } satisfies Dictionary