@stack-spot/portal-components 2.26.0 → 2.27.1

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 (243) hide show
  1. package/CHANGELOG.md +628 -614
  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 +3 -3
  40. package/dist/components/Placeholder.js +1 -1
  41. package/dist/components/ScrollView.js +16 -16
  42. package/dist/components/Select/BadgeItem.d.ts +1 -1
  43. package/dist/components/Select/BadgeItem.js +1 -1
  44. package/dist/components/Select/ClearInput.d.ts +1 -1
  45. package/dist/components/Select/ClearInput.js +1 -1
  46. package/dist/components/Select/CloseItem.d.ts +1 -1
  47. package/dist/components/Select/CloseItem.js +1 -1
  48. package/dist/components/Select/CreatableSelect.js +1 -1
  49. package/dist/components/Select/CustomMenu.d.ts +1 -1
  50. package/dist/components/Select/CustomMenu.js +1 -1
  51. package/dist/components/Select/LabelItem.d.ts +1 -1
  52. package/dist/components/Select/LabelItem.js +1 -1
  53. package/dist/components/Select/MultiValue.d.ts +1 -1
  54. package/dist/components/Select/MultiValue.js +1 -1
  55. package/dist/components/Select/SelectInfiniteScroll.d.ts +1 -1
  56. package/dist/components/Select/SelectInfiniteScroll.js +1 -1
  57. package/dist/components/Select/SelectSearch.d.ts +1 -1
  58. package/dist/components/Select/SelectSearch.js +1 -1
  59. package/dist/components/SelectionList.d.ts +1 -1
  60. package/dist/components/SelectionList.js +61 -61
  61. package/dist/components/StatusCircle.d.ts +1 -1
  62. package/dist/components/StatusCircle.js +6 -6
  63. package/dist/components/Stepper/Navigation.js +4 -4
  64. package/dist/components/Stepper/Step.js +3 -3
  65. package/dist/components/Stepper/Stepper.js +6 -6
  66. package/dist/components/Stepper/headers.js +22 -22
  67. package/dist/components/Table/HeaderItem.js +1 -1
  68. package/dist/components/Table/SettingsVerticalMenu.d.ts +1 -1
  69. package/dist/components/Table/SettingsVerticalMenu.js +1 -1
  70. package/dist/components/Table/StyledLinkTable.d.ts +1 -1
  71. package/dist/components/Table/StyledLinkTable.js +5 -5
  72. package/dist/components/Table/TableData.d.ts +1 -1
  73. package/dist/components/Table/TableData.js +25 -25
  74. package/dist/components/TimelineSection.d.ts +1 -1
  75. package/dist/components/TimelineSection.js +14 -14
  76. package/dist/components/error/ErrorFeedback.d.ts +1 -1
  77. package/dist/components/error/ErrorFeedback.js +35 -35
  78. package/dist/components/error/NotFound.d.ts +1 -1
  79. package/dist/components/error/NotFound.js +1 -1
  80. package/dist/components/error/UnderMaintenance.d.ts +1 -1
  81. package/dist/components/error/UnderMaintenance.js +1 -1
  82. package/dist/components/form/Form/Form.d.ts +1 -1
  83. package/dist/components/form/Form/Form.js +1 -1
  84. package/dist/components/form/Form/FormGroup.d.ts +2 -2
  85. package/dist/components/form/Form/FormGroup.js +1 -1
  86. package/dist/components/form/SearchInput.d.ts +1 -1
  87. package/dist/components/form/SearchInput.js +1 -1
  88. package/dist/components/form/Select/CustomSelect.d.ts +1 -1
  89. package/dist/components/form/Select/CustomSelect.js +1 -1
  90. package/dist/components/form/Select/DetailedSelect.d.ts +1 -1
  91. package/dist/components/form/Select/DetailedSelect.js +1 -1
  92. package/dist/components/form/Select/Select.d.ts +1 -1
  93. package/dist/components/form/Select/Select.js +1 -1
  94. package/dist/components/form/Select/styled.js +161 -161
  95. package/dist/components/form/Select/utils.js +1 -1
  96. package/dist/components/notification/NotificationComponent.d.ts +1 -1
  97. package/dist/components/notification/NotificationComponent.js +54 -54
  98. package/dist/components/notification/NotificationItem.d.ts +1 -1
  99. package/dist/components/notification/NotificationItem.d.ts.map +1 -1
  100. package/dist/components/notification/NotificationItem.js +4 -4
  101. package/dist/components/notification/NotificationItem.js.map +1 -1
  102. package/dist/components/notification/NotificationList.d.ts +1 -1
  103. package/dist/components/notification/NotificationList.js +43 -43
  104. package/dist/components/notification/NotificationPlaceholder.d.ts +1 -1
  105. package/dist/components/notification/NotificationPlaceholder.js +9 -9
  106. package/dist/components/notification/NotificationPlaceholder.js.map +1 -1
  107. package/dist/containers/NotificationsPage.d.ts +1 -1
  108. package/dist/containers/NotificationsPage.d.ts.map +1 -1
  109. package/dist/containers/NotificationsPage.js +24 -11
  110. package/dist/containers/NotificationsPage.js.map +1 -1
  111. package/dist/context/anchor.d.ts +1 -1
  112. package/dist/context/anchor.js +1 -1
  113. package/dist/context/loading.d.ts +1 -1
  114. package/dist/context/loading.js +1 -1
  115. package/dist/context/notification/context.d.ts +1 -1
  116. package/dist/context/notification/context.js +1 -1
  117. package/dist/context/notification/types.d.ts +1 -0
  118. package/dist/context/notification/types.d.ts.map +1 -1
  119. package/dist/hooks/date.js +1 -1
  120. package/dist/hooks/service-now.js +28 -28
  121. package/dist/svg/AI.d.ts +1 -1
  122. package/dist/svg/AI.js +1 -1
  123. package/dist/svg/CS.d.ts +1 -1
  124. package/dist/svg/CS.js +1 -1
  125. package/dist/svg/EDP.d.ts +1 -1
  126. package/dist/svg/EDP.js +1 -1
  127. package/dist/svg/Forbidden.d.ts +1 -1
  128. package/dist/svg/Forbidden.js +1 -1
  129. package/dist/svg/GenericPlaceholder.d.ts +1 -1
  130. package/dist/svg/GenericPlaceholder.js +1 -1
  131. package/dist/svg/HUB.d.ts +1 -1
  132. package/dist/svg/HUB.js +1 -1
  133. package/dist/svg/Logo.d.ts +1 -1
  134. package/dist/svg/Logo.js +1 -1
  135. package/dist/svg/MiniLogo.d.ts +1 -1
  136. package/dist/svg/MiniLogo.js +1 -1
  137. package/dist/svg/NotFound.d.ts +1 -1
  138. package/dist/svg/NotFound.js +1 -1
  139. package/dist/svg/ServerError.d.ts +1 -1
  140. package/dist/svg/ServerError.js +1 -1
  141. package/dist/svg/Unauthenticated.d.ts +1 -1
  142. package/dist/svg/Unauthenticated.js +1 -1
  143. package/package.json +80 -80
  144. package/readme.md +66 -66
  145. package/src/components/AnimatedHeight.tsx +174 -174
  146. package/src/components/AsyncContent.tsx +78 -78
  147. package/src/components/BannerWarning.tsx +91 -91
  148. package/src/components/Breadcrumb/index.tsx +76 -76
  149. package/src/components/Breadcrumb/styled.ts +37 -37
  150. package/src/components/ButtonLoading.tsx +29 -29
  151. package/src/components/ChatBot.tsx +82 -82
  152. package/src/components/ContentValidateFilter.tsx +15 -15
  153. package/src/components/FadingOverflow.tsx +265 -265
  154. package/src/components/FileTreeView/More.tsx +114 -114
  155. package/src/components/FileTreeView/index.tsx +186 -186
  156. package/src/components/InfiniteScroll.tsx +24 -24
  157. package/src/components/InfoMaintenanceBanner.tsx +29 -29
  158. package/src/components/LazyMarkdown/BlockquoteMd.tsx +107 -107
  159. package/src/components/LazyMarkdown/CodeViewer.tsx +161 -161
  160. package/src/components/LazyMarkdown/Markdown.tsx +122 -122
  161. package/src/components/LazyMarkdown/MarkdownButton.tsx +24 -24
  162. package/src/components/LazyMarkdown/Video.tsx +13 -13
  163. package/src/components/LazyMarkdown/index.tsx +21 -21
  164. package/src/components/Placeholder.tsx +118 -118
  165. package/src/components/ScrollView.tsx +57 -57
  166. package/src/components/Select/BadgeItem.tsx +58 -58
  167. package/src/components/Select/ClearInput.tsx +24 -24
  168. package/src/components/Select/CloseItem.tsx +38 -38
  169. package/src/components/Select/CreatableSelect.tsx +155 -155
  170. package/src/components/Select/CustomMenu.tsx +16 -16
  171. package/src/components/Select/LabelItem.tsx +8 -8
  172. package/src/components/Select/MultiValue.tsx +49 -49
  173. package/src/components/Select/SelectInfiniteScroll.tsx +82 -82
  174. package/src/components/Select/SelectSearch.tsx +195 -195
  175. package/src/components/Select/index.tsx +7 -7
  176. package/src/components/Select/types.ts +8 -8
  177. package/src/components/SelectionList.tsx +427 -427
  178. package/src/components/StatusCircle.tsx +67 -67
  179. package/src/components/Stepper/Navigation.tsx +97 -97
  180. package/src/components/Stepper/Step.tsx +30 -30
  181. package/src/components/Stepper/Stepper.tsx +113 -113
  182. package/src/components/Stepper/headers.tsx +64 -64
  183. package/src/components/Stepper/index.ts +3 -3
  184. package/src/components/Table/HeaderItem.tsx +52 -52
  185. package/src/components/Table/SettingsVerticalMenu.tsx +50 -50
  186. package/src/components/Table/StyledLinkTable.tsx +22 -22
  187. package/src/components/Table/TableData.tsx +251 -251
  188. package/src/components/Table/index.tsx +2 -2
  189. package/src/components/TimelineSection.tsx +66 -66
  190. package/src/components/error/ErrorFeedback.tsx +217 -217
  191. package/src/components/error/NotFound.tsx +24 -24
  192. package/src/components/error/UnderMaintenance.tsx +30 -30
  193. package/src/components/error/index.ts +4 -4
  194. package/src/components/form/Form/Form.tsx +101 -101
  195. package/src/components/form/Form/FormGroup.tsx +221 -221
  196. package/src/components/form/Form/index.ts +2 -2
  197. package/src/components/form/SearchInput.tsx +69 -69
  198. package/src/components/form/Select/CustomSelect.tsx +232 -232
  199. package/src/components/form/Select/DetailedSelect.tsx +85 -85
  200. package/src/components/form/Select/Select.tsx +67 -67
  201. package/src/components/form/Select/index.ts +4 -4
  202. package/src/components/form/Select/styled.ts +165 -165
  203. package/src/components/form/Select/types.ts +112 -112
  204. package/src/components/form/Select/utils.tsx +28 -28
  205. package/src/components/notification/NotificationComponent.tsx +340 -340
  206. package/src/components/notification/NotificationItem.tsx +337 -336
  207. package/src/components/notification/NotificationList.tsx +178 -178
  208. package/src/components/notification/NotificationPlaceholder.tsx +43 -43
  209. package/src/components/notification/types.ts +72 -72
  210. package/src/containers/NotificationsPage.tsx +119 -98
  211. package/src/context/anchor.tsx +37 -37
  212. package/src/context/loading.tsx +36 -36
  213. package/src/context/notification/LazyNotificationList.ts +103 -103
  214. package/src/context/notification/NotificationController.ts +104 -104
  215. package/src/context/notification/context.tsx +23 -23
  216. package/src/context/notification/hooks.ts +98 -98
  217. package/src/context/notification/types.ts +66 -65
  218. package/src/hooks/date.ts +31 -31
  219. package/src/hooks/keyboard.tsx +128 -128
  220. package/src/hooks/manual-render.tsx +10 -10
  221. package/src/hooks/service-now.tsx +233 -233
  222. package/src/hooks/text.tsx +30 -30
  223. package/src/hooks/title.tsx +28 -28
  224. package/src/hooks/use-effect-once.tsx +43 -43
  225. package/src/index.ts +19 -19
  226. package/src/notifications.ts +11 -11
  227. package/src/svg/AI.tsx +41 -41
  228. package/src/svg/CS.tsx +48 -48
  229. package/src/svg/EDP.tsx +31 -31
  230. package/src/svg/Forbidden.tsx +22 -22
  231. package/src/svg/GenericPlaceholder.tsx +20 -20
  232. package/src/svg/HUB.tsx +48 -48
  233. package/src/svg/Logo.tsx +16 -16
  234. package/src/svg/MiniLogo.tsx +12 -12
  235. package/src/svg/NotFound.tsx +16 -16
  236. package/src/svg/ServerError.tsx +33 -33
  237. package/src/svg/Unauthenticated.tsx +16 -16
  238. package/src/svg/index.ts +11 -11
  239. package/src/utils/accessibility.ts +135 -135
  240. package/src/utils/cookie.ts +73 -73
  241. package/src/utils/promise.ts +5 -5
  242. package/src/utils/read-file.ts +16 -16
  243. 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