@stack-spot/portal-components 2.26.0 → 2.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/CHANGELOG.md +621 -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.js +1 -1
  100. package/dist/components/notification/NotificationList.d.ts +1 -1
  101. package/dist/components/notification/NotificationList.js +43 -43
  102. package/dist/components/notification/NotificationPlaceholder.d.ts +1 -1
  103. package/dist/components/notification/NotificationPlaceholder.js +9 -9
  104. package/dist/components/notification/NotificationPlaceholder.js.map +1 -1
  105. package/dist/containers/NotificationsPage.d.ts +1 -1
  106. package/dist/containers/NotificationsPage.js +10 -10
  107. package/dist/context/anchor.d.ts +1 -1
  108. package/dist/context/anchor.js +1 -1
  109. package/dist/context/loading.d.ts +1 -1
  110. package/dist/context/loading.js +1 -1
  111. package/dist/context/notification/context.d.ts +1 -1
  112. package/dist/context/notification/context.js +1 -1
  113. package/dist/hooks/date.js +1 -1
  114. package/dist/hooks/service-now.js +28 -28
  115. package/dist/svg/AI.d.ts +1 -1
  116. package/dist/svg/AI.js +1 -1
  117. package/dist/svg/CS.d.ts +1 -1
  118. package/dist/svg/CS.js +1 -1
  119. package/dist/svg/EDP.d.ts +1 -1
  120. package/dist/svg/EDP.js +1 -1
  121. package/dist/svg/Forbidden.d.ts +1 -1
  122. package/dist/svg/Forbidden.js +1 -1
  123. package/dist/svg/GenericPlaceholder.d.ts +1 -1
  124. package/dist/svg/GenericPlaceholder.js +1 -1
  125. package/dist/svg/HUB.d.ts +1 -1
  126. package/dist/svg/HUB.js +1 -1
  127. package/dist/svg/Logo.d.ts +1 -1
  128. package/dist/svg/Logo.js +1 -1
  129. package/dist/svg/MiniLogo.d.ts +1 -1
  130. package/dist/svg/MiniLogo.js +1 -1
  131. package/dist/svg/NotFound.d.ts +1 -1
  132. package/dist/svg/NotFound.js +1 -1
  133. package/dist/svg/ServerError.d.ts +1 -1
  134. package/dist/svg/ServerError.js +1 -1
  135. package/dist/svg/Unauthenticated.d.ts +1 -1
  136. package/dist/svg/Unauthenticated.js +1 -1
  137. package/package.json +6 -6
  138. package/readme.md +66 -66
  139. package/src/components/AnimatedHeight.tsx +174 -174
  140. package/src/components/AsyncContent.tsx +78 -78
  141. package/src/components/BannerWarning.tsx +91 -91
  142. package/src/components/Breadcrumb/index.tsx +76 -76
  143. package/src/components/Breadcrumb/styled.ts +37 -37
  144. package/src/components/ButtonLoading.tsx +29 -29
  145. package/src/components/ChatBot.tsx +82 -82
  146. package/src/components/ContentValidateFilter.tsx +15 -15
  147. package/src/components/FadingOverflow.tsx +265 -265
  148. package/src/components/FileTreeView/More.tsx +114 -114
  149. package/src/components/FileTreeView/index.tsx +186 -186
  150. package/src/components/InfiniteScroll.tsx +24 -24
  151. package/src/components/InfoMaintenanceBanner.tsx +29 -29
  152. package/src/components/LazyMarkdown/BlockquoteMd.tsx +107 -107
  153. package/src/components/LazyMarkdown/CodeViewer.tsx +161 -161
  154. package/src/components/LazyMarkdown/Markdown.tsx +122 -122
  155. package/src/components/LazyMarkdown/MarkdownButton.tsx +24 -24
  156. package/src/components/LazyMarkdown/Video.tsx +13 -13
  157. package/src/components/LazyMarkdown/index.tsx +21 -21
  158. package/src/components/Placeholder.tsx +118 -118
  159. package/src/components/ScrollView.tsx +57 -57
  160. package/src/components/Select/BadgeItem.tsx +58 -58
  161. package/src/components/Select/ClearInput.tsx +24 -24
  162. package/src/components/Select/CloseItem.tsx +38 -38
  163. package/src/components/Select/CreatableSelect.tsx +155 -155
  164. package/src/components/Select/CustomMenu.tsx +16 -16
  165. package/src/components/Select/LabelItem.tsx +8 -8
  166. package/src/components/Select/MultiValue.tsx +49 -49
  167. package/src/components/Select/SelectInfiniteScroll.tsx +82 -82
  168. package/src/components/Select/SelectSearch.tsx +195 -195
  169. package/src/components/Select/index.tsx +7 -7
  170. package/src/components/Select/types.ts +8 -8
  171. package/src/components/SelectionList.tsx +427 -427
  172. package/src/components/StatusCircle.tsx +67 -67
  173. package/src/components/Stepper/Navigation.tsx +97 -97
  174. package/src/components/Stepper/Step.tsx +30 -30
  175. package/src/components/Stepper/Stepper.tsx +113 -113
  176. package/src/components/Stepper/headers.tsx +64 -64
  177. package/src/components/Stepper/index.ts +3 -3
  178. package/src/components/Table/HeaderItem.tsx +52 -52
  179. package/src/components/Table/SettingsVerticalMenu.tsx +50 -50
  180. package/src/components/Table/StyledLinkTable.tsx +22 -22
  181. package/src/components/Table/TableData.tsx +251 -251
  182. package/src/components/Table/index.tsx +2 -2
  183. package/src/components/TimelineSection.tsx +66 -66
  184. package/src/components/error/ErrorFeedback.tsx +217 -217
  185. package/src/components/error/NotFound.tsx +24 -24
  186. package/src/components/error/UnderMaintenance.tsx +30 -30
  187. package/src/components/error/index.ts +4 -4
  188. package/src/components/form/Form/Form.tsx +101 -101
  189. package/src/components/form/Form/FormGroup.tsx +221 -221
  190. package/src/components/form/Form/index.ts +2 -2
  191. package/src/components/form/SearchInput.tsx +69 -69
  192. package/src/components/form/Select/CustomSelect.tsx +232 -232
  193. package/src/components/form/Select/DetailedSelect.tsx +85 -85
  194. package/src/components/form/Select/Select.tsx +67 -67
  195. package/src/components/form/Select/index.ts +4 -4
  196. package/src/components/form/Select/styled.ts +165 -165
  197. package/src/components/form/Select/types.ts +112 -112
  198. package/src/components/form/Select/utils.tsx +28 -28
  199. package/src/components/notification/NotificationComponent.tsx +340 -340
  200. package/src/components/notification/NotificationItem.tsx +336 -336
  201. package/src/components/notification/NotificationList.tsx +178 -178
  202. package/src/components/notification/NotificationPlaceholder.tsx +43 -43
  203. package/src/components/notification/types.ts +72 -72
  204. package/src/containers/NotificationsPage.tsx +98 -98
  205. package/src/context/anchor.tsx +37 -37
  206. package/src/context/loading.tsx +36 -36
  207. package/src/context/notification/LazyNotificationList.ts +103 -103
  208. package/src/context/notification/NotificationController.ts +104 -104
  209. package/src/context/notification/context.tsx +23 -23
  210. package/src/context/notification/hooks.ts +98 -98
  211. package/src/context/notification/types.ts +65 -65
  212. package/src/hooks/date.ts +31 -31
  213. package/src/hooks/keyboard.tsx +128 -128
  214. package/src/hooks/manual-render.tsx +10 -10
  215. package/src/hooks/service-now.tsx +233 -233
  216. package/src/hooks/text.tsx +30 -30
  217. package/src/hooks/title.tsx +28 -28
  218. package/src/hooks/use-effect-once.tsx +43 -43
  219. package/src/index.ts +19 -19
  220. package/src/notifications.ts +11 -11
  221. package/src/svg/AI.tsx +41 -41
  222. package/src/svg/CS.tsx +48 -48
  223. package/src/svg/EDP.tsx +31 -31
  224. package/src/svg/Forbidden.tsx +22 -22
  225. package/src/svg/GenericPlaceholder.tsx +20 -20
  226. package/src/svg/HUB.tsx +48 -48
  227. package/src/svg/Logo.tsx +16 -16
  228. package/src/svg/MiniLogo.tsx +12 -12
  229. package/src/svg/NotFound.tsx +16 -16
  230. package/src/svg/ServerError.tsx +33 -33
  231. package/src/svg/Unauthenticated.tsx +16 -16
  232. package/src/svg/index.ts +11 -11
  233. package/src/utils/accessibility.ts +135 -135
  234. package/src/utils/cookie.ts +73 -73
  235. package/src/utils/promise.ts +5 -5
  236. package/src/utils/read-file.ts +16 -16
  237. 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