@stack-spot/portal-components 2.27.0 → 2.27.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/CHANGELOG.md +635 -621
  2. package/dist/components/AnimatedHeight.d.ts +1 -1
  3. package/dist/components/AnimatedHeight.js +26 -26
  4. package/dist/components/AsyncContent.d.ts +1 -1
  5. package/dist/components/AsyncContent.js +1 -1
  6. package/dist/components/BannerWarning.d.ts +1 -1
  7. package/dist/components/BannerWarning.js +1 -1
  8. package/dist/components/Breadcrumb/index.d.ts +2 -2
  9. package/dist/components/Breadcrumb/index.js +1 -1
  10. package/dist/components/Breadcrumb/styled.js +31 -31
  11. package/dist/components/ButtonLoading.d.ts +1 -1
  12. package/dist/components/ButtonLoading.js +1 -1
  13. package/dist/components/ChatBot.d.ts +1 -1
  14. package/dist/components/ChatBot.js +1 -1
  15. package/dist/components/ContentValidateFilter.d.ts +1 -1
  16. package/dist/components/ContentValidateFilter.js +1 -1
  17. package/dist/components/FadingOverflow.d.ts +1 -1
  18. package/dist/components/FadingOverflow.js +69 -69
  19. package/dist/components/FileTreeView/More.d.ts +1 -1
  20. package/dist/components/FileTreeView/More.js +1 -1
  21. package/dist/components/FileTreeView/index.d.ts +1 -1
  22. package/dist/components/FileTreeView/index.js +1 -1
  23. package/dist/components/InfiniteScroll.d.ts +1 -1
  24. package/dist/components/InfiniteScroll.js +1 -1
  25. package/dist/components/InfoMaintenanceBanner.d.ts +1 -1
  26. package/dist/components/InfoMaintenanceBanner.js +2 -2
  27. package/dist/components/LazyMarkdown/BlockquoteMd.d.ts +1 -1
  28. package/dist/components/LazyMarkdown/BlockquoteMd.js +1 -1
  29. package/dist/components/LazyMarkdown/CodeViewer.d.ts +1 -1
  30. package/dist/components/LazyMarkdown/CodeViewer.js +76 -76
  31. package/dist/components/LazyMarkdown/Markdown.d.ts +1 -1
  32. package/dist/components/LazyMarkdown/Markdown.js +1 -1
  33. package/dist/components/LazyMarkdown/MarkdownButton.d.ts +1 -1
  34. package/dist/components/LazyMarkdown/MarkdownButton.js +1 -1
  35. package/dist/components/LazyMarkdown/Video.d.ts +1 -1
  36. package/dist/components/LazyMarkdown/Video.js +1 -1
  37. package/dist/components/LazyMarkdown/index.d.ts +1 -1
  38. package/dist/components/LazyMarkdown/index.js +1 -1
  39. package/dist/components/Placeholder.d.ts +7 -3
  40. package/dist/components/Placeholder.d.ts.map +1 -1
  41. package/dist/components/Placeholder.js +3 -3
  42. package/dist/components/Placeholder.js.map +1 -1
  43. package/dist/components/ScrollView.js +16 -16
  44. package/dist/components/Select/BadgeItem.d.ts +1 -1
  45. package/dist/components/Select/BadgeItem.js +1 -1
  46. package/dist/components/Select/ClearInput.d.ts +1 -1
  47. package/dist/components/Select/ClearInput.js +1 -1
  48. package/dist/components/Select/CloseItem.d.ts +1 -1
  49. package/dist/components/Select/CloseItem.js +1 -1
  50. package/dist/components/Select/CreatableSelect.js +1 -1
  51. package/dist/components/Select/CustomMenu.d.ts +1 -1
  52. package/dist/components/Select/CustomMenu.js +1 -1
  53. package/dist/components/Select/LabelItem.d.ts +1 -1
  54. package/dist/components/Select/LabelItem.js +1 -1
  55. package/dist/components/Select/MultiValue.d.ts +1 -1
  56. package/dist/components/Select/MultiValue.js +1 -1
  57. package/dist/components/Select/SelectInfiniteScroll.d.ts +1 -1
  58. package/dist/components/Select/SelectInfiniteScroll.js +1 -1
  59. package/dist/components/Select/SelectSearch.d.ts +1 -1
  60. package/dist/components/Select/SelectSearch.js +1 -1
  61. package/dist/components/SelectionList.d.ts +1 -1
  62. package/dist/components/SelectionList.js +61 -61
  63. package/dist/components/StatusCircle.d.ts +1 -1
  64. package/dist/components/StatusCircle.js +6 -6
  65. package/dist/components/Stepper/Navigation.js +4 -4
  66. package/dist/components/Stepper/Step.js +3 -3
  67. package/dist/components/Stepper/Stepper.js +6 -6
  68. package/dist/components/Stepper/headers.js +22 -22
  69. package/dist/components/Table/HeaderItem.js +1 -1
  70. package/dist/components/Table/SettingsVerticalMenu.d.ts +1 -1
  71. package/dist/components/Table/SettingsVerticalMenu.js +1 -1
  72. package/dist/components/Table/StyledLinkTable.d.ts +1 -1
  73. package/dist/components/Table/StyledLinkTable.js +5 -5
  74. package/dist/components/Table/TableData.d.ts +1 -1
  75. package/dist/components/Table/TableData.js +25 -25
  76. package/dist/components/TimelineSection.d.ts +1 -1
  77. package/dist/components/TimelineSection.js +14 -14
  78. package/dist/components/error/ErrorFeedback.d.ts +1 -1
  79. package/dist/components/error/ErrorFeedback.js +35 -35
  80. package/dist/components/error/NotFound.d.ts +1 -1
  81. package/dist/components/error/NotFound.js +1 -1
  82. package/dist/components/error/UnderMaintenance.d.ts +1 -1
  83. package/dist/components/error/UnderMaintenance.js +1 -1
  84. package/dist/components/form/Form/Form.d.ts +1 -1
  85. package/dist/components/form/Form/Form.js +1 -1
  86. package/dist/components/form/Form/FormGroup.d.ts +2 -2
  87. package/dist/components/form/Form/FormGroup.js +1 -1
  88. package/dist/components/form/SearchInput.d.ts +1 -1
  89. package/dist/components/form/SearchInput.js +1 -1
  90. package/dist/components/form/Select/CustomSelect.d.ts +1 -1
  91. package/dist/components/form/Select/CustomSelect.js +1 -1
  92. package/dist/components/form/Select/DetailedSelect.d.ts +1 -1
  93. package/dist/components/form/Select/DetailedSelect.js +1 -1
  94. package/dist/components/form/Select/Select.d.ts +1 -1
  95. package/dist/components/form/Select/Select.js +1 -1
  96. package/dist/components/form/Select/styled.js +161 -161
  97. package/dist/components/form/Select/utils.js +1 -1
  98. package/dist/components/notification/NotificationComponent.d.ts +1 -1
  99. package/dist/components/notification/NotificationComponent.js +54 -54
  100. package/dist/components/notification/NotificationItem.d.ts +1 -1
  101. package/dist/components/notification/NotificationItem.d.ts.map +1 -1
  102. package/dist/components/notification/NotificationItem.js +11 -5
  103. package/dist/components/notification/NotificationItem.js.map +1 -1
  104. package/dist/components/notification/NotificationList.d.ts +1 -1
  105. package/dist/components/notification/NotificationList.d.ts.map +1 -1
  106. package/dist/components/notification/NotificationList.js +44 -44
  107. package/dist/components/notification/NotificationList.js.map +1 -1
  108. package/dist/components/notification/NotificationPlaceholder.d.ts +1 -1
  109. package/dist/components/notification/NotificationPlaceholder.d.ts.map +1 -1
  110. package/dist/components/notification/NotificationPlaceholder.js +2 -2
  111. package/dist/components/notification/NotificationPlaceholder.js.map +1 -1
  112. package/dist/containers/NotificationsPage.d.ts +1 -1
  113. package/dist/containers/NotificationsPage.d.ts.map +1 -1
  114. package/dist/containers/NotificationsPage.js +24 -11
  115. package/dist/containers/NotificationsPage.js.map +1 -1
  116. package/dist/context/anchor.d.ts +1 -1
  117. package/dist/context/anchor.js +1 -1
  118. package/dist/context/loading.d.ts +1 -1
  119. package/dist/context/loading.js +1 -1
  120. package/dist/context/notification/context.d.ts +1 -1
  121. package/dist/context/notification/context.js +1 -1
  122. package/dist/context/notification/types.d.ts +1 -0
  123. package/dist/context/notification/types.d.ts.map +1 -1
  124. package/dist/hooks/date.js +1 -1
  125. package/dist/hooks/service-now.js +28 -28
  126. package/dist/svg/AI.d.ts +1 -1
  127. package/dist/svg/AI.js +1 -1
  128. package/dist/svg/CS.d.ts +1 -1
  129. package/dist/svg/CS.js +1 -1
  130. package/dist/svg/EDP.d.ts +1 -1
  131. package/dist/svg/EDP.js +1 -1
  132. package/dist/svg/Forbidden.d.ts +1 -1
  133. package/dist/svg/Forbidden.js +1 -1
  134. package/dist/svg/GenericPlaceholder.d.ts +4 -2
  135. package/dist/svg/GenericPlaceholder.d.ts.map +1 -1
  136. package/dist/svg/GenericPlaceholder.js +2 -2
  137. package/dist/svg/GenericPlaceholder.js.map +1 -1
  138. package/dist/svg/HUB.d.ts +1 -1
  139. package/dist/svg/HUB.js +1 -1
  140. package/dist/svg/Logo.d.ts +1 -1
  141. package/dist/svg/Logo.js +1 -1
  142. package/dist/svg/MiniLogo.d.ts +1 -1
  143. package/dist/svg/MiniLogo.js +1 -1
  144. package/dist/svg/NotFound.d.ts +1 -1
  145. package/dist/svg/NotFound.js +1 -1
  146. package/dist/svg/ServerError.d.ts +1 -1
  147. package/dist/svg/ServerError.js +1 -1
  148. package/dist/svg/Unauthenticated.d.ts +1 -1
  149. package/dist/svg/Unauthenticated.js +1 -1
  150. package/package.json +6 -6
  151. package/readme.md +66 -66
  152. package/src/components/AnimatedHeight.tsx +174 -174
  153. package/src/components/AsyncContent.tsx +78 -78
  154. package/src/components/BannerWarning.tsx +91 -91
  155. package/src/components/Breadcrumb/index.tsx +76 -76
  156. package/src/components/Breadcrumb/styled.ts +37 -37
  157. package/src/components/ButtonLoading.tsx +29 -29
  158. package/src/components/ChatBot.tsx +82 -82
  159. package/src/components/ContentValidateFilter.tsx +15 -15
  160. package/src/components/FadingOverflow.tsx +265 -265
  161. package/src/components/FileTreeView/More.tsx +114 -114
  162. package/src/components/FileTreeView/index.tsx +186 -186
  163. package/src/components/InfiniteScroll.tsx +24 -24
  164. package/src/components/InfoMaintenanceBanner.tsx +29 -29
  165. package/src/components/LazyMarkdown/BlockquoteMd.tsx +107 -107
  166. package/src/components/LazyMarkdown/CodeViewer.tsx +161 -161
  167. package/src/components/LazyMarkdown/Markdown.tsx +122 -122
  168. package/src/components/LazyMarkdown/MarkdownButton.tsx +24 -24
  169. package/src/components/LazyMarkdown/Video.tsx +13 -13
  170. package/src/components/LazyMarkdown/index.tsx +21 -21
  171. package/src/components/Placeholder.tsx +123 -118
  172. package/src/components/ScrollView.tsx +57 -57
  173. package/src/components/Select/BadgeItem.tsx +58 -58
  174. package/src/components/Select/ClearInput.tsx +24 -24
  175. package/src/components/Select/CloseItem.tsx +38 -38
  176. package/src/components/Select/CreatableSelect.tsx +155 -155
  177. package/src/components/Select/CustomMenu.tsx +16 -16
  178. package/src/components/Select/LabelItem.tsx +8 -8
  179. package/src/components/Select/MultiValue.tsx +49 -49
  180. package/src/components/Select/SelectInfiniteScroll.tsx +82 -82
  181. package/src/components/Select/SelectSearch.tsx +195 -195
  182. package/src/components/Select/index.tsx +7 -7
  183. package/src/components/Select/types.ts +8 -8
  184. package/src/components/SelectionList.tsx +427 -427
  185. package/src/components/StatusCircle.tsx +67 -67
  186. package/src/components/Stepper/Navigation.tsx +97 -97
  187. package/src/components/Stepper/Step.tsx +30 -30
  188. package/src/components/Stepper/Stepper.tsx +113 -113
  189. package/src/components/Stepper/headers.tsx +64 -64
  190. package/src/components/Stepper/index.ts +3 -3
  191. package/src/components/Table/HeaderItem.tsx +52 -52
  192. package/src/components/Table/SettingsVerticalMenu.tsx +50 -50
  193. package/src/components/Table/StyledLinkTable.tsx +22 -22
  194. package/src/components/Table/TableData.tsx +251 -251
  195. package/src/components/Table/index.tsx +2 -2
  196. package/src/components/TimelineSection.tsx +66 -66
  197. package/src/components/error/ErrorFeedback.tsx +217 -217
  198. package/src/components/error/NotFound.tsx +24 -24
  199. package/src/components/error/UnderMaintenance.tsx +30 -30
  200. package/src/components/error/index.ts +4 -4
  201. package/src/components/form/Form/Form.tsx +101 -101
  202. package/src/components/form/Form/FormGroup.tsx +221 -221
  203. package/src/components/form/Form/index.ts +2 -2
  204. package/src/components/form/SearchInput.tsx +69 -69
  205. package/src/components/form/Select/CustomSelect.tsx +232 -232
  206. package/src/components/form/Select/DetailedSelect.tsx +85 -85
  207. package/src/components/form/Select/Select.tsx +67 -67
  208. package/src/components/form/Select/index.ts +4 -4
  209. package/src/components/form/Select/styled.ts +165 -165
  210. package/src/components/form/Select/types.ts +112 -112
  211. package/src/components/form/Select/utils.tsx +28 -28
  212. package/src/components/notification/NotificationComponent.tsx +340 -340
  213. package/src/components/notification/NotificationItem.tsx +345 -336
  214. package/src/components/notification/NotificationList.tsx +179 -178
  215. package/src/components/notification/NotificationPlaceholder.tsx +44 -43
  216. package/src/components/notification/types.ts +72 -72
  217. package/src/containers/NotificationsPage.tsx +119 -98
  218. package/src/context/anchor.tsx +37 -37
  219. package/src/context/loading.tsx +36 -36
  220. package/src/context/notification/LazyNotificationList.ts +103 -103
  221. package/src/context/notification/NotificationController.ts +104 -104
  222. package/src/context/notification/context.tsx +23 -23
  223. package/src/context/notification/hooks.ts +98 -98
  224. package/src/context/notification/types.ts +66 -65
  225. package/src/hooks/date.ts +31 -31
  226. package/src/hooks/keyboard.tsx +128 -128
  227. package/src/hooks/manual-render.tsx +10 -10
  228. package/src/hooks/service-now.tsx +233 -233
  229. package/src/hooks/text.tsx +30 -30
  230. package/src/hooks/title.tsx +28 -28
  231. package/src/hooks/use-effect-once.tsx +43 -43
  232. package/src/index.ts +19 -19
  233. package/src/notifications.ts +11 -11
  234. package/src/svg/AI.tsx +41 -41
  235. package/src/svg/CS.tsx +48 -48
  236. package/src/svg/EDP.tsx +31 -31
  237. package/src/svg/Forbidden.tsx +22 -22
  238. package/src/svg/GenericPlaceholder.tsx +20 -20
  239. package/src/svg/HUB.tsx +48 -48
  240. package/src/svg/Logo.tsx +16 -16
  241. package/src/svg/MiniLogo.tsx +12 -12
  242. package/src/svg/NotFound.tsx +16 -16
  243. package/src/svg/ServerError.tsx +33 -33
  244. package/src/svg/Unauthenticated.tsx +16 -16
  245. package/src/svg/index.ts +11 -11
  246. package/src/utils/accessibility.ts +135 -135
  247. package/src/utils/cookie.ts +73 -73
  248. package/src/utils/promise.ts +5 -5
  249. package/src/utils/read-file.ts +16 -16
  250. package/tsconfig.json +10 -10
@@ -1,233 +1,233 @@
1
-
2
- import { Dictionary, getLanguage, ptEn, translate } from '@stack-spot/portal-translate'
3
- import { pull } from 'lodash'
4
- import { useState } from 'react'
5
- import { useEffectOnce } from './use-effect-once'
6
-
7
- const CHAT_IFRAME_ID = 'sn_va_web_client'
8
- export const CHAT_BUTTON_ID = 'service-now-button'
9
- export const CHAT_BUTTON_WRAPPER_ID = 'service-now-content'
10
-
11
- const closeButtonStyle = `
12
- position: absolute;
13
- bottom: -64px;
14
- right: 60px;
15
- width: 20px;
16
- height: 20px;
17
- display: flex;
18
- align-items: center;
19
- justify-content: center;
20
- background-color: #BCBCCF;
21
- border: none;
22
- outline: none;
23
- border-radius: 50%;
24
- font: 400 20px Roboto, sans-serif;
25
- color: #0B0B0E;
26
- cursor: pointer;
27
- transform: scale(0);
28
- transition: transform 0.2s;
29
- z-index: 999999;
30
- `
31
-
32
- const iframeStyle = `
33
- position: fixed;
34
- display: none;
35
- right: 15px;
36
- bottom: 88px;
37
- max-height: calc(100vh - 152px);
38
- border-radius: 12px;
39
- width: 380px;
40
- height: 600px;
41
- `
42
-
43
- type ChatVisibilityListener = (visible: boolean) => void
44
-
45
- interface Options {
46
- /**
47
- * Function to run when the chat button is hidden, i.e. when the user clicks the close button that appears when the chat button is
48
- * hovered.
49
- */
50
- onClose?: () => void,
51
- }
52
-
53
- const HOVER_AREA_PX = 100
54
- const LOCAL_STORAGE_SERVICE_NOW_VALUE = 'serviceNowChatButtonVisible'
55
- const chatVisibilityListeners: ChatVisibilityListener[] = []
56
-
57
- const serviceNowReference = 'https://zup.service-now.com/sn_va_web_client_app_embed.do?sysparm_branding_key=stackspotvirtualagent&sysparm_topic=f2b636c11bb50e50fe670dcbe54bcb1d'
58
-
59
- function loadScript(src: string): Promise<void> {
60
- return new Promise<void>((resolve, reject) => {
61
- const iframe = document.createElement('iframe')
62
- const serviceNowContent = document.getElementById(CHAT_BUTTON_WRAPPER_ID)
63
- iframe.setAttribute('id', CHAT_IFRAME_ID)
64
- iframe.setAttribute('src', src)
65
- iframe.style.cssText = iframeStyle
66
-
67
- iframe.onload = () => resolve()
68
- iframe.onerror = (error) => reject(error)
69
-
70
- serviceNowContent && serviceNowContent.appendChild(iframe)
71
- })
72
- }
73
-
74
- /**
75
- * @returns the iframe loaded by service now, i.e. the chat window; or null if the chat window doesn't exist.
76
- */
77
- export function getServiceNowLauncher() {
78
- return document.getElementById(CHAT_IFRAME_ID)
79
- }
80
-
81
- /**
82
- * Opens the chat window and makes the chat button visible.
83
- */
84
- export function openServiceNowChat() {
85
- hideOrShowServiceNowContent('visible')
86
- showChatWindow()
87
- showServiceNowChatButton()
88
- }
89
-
90
- function getServiceNowButton() {
91
- return document.getElementById(CHAT_BUTTON_ID)
92
- }
93
-
94
- function hideOrShowServiceNowContent(action: 'visible' | 'hidden') {
95
- const content = document.getElementById(CHAT_BUTTON_WRAPPER_ID)
96
- if (content) {
97
- content.style.visibility = action
98
- }
99
- }
100
-
101
- function toggleChatWindow(show: boolean) {
102
- const launcher = getServiceNowLauncher()
103
- const content = document.getElementById(CHAT_BUTTON_WRAPPER_ID)
104
- if (!launcher || !content) return
105
- const [action, display] = show ? ['add', 'block'] as const : ['remove', 'none'] as const
106
- launcher.classList[action]('show')
107
- content.classList[action]('opened')
108
- launcher.style.display = display
109
- }
110
-
111
- /**
112
- * Shows the chat window, i.e. shows the service now iframe and switch the icon in the chat button to "close".
113
- */
114
- export function showChatWindow() {
115
- toggleChatWindow(true)
116
- }
117
-
118
- /**
119
- * Hides the chat window, i.e. hides the service now iframe and switch the icon in the chat button to "chat".
120
- */
121
- export function hideChatWindow() {
122
- toggleChatWindow(false)
123
- }
124
-
125
- /**
126
- * Adds a listener that is called whenever the visibility of the chat button changes.
127
- *
128
- * The listener is called with a single parameter which indicates if the button is visible or not.
129
- * @param listener the listener to register.
130
- * @returns a function that, when called, removes the listener.
131
- */
132
- function addChatVisibilityListener(listener: ChatVisibilityListener) {
133
- chatVisibilityListeners.push(listener)
134
- return () => pull(chatVisibilityListeners, listener)
135
- }
136
-
137
- function renderCloseButton(onClose?: () => void) {
138
- const serviceNowButton = getServiceNowButton()
139
- const anchor = serviceNowButton?.parentElement
140
- if (!anchor) return
141
- const close = document.createElement('button')
142
- close.setAttribute('id', 'close-service-now')
143
- close.style.cssText = closeButtonStyle
144
- close.textContent = '-'
145
-
146
- close.addEventListener('click', () => hideServiceNowChatButton(onClose))
147
- anchor.appendChild(close)
148
-
149
- serviceNowButton.addEventListener('mouseenter', () => {
150
- const t = translate(dictionary, getLanguage(ptEn))
151
- close.setAttribute('title', t.hide)
152
- close.style.transform = 'scale(1)'
153
-
154
- const onMouseMove = (ev: MouseEvent) => {
155
- if (ev.clientX < window.innerWidth - HOVER_AREA_PX || ev.clientY < window.innerHeight - HOVER_AREA_PX) {
156
- close.style.transform = 'scale(0)'
157
- window.removeEventListener('mousemove', onMouseMove)
158
- }
159
- }
160
- window.addEventListener('mousemove', onMouseMove)
161
- })
162
- }
163
-
164
- function showServiceNowChatButton() {
165
- getServiceNowButton()?.classList?.add('show')
166
- localStorage.setItem(LOCAL_STORAGE_SERVICE_NOW_VALUE, 'true')
167
- chatVisibilityListeners.forEach(l => l(true))
168
- }
169
-
170
- function hideServiceNowChatButton(onClose?: () => void) {
171
- hideOrShowServiceNowContent('hidden')
172
- hideChatWindow()
173
- getServiceNowButton()?.classList?.remove('show')
174
- localStorage.setItem(LOCAL_STORAGE_SERVICE_NOW_VALUE, 'false')
175
- onClose?.()
176
- chatVisibilityListeners.forEach(l => l(false))
177
- }
178
-
179
- function isServiceNowChatButtonVisible() {
180
- return localStorage.getItem(LOCAL_STORAGE_SERVICE_NOW_VALUE) !== 'false'
181
- }
182
-
183
- async function initializeChat({ onClose }: Options = {}) {
184
- try {
185
- await loadScript(serviceNowReference)
186
- if (isServiceNowChatButtonVisible()) showServiceNowChatButton()
187
- else hideServiceNowChatButton()
188
- renderCloseButton(onClose)
189
- } catch {
190
- const t = translate(dictionary, getLanguage(ptEn))
191
- // eslint-disable-next-line no-console
192
- console.warn(t.errorLoadScript)
193
- }
194
- }
195
-
196
- /**
197
- * A react hook that indicates whether the chat button is visible or not. This is updated whenever the value changes.
198
- * @returns true if the chat button is visible and false otherwise.
199
- */
200
- export function useServiceNowChatButtonVisibility() {
201
- const [visible, setVisible] = useState(isServiceNowChatButtonVisible())
202
- useEffectOnce(() => addChatVisibilityListener(setVisible))
203
- return visible
204
- }
205
-
206
- /**
207
- * Starts Service Now:
208
- *
209
- * - Creates the chat window feature by loading the required script.
210
- * - Adds a "minimize" button on top of the chat button (left bottom corner). This button appears whenever the user hovers the chat button.
211
- * If clicked, the chat button is hidden.
212
- * @param options
213
- */
214
- export function useServiceNowEffect(options?: Options) {
215
- useEffectOnce(() => {
216
- initializeChat(options)
217
- })
218
- }
219
-
220
- export const dictionary = {
221
- en: {
222
- errorLoadScript: 'An error occurred while loading the script:',
223
- hide: 'Hide chat button',
224
- hiddenTitle: 'The chat button is now hidden',
225
- hiddenMessage: 'To see it again, please select the option "Chat with us" in the menu just under this message.',
226
- },
227
- pt: {
228
- errorLoadScript: 'Ocorreu um erro ao carregar o script:',
229
- hide: 'Esconder o botão de chat',
230
- hiddenTitle: 'O botão de chat foi escondido',
231
- hiddenMessage: 'Para vê-lo novamente, por favor escolha a opção "Converse conosco" no menu logo em baixo desta mensagem.',
232
- },
233
- } satisfies Dictionary
1
+
2
+ import { Dictionary, getLanguage, ptEn, translate } from '@stack-spot/portal-translate'
3
+ import { pull } from 'lodash'
4
+ import { useState } from 'react'
5
+ import { useEffectOnce } from './use-effect-once'
6
+
7
+ const CHAT_IFRAME_ID = 'sn_va_web_client'
8
+ export const CHAT_BUTTON_ID = 'service-now-button'
9
+ export const CHAT_BUTTON_WRAPPER_ID = 'service-now-content'
10
+
11
+ const closeButtonStyle = `
12
+ position: absolute;
13
+ bottom: -64px;
14
+ right: 60px;
15
+ width: 20px;
16
+ height: 20px;
17
+ display: flex;
18
+ align-items: center;
19
+ justify-content: center;
20
+ background-color: #BCBCCF;
21
+ border: none;
22
+ outline: none;
23
+ border-radius: 50%;
24
+ font: 400 20px Roboto, sans-serif;
25
+ color: #0B0B0E;
26
+ cursor: pointer;
27
+ transform: scale(0);
28
+ transition: transform 0.2s;
29
+ z-index: 999999;
30
+ `
31
+
32
+ const iframeStyle = `
33
+ position: fixed;
34
+ display: none;
35
+ right: 15px;
36
+ bottom: 88px;
37
+ max-height: calc(100vh - 152px);
38
+ border-radius: 12px;
39
+ width: 380px;
40
+ height: 600px;
41
+ `
42
+
43
+ type ChatVisibilityListener = (visible: boolean) => void
44
+
45
+ interface Options {
46
+ /**
47
+ * Function to run when the chat button is hidden, i.e. when the user clicks the close button that appears when the chat button is
48
+ * hovered.
49
+ */
50
+ onClose?: () => void,
51
+ }
52
+
53
+ const HOVER_AREA_PX = 100
54
+ const LOCAL_STORAGE_SERVICE_NOW_VALUE = 'serviceNowChatButtonVisible'
55
+ const chatVisibilityListeners: ChatVisibilityListener[] = []
56
+
57
+ const serviceNowReference = 'https://zup.service-now.com/sn_va_web_client_app_embed.do?sysparm_branding_key=stackspotvirtualagent&sysparm_topic=f2b636c11bb50e50fe670dcbe54bcb1d'
58
+
59
+ function loadScript(src: string): Promise<void> {
60
+ return new Promise<void>((resolve, reject) => {
61
+ const iframe = document.createElement('iframe')
62
+ const serviceNowContent = document.getElementById(CHAT_BUTTON_WRAPPER_ID)
63
+ iframe.setAttribute('id', CHAT_IFRAME_ID)
64
+ iframe.setAttribute('src', src)
65
+ iframe.style.cssText = iframeStyle
66
+
67
+ iframe.onload = () => resolve()
68
+ iframe.onerror = (error) => reject(error)
69
+
70
+ serviceNowContent && serviceNowContent.appendChild(iframe)
71
+ })
72
+ }
73
+
74
+ /**
75
+ * @returns the iframe loaded by service now, i.e. the chat window; or null if the chat window doesn't exist.
76
+ */
77
+ export function getServiceNowLauncher() {
78
+ return document.getElementById(CHAT_IFRAME_ID)
79
+ }
80
+
81
+ /**
82
+ * Opens the chat window and makes the chat button visible.
83
+ */
84
+ export function openServiceNowChat() {
85
+ hideOrShowServiceNowContent('visible')
86
+ showChatWindow()
87
+ showServiceNowChatButton()
88
+ }
89
+
90
+ function getServiceNowButton() {
91
+ return document.getElementById(CHAT_BUTTON_ID)
92
+ }
93
+
94
+ function hideOrShowServiceNowContent(action: 'visible' | 'hidden') {
95
+ const content = document.getElementById(CHAT_BUTTON_WRAPPER_ID)
96
+ if (content) {
97
+ content.style.visibility = action
98
+ }
99
+ }
100
+
101
+ function toggleChatWindow(show: boolean) {
102
+ const launcher = getServiceNowLauncher()
103
+ const content = document.getElementById(CHAT_BUTTON_WRAPPER_ID)
104
+ if (!launcher || !content) return
105
+ const [action, display] = show ? ['add', 'block'] as const : ['remove', 'none'] as const
106
+ launcher.classList[action]('show')
107
+ content.classList[action]('opened')
108
+ launcher.style.display = display
109
+ }
110
+
111
+ /**
112
+ * Shows the chat window, i.e. shows the service now iframe and switch the icon in the chat button to "close".
113
+ */
114
+ export function showChatWindow() {
115
+ toggleChatWindow(true)
116
+ }
117
+
118
+ /**
119
+ * Hides the chat window, i.e. hides the service now iframe and switch the icon in the chat button to "chat".
120
+ */
121
+ export function hideChatWindow() {
122
+ toggleChatWindow(false)
123
+ }
124
+
125
+ /**
126
+ * Adds a listener that is called whenever the visibility of the chat button changes.
127
+ *
128
+ * The listener is called with a single parameter which indicates if the button is visible or not.
129
+ * @param listener the listener to register.
130
+ * @returns a function that, when called, removes the listener.
131
+ */
132
+ function addChatVisibilityListener(listener: ChatVisibilityListener) {
133
+ chatVisibilityListeners.push(listener)
134
+ return () => pull(chatVisibilityListeners, listener)
135
+ }
136
+
137
+ function renderCloseButton(onClose?: () => void) {
138
+ const serviceNowButton = getServiceNowButton()
139
+ const anchor = serviceNowButton?.parentElement
140
+ if (!anchor) return
141
+ const close = document.createElement('button')
142
+ close.setAttribute('id', 'close-service-now')
143
+ close.style.cssText = closeButtonStyle
144
+ close.textContent = '-'
145
+
146
+ close.addEventListener('click', () => hideServiceNowChatButton(onClose))
147
+ anchor.appendChild(close)
148
+
149
+ serviceNowButton.addEventListener('mouseenter', () => {
150
+ const t = translate(dictionary, getLanguage(ptEn))
151
+ close.setAttribute('title', t.hide)
152
+ close.style.transform = 'scale(1)'
153
+
154
+ const onMouseMove = (ev: MouseEvent) => {
155
+ if (ev.clientX < window.innerWidth - HOVER_AREA_PX || ev.clientY < window.innerHeight - HOVER_AREA_PX) {
156
+ close.style.transform = 'scale(0)'
157
+ window.removeEventListener('mousemove', onMouseMove)
158
+ }
159
+ }
160
+ window.addEventListener('mousemove', onMouseMove)
161
+ })
162
+ }
163
+
164
+ function showServiceNowChatButton() {
165
+ getServiceNowButton()?.classList?.add('show')
166
+ localStorage.setItem(LOCAL_STORAGE_SERVICE_NOW_VALUE, 'true')
167
+ chatVisibilityListeners.forEach(l => l(true))
168
+ }
169
+
170
+ function hideServiceNowChatButton(onClose?: () => void) {
171
+ hideOrShowServiceNowContent('hidden')
172
+ hideChatWindow()
173
+ getServiceNowButton()?.classList?.remove('show')
174
+ localStorage.setItem(LOCAL_STORAGE_SERVICE_NOW_VALUE, 'false')
175
+ onClose?.()
176
+ chatVisibilityListeners.forEach(l => l(false))
177
+ }
178
+
179
+ function isServiceNowChatButtonVisible() {
180
+ return localStorage.getItem(LOCAL_STORAGE_SERVICE_NOW_VALUE) !== 'false'
181
+ }
182
+
183
+ async function initializeChat({ onClose }: Options = {}) {
184
+ try {
185
+ await loadScript(serviceNowReference)
186
+ if (isServiceNowChatButtonVisible()) showServiceNowChatButton()
187
+ else hideServiceNowChatButton()
188
+ renderCloseButton(onClose)
189
+ } catch {
190
+ const t = translate(dictionary, getLanguage(ptEn))
191
+ // eslint-disable-next-line no-console
192
+ console.warn(t.errorLoadScript)
193
+ }
194
+ }
195
+
196
+ /**
197
+ * A react hook that indicates whether the chat button is visible or not. This is updated whenever the value changes.
198
+ * @returns true if the chat button is visible and false otherwise.
199
+ */
200
+ export function useServiceNowChatButtonVisibility() {
201
+ const [visible, setVisible] = useState(isServiceNowChatButtonVisible())
202
+ useEffectOnce(() => addChatVisibilityListener(setVisible))
203
+ return visible
204
+ }
205
+
206
+ /**
207
+ * Starts Service Now:
208
+ *
209
+ * - Creates the chat window feature by loading the required script.
210
+ * - Adds a "minimize" button on top of the chat button (left bottom corner). This button appears whenever the user hovers the chat button.
211
+ * If clicked, the chat button is hidden.
212
+ * @param options
213
+ */
214
+ export function useServiceNowEffect(options?: Options) {
215
+ useEffectOnce(() => {
216
+ initializeChat(options)
217
+ })
218
+ }
219
+
220
+ export const dictionary = {
221
+ en: {
222
+ errorLoadScript: 'An error occurred while loading the script:',
223
+ hide: 'Hide chat button',
224
+ hiddenTitle: 'The chat button is now hidden',
225
+ hiddenMessage: 'To see it again, please select the option "Chat with us" in the menu just under this message.',
226
+ },
227
+ pt: {
228
+ errorLoadScript: 'Ocorreu um erro ao carregar o script:',
229
+ hide: 'Esconder o botão de chat',
230
+ hiddenTitle: 'O botão de chat foi escondido',
231
+ hiddenMessage: 'Para vê-lo novamente, por favor escolha a opção "Converse conosco" no menu logo em baixo desta mensagem.',
232
+ },
233
+ } satisfies Dictionary
@@ -1,30 +1,30 @@
1
- import { useEffect, useRef, useState } from 'react'
2
-
3
- /**
4
- * Checks if the text fits completely in the space available.
5
- * @returns true if it fits, false otherwise.
6
- */
7
- export function useCheckTextOverflow() {
8
- const [overflow, setOverflow] = useState<boolean>(false)
9
- const ref = useRef<HTMLParagraphElement>(null)
10
-
11
- const checkOverflow = () => {
12
- if (!ref.current) {
13
- return
14
- }
15
-
16
- const hasOverflow = ref.current.offsetWidth < ref.current.scrollWidth
17
-
18
- if (hasOverflow === overflow) {
19
- return
20
- }
21
-
22
- setOverflow(hasOverflow)
23
- }
24
-
25
- useEffect(() => {
26
- checkOverflow()
27
- }, [ref.current])
28
-
29
- return { overflow, ref }
30
- }
1
+ import { useEffect, useRef, useState } from 'react'
2
+
3
+ /**
4
+ * Checks if the text fits completely in the space available.
5
+ * @returns true if it fits, false otherwise.
6
+ */
7
+ export function useCheckTextOverflow() {
8
+ const [overflow, setOverflow] = useState<boolean>(false)
9
+ const ref = useRef<HTMLParagraphElement>(null)
10
+
11
+ const checkOverflow = () => {
12
+ if (!ref.current) {
13
+ return
14
+ }
15
+
16
+ const hasOverflow = ref.current.offsetWidth < ref.current.scrollWidth
17
+
18
+ if (hasOverflow === overflow) {
19
+ return
20
+ }
21
+
22
+ setOverflow(hasOverflow)
23
+ }
24
+
25
+ useEffect(() => {
26
+ checkOverflow()
27
+ }, [ref.current])
28
+
29
+ return { overflow, ref }
30
+ }
@@ -1,28 +1,28 @@
1
- import { useEffect } from 'react'
2
-
3
- let originalTitle: string | undefined
4
-
5
- /**
6
- * A React effect for prefixing the page title with the string passed as parameter followed by " - ".
7
- *
8
- * This returns a dispose function that changes the title back to its original value.
9
- * @param title the prefix to add to the page title.
10
- * @returns a dispose function.
11
- */
12
- export function titleEffect(title: string | undefined | null) {
13
- originalTitle ??= document.title
14
- document.title = title ? `${title} - ${originalTitle}` : originalTitle
15
- return () => {
16
- document.title = originalTitle ?? ''
17
- }
18
- }
19
-
20
- /**
21
- * Changes the page title to have the string passed as parameter as a prefix followed by " - " and the original title.
22
- *
23
- * Once disposed, this hook changes the title back to its original value.
24
- * @param title the prefix to add to the page title.
25
- */
26
- export function useTitleEffect(title: string | undefined | null) {
27
- useEffect(() => titleEffect(title), [title])
28
- }
1
+ import { useEffect } from 'react'
2
+
3
+ let originalTitle: string | undefined
4
+
5
+ /**
6
+ * A React effect for prefixing the page title with the string passed as parameter followed by " - ".
7
+ *
8
+ * This returns a dispose function that changes the title back to its original value.
9
+ * @param title the prefix to add to the page title.
10
+ * @returns a dispose function.
11
+ */
12
+ export function titleEffect(title: string | undefined | null) {
13
+ originalTitle ??= document.title
14
+ document.title = title ? `${title} - ${originalTitle}` : originalTitle
15
+ return () => {
16
+ document.title = originalTitle ?? ''
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Changes the page title to have the string passed as parameter as a prefix followed by " - " and the original title.
22
+ *
23
+ * Once disposed, this hook changes the title back to its original value.
24
+ * @param title the prefix to add to the page title.
25
+ */
26
+ export function useTitleEffect(title: string | undefined | null) {
27
+ useEffect(() => titleEffect(title), [title])
28
+ }
@@ -1,43 +1,43 @@
1
- import { useEffect, useRef, useState } from 'react'
2
-
3
- /**
4
- * Code taken from https://blog.ag-grid.com/avoiding-react-18-double-mount/
5
- *
6
- * Attention: don't use this hook unless you really have to!
7
- *
8
- * This hook fixes the React 18 behavior of calling useEffect hooks twice in strict/development mode, which ruins some mounting/unmounting
9
- * behaviors.
10
- *
11
- * @param effect refer to React's useEffect.
12
- */
13
- export const useEffectOnce = (effect: () => void | (() => void)) => {
14
- const effectFn = useRef<() => void | (() => void)>(effect)
15
- const destroyFn = useRef<void | (() => void)>()
16
- const effectCalled = useRef(false)
17
- const rendered = useRef(false)
18
- const [, setVal] = useState<number>(0)
19
-
20
- if (effectCalled.current) {
21
- rendered.current = true
22
- }
23
-
24
- useEffect(() => {
25
- // only execute the effect first time around
26
- if (!effectCalled.current) {
27
- destroyFn.current = effectFn.current()
28
- effectCalled.current = true
29
- }
30
-
31
- // this forces one render after the effect is run
32
- setVal((val) => val + 1)
33
-
34
- return () => {
35
- // if the comp didn't render since the useEffect was called,
36
- // we know it's the dummy React cycle
37
- if (!rendered.current) return
38
-
39
- // otherwise this is not a dummy destroy, so call the destroy func
40
- if (destroyFn.current) destroyFn.current()
41
- }
42
- }, [])
43
- }
1
+ import { useEffect, useRef, useState } from 'react'
2
+
3
+ /**
4
+ * Code taken from https://blog.ag-grid.com/avoiding-react-18-double-mount/
5
+ *
6
+ * Attention: don't use this hook unless you really have to!
7
+ *
8
+ * This hook fixes the React 18 behavior of calling useEffect hooks twice in strict/development mode, which ruins some mounting/unmounting
9
+ * behaviors.
10
+ *
11
+ * @param effect refer to React's useEffect.
12
+ */
13
+ export const useEffectOnce = (effect: () => void | (() => void)) => {
14
+ const effectFn = useRef<() => void | (() => void)>(effect)
15
+ const destroyFn = useRef<void | (() => void)>()
16
+ const effectCalled = useRef(false)
17
+ const rendered = useRef(false)
18
+ const [, setVal] = useState<number>(0)
19
+
20
+ if (effectCalled.current) {
21
+ rendered.current = true
22
+ }
23
+
24
+ useEffect(() => {
25
+ // only execute the effect first time around
26
+ if (!effectCalled.current) {
27
+ destroyFn.current = effectFn.current()
28
+ effectCalled.current = true
29
+ }
30
+
31
+ // this forces one render after the effect is run
32
+ setVal((val) => val + 1)
33
+
34
+ return () => {
35
+ // if the comp didn't render since the useEffect was called,
36
+ // we know it's the dummy React cycle
37
+ if (!rendered.current) return
38
+
39
+ // otherwise this is not a dummy destroy, so call the destroy func
40
+ if (destroyFn.current) destroyFn.current()
41
+ }
42
+ }, [])
43
+ }