@stack-spot/portal-components 2.27.1 → 2.27.3

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