@seamly/web-ui 21.0.9 → 22.0.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.
- package/build/dist/lib/components.js +9228 -7777
- package/build/dist/lib/components.js.map +1 -0
- package/build/dist/lib/components.min.js +2 -1
- package/build/dist/lib/components.min.js.LICENSE.txt +2 -2
- package/build/dist/lib/components.min.js.map +1 -0
- package/build/dist/lib/config.js +2 -1
- package/build/dist/lib/config.js.map +1 -0
- package/build/dist/lib/config.min.js +2 -1
- package/build/dist/lib/config.min.js.map +1 -0
- package/build/dist/lib/contexts.js +2 -1
- package/build/dist/lib/contexts.js.map +1 -0
- package/build/dist/lib/contexts.min.js +2 -1
- package/build/dist/lib/contexts.min.js.map +1 -0
- package/build/dist/lib/deprecated-view.css +1 -1
- package/build/dist/lib/deprecated-view.js +1 -1
- package/build/dist/lib/hooks.js +6999 -5996
- package/build/dist/lib/hooks.js.map +1 -0
- package/build/dist/lib/hooks.min.js +2 -1
- package/build/dist/lib/hooks.min.js.map +1 -0
- package/build/dist/lib/index.debug.js +940 -370
- package/build/dist/lib/index.debug.js.map +1 -0
- package/build/dist/lib/index.debug.min.js +2 -1
- package/build/dist/lib/index.debug.min.js.LICENSE.txt +334 -110
- package/build/dist/lib/index.debug.min.js.map +1 -0
- package/build/dist/lib/index.js +2810 -5472
- package/build/dist/lib/index.js.map +1 -0
- package/build/dist/lib/index.min.js +2 -1
- package/build/dist/lib/index.min.js.LICENSE.txt +2 -2
- package/build/dist/lib/index.min.js.map +1 -0
- package/build/dist/lib/sounds/beep.mp3 +0 -0
- package/build/dist/lib/standalone.js +10575 -13540
- package/build/dist/lib/standalone.js.map +1 -0
- package/build/dist/lib/standalone.min.js +2 -1
- package/build/dist/lib/standalone.min.js.LICENSE.txt +1 -1
- package/build/dist/lib/standalone.min.js.map +1 -0
- package/build/dist/lib/storage.js +2 -1
- package/build/dist/lib/storage.js.map +1 -0
- package/build/dist/lib/storage.min.js +2 -1
- package/build/dist/lib/storage.min.js.map +1 -0
- package/build/dist/lib/style-guide.js +1701 -5859
- package/build/dist/lib/style-guide.js.map +1 -0
- package/build/dist/lib/style-guide.min.js +2 -1
- package/build/dist/lib/style-guide.min.js.LICENSE.txt +2 -2
- package/build/dist/lib/style-guide.min.js.map +1 -0
- package/build/dist/lib/styles-default-implementation.css +1 -1
- package/build/dist/lib/styles-default-implementation.js +1 -1
- package/build/dist/lib/styles.css +1 -1
- package/build/dist/lib/styles.js +1 -1
- package/build/dist/lib/utils.js +11536 -14530
- package/build/dist/lib/utils.js.map +1 -0
- package/build/dist/lib/utils.min.js +2 -1
- package/build/dist/lib/utils.min.js.LICENSE.txt +1 -6
- package/build/dist/lib/utils.min.js.map +1 -0
- package/package.json +58 -48
- package/src/javascripts/api/conversation-connector.ts +2 -0
- package/src/javascripts/api/errors/seamly-api-error.ts +15 -0
- package/src/javascripts/api/index.ts +168 -94
- package/src/javascripts/config.ts +1 -1
- package/src/javascripts/config.types.ts +18 -11
- package/src/javascripts/domains/config/selectors.ts +1 -1
- package/src/javascripts/domains/config/slice.ts +12 -0
- package/src/javascripts/domains/forms/forms.types.ts +1 -0
- package/src/javascripts/domains/forms/hooks.ts +10 -2
- package/src/javascripts/domains/store/selectors.ts +23 -10
- package/src/javascripts/domains/store/slice.ts +63 -11
- package/src/javascripts/domains/store/store.types.ts +41 -1
- package/src/javascripts/domains/translations/components/options-button.tsx +1 -4
- package/src/javascripts/domains/translations/hooks.ts +11 -4
- package/src/javascripts/index.ts +2 -0
- package/src/javascripts/lib/url-helpers.ts +24 -0
- package/src/javascripts/schema.ts +10 -0
- package/src/javascripts/style-guide/states.js +109 -0
- package/src/javascripts/ui/components/conversation/conversation.tsx +2 -0
- package/src/javascripts/ui/components/conversation/event/chat-scroll/chat-scroll-provider.tsx +2 -0
- package/src/javascripts/ui/components/conversation/event/choice-prompt.js +1 -1
- package/src/javascripts/ui/components/conversation/event/text.js +1 -1
- package/src/javascripts/ui/components/conversation/event/upload.js +50 -9
- package/src/javascripts/ui/components/conversation/use-chat-scroll.ts +3 -2
- package/src/javascripts/ui/components/core/seamly-event-subscriber.ts +7 -1
- package/src/javascripts/ui/components/core/seamly-file-upload.tsx +156 -0
- package/src/javascripts/ui/components/entry/abort-transaction-button/abort-transaction-button.tsx +45 -0
- package/src/javascripts/ui/components/entry/text-entry/hooks.ts +108 -0
- package/src/javascripts/ui/components/entry/text-entry/index.js +7 -4
- package/src/javascripts/ui/components/entry/text-entry/{text-entry-form.js → text-entry-form.tsx} +8 -22
- package/src/javascripts/ui/components/form-controls/{input.js → input.tsx} +13 -2
- package/src/javascripts/ui/components/form-controls/{wrapper.js → wrapper.tsx} +8 -4
- package/src/javascripts/ui/components/view/{index.js → index.tsx} +53 -4
- package/src/javascripts/ui/components/view/window-view/{index.js → index.tsx} +14 -2
- package/src/javascripts/ui/components/widgets/{in-out-transition.js → in-out-transition.tsx} +67 -28
- package/src/javascripts/ui/hooks/{seamly-api-hooks.js → seamly-api-hooks.ts} +1 -1
- package/src/javascripts/ui/hooks/sounds/beep.mp3 +0 -0
- package/src/javascripts/ui/hooks/use-click-outside.ts +5 -3
- package/src/javascripts/ui/hooks/use-notifications.ts +114 -0
- package/src/javascripts/ui/hooks/use-timeout.ts +20 -0
- package/src/stylesheets/3-chat/_chat.scss +3 -5
- package/src/stylesheets/4-base/_formelements.scss +0 -36
- package/src/stylesheets/5-components/_abort-transaction.scss +10 -0
- package/src/stylesheets/5-components/_buttons.scss +18 -3
- package/src/stylesheets/5-components/_character-limit.scss +2 -2
- package/src/stylesheets/5-components/_chat-status.scss +26 -37
- package/src/stylesheets/5-components/_choice-prompt.scss +9 -10
- package/src/stylesheets/5-components/_conversation.scss +9 -62
- package/src/stylesheets/5-components/_disclaimer.scss +11 -3
- package/src/stylesheets/5-components/_error.scss +3 -2
- package/src/stylesheets/5-components/_idle.scss +3 -8
- package/src/stylesheets/5-components/_input.scss +34 -13
- package/src/stylesheets/5-components/_interrupt.scss +3 -10
- package/src/stylesheets/5-components/_loader.scss +1 -2
- package/src/stylesheets/5-components/_message-author.scss +2 -4
- package/src/stylesheets/5-components/_message-body.scss +33 -10
- package/src/stylesheets/5-components/_message-card.scss +2 -10
- package/src/stylesheets/5-components/_message-carousel.scss +4 -4
- package/src/stylesheets/5-components/_message-cta.scss +0 -6
- package/src/stylesheets/5-components/_message.scss +1 -0
- package/src/stylesheets/5-components/_modal.scss +2 -5
- package/src/stylesheets/5-components/_options.scss +17 -22
- package/src/stylesheets/5-components/_pre-chat-messages.scss +3 -1
- package/src/stylesheets/5-components/_prompt.scss +3 -7
- package/src/stylesheets/5-components/_skip-link.scss +2 -1
- package/src/stylesheets/5-components/_suggestions.scss +2 -2
- package/src/stylesheets/5-components/_translation-options.scss +5 -2
- package/src/stylesheets/5-components/_unread-messages.scss +33 -0
- package/src/stylesheets/5-components/_upload.scss +20 -27
- package/src/stylesheets/6-default-implementation/_hover.scss +14 -17
- package/src/stylesheets/7-deprecated/1-settings/_config.scss +17 -0
- package/src/stylesheets/7-deprecated/3-app/_app.scss +2 -1
- package/src/stylesheets/7-deprecated/5-components/_card.scss +1 -0
- package/src/stylesheets/7-deprecated/5-components/_chat-status.scss +66 -20
- package/src/stylesheets/7-deprecated/5-components/_conversation.scss +1 -4
- package/src/stylesheets/7-deprecated/5-components/_input.scss +6 -1
- package/src/stylesheets/7-deprecated/5-components/_interrupt.scss +1 -4
- package/src/stylesheets/7-deprecated/5-components/_message.scss +49 -12
- package/src/stylesheets/7-deprecated/5-components/_translation-options.scss +30 -37
- package/src/stylesheets/7-deprecated/5-components/_unread-messages.scss +38 -0
- package/src/stylesheets/deprecated-view.scss +1 -0
- package/src/stylesheets/styles.scss +2 -0
- package/webpack/config.common.js +6 -1
- package/webpack/config.package.js +18 -0
- package/webpack/defaults.js +1 -1
- package/src/javascripts/ui/components/core/seamly-file-upload.js +0 -86
- package/src/javascripts/ui/components/entry/text-entry/hooks.js +0 -46
|
@@ -31,7 +31,7 @@ export const useChoicePrompt = (event) => {
|
|
|
31
31
|
body: event.payload.body?.prompt,
|
|
32
32
|
translatedBody: event.payload.translatedBody && {
|
|
33
33
|
...event.payload.translatedBody,
|
|
34
|
-
data: event.payload.translatedBody.
|
|
34
|
+
data: event.payload.translatedBody.prompt,
|
|
35
35
|
},
|
|
36
36
|
},
|
|
37
37
|
}
|
|
@@ -1,16 +1,53 @@
|
|
|
1
1
|
import { useMemo } from 'preact/hooks'
|
|
2
|
+
import { useSelector } from 'react-redux'
|
|
2
3
|
import MessageContainer from 'ui/components/conversation/message-container'
|
|
3
4
|
import Icon from 'ui/components/layout/icon'
|
|
4
5
|
import { useI18n } from 'domains/i18n/hooks'
|
|
5
6
|
import { useTranslatedEventData } from 'domains/translations/hooks'
|
|
6
7
|
import { className } from 'lib/css'
|
|
7
8
|
|
|
9
|
+
const PROCESSING_IMAGE = 'PROCESSING_IMAGE'
|
|
10
|
+
|
|
11
|
+
const useImageFromStorage = (currentFileId) => {
|
|
12
|
+
const { processingFileUploads } = useSelector(({ state }) => state)
|
|
13
|
+
|
|
14
|
+
return useMemo(() => {
|
|
15
|
+
const isProcessingImg = processingFileUploads.some(
|
|
16
|
+
(fileId) => fileId === currentFileId,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
if (isProcessingImg) return PROCESSING_IMAGE
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
return sessionStorage.getItem(`image-${currentFileId}`)
|
|
23
|
+
} catch (error) {
|
|
24
|
+
return undefined
|
|
25
|
+
}
|
|
26
|
+
}, [currentFileId, processingFileUploads])
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const UploadedImage = ({ img, filename }) => {
|
|
30
|
+
const { t } = useI18n()
|
|
31
|
+
const srText = t('fileUpload.srFileUploadedText', { fileName: filename })
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<>
|
|
35
|
+
<span className={className(['download', 'download--preview'])}>
|
|
36
|
+
<img src={img} alt={srText} />
|
|
37
|
+
<span aria-hidden="true" className={className('file-download')}>
|
|
38
|
+
{filename}
|
|
39
|
+
</span>
|
|
40
|
+
</span>
|
|
41
|
+
</>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
8
45
|
const UploadContent = ({ children, url, target }) =>
|
|
9
46
|
url ? (
|
|
10
47
|
<a
|
|
11
48
|
href={url}
|
|
12
49
|
download
|
|
13
|
-
target={target
|
|
50
|
+
target={target}
|
|
14
51
|
className={className(['download', 'download-link'])}
|
|
15
52
|
>
|
|
16
53
|
{children}
|
|
@@ -22,8 +59,9 @@ const UploadContent = ({ children, url, target }) =>
|
|
|
22
59
|
const Upload = ({ event, ...props }) => {
|
|
23
60
|
const { t } = useI18n()
|
|
24
61
|
const { body } = useTranslatedEventData(event)
|
|
25
|
-
const { fromClient } = event.payload
|
|
62
|
+
const { fromClient, id } = event.payload
|
|
26
63
|
const { filename, url } = body
|
|
64
|
+
const img = useImageFromStorage(id)
|
|
27
65
|
|
|
28
66
|
const srText = useMemo(
|
|
29
67
|
() =>
|
|
@@ -35,13 +73,16 @@ const Upload = ({ event, ...props }) => {
|
|
|
35
73
|
|
|
36
74
|
return (
|
|
37
75
|
<MessageContainer event={event} type="upload" {...props}>
|
|
38
|
-
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
76
|
+
{img && img !== PROCESSING_IMAGE ? (
|
|
77
|
+
<UploadedImage img={img} filename={filename} />
|
|
78
|
+
) : (
|
|
79
|
+
<UploadContent url={url} target={!fromClient ? '_blank' : undefined}>
|
|
80
|
+
<Icon name="download" size="16" alt={srText} />
|
|
81
|
+
<span aria-hidden="true" className={className('file-download')}>
|
|
82
|
+
{filename}
|
|
83
|
+
</span>
|
|
84
|
+
</UploadContent>
|
|
85
|
+
)}
|
|
45
86
|
</MessageContainer>
|
|
46
87
|
)
|
|
47
88
|
}
|
|
@@ -37,8 +37,8 @@ const useChatScroll = (
|
|
|
37
37
|
const isLoading = useSeamlyIsLoading()
|
|
38
38
|
const { isOpen } = useVisibility()
|
|
39
39
|
const loadedImageEventIds = useLoadedImageEventIds()
|
|
40
|
-
const isLastEventFromClient = useSelector(
|
|
41
|
-
(state: RootState) => state
|
|
40
|
+
const { processingFileUploads, isLastEventFromClient } = useSelector(
|
|
41
|
+
({ state }: RootState) => state,
|
|
42
42
|
)
|
|
43
43
|
|
|
44
44
|
useEffect(() => {
|
|
@@ -84,6 +84,7 @@ const useChatScroll = (
|
|
|
84
84
|
isLoading,
|
|
85
85
|
isOpen,
|
|
86
86
|
loadedImageEventIds,
|
|
87
|
+
processingFileUploads,
|
|
87
88
|
scrollToBottom,
|
|
88
89
|
])
|
|
89
90
|
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
setHistory,
|
|
25
25
|
setIsLoading,
|
|
26
26
|
setParticipant,
|
|
27
|
+
setProactiveMessages,
|
|
27
28
|
setServiceDataItem,
|
|
28
29
|
setServiceEntryMetadata,
|
|
29
30
|
} from 'domains/store/slice'
|
|
@@ -201,7 +202,7 @@ const SeamlyEventSubscriber = () => {
|
|
|
201
202
|
case 'system':
|
|
202
203
|
if (payload.type === 'service_changed') {
|
|
203
204
|
const { serviceSettings, ...eventPayload } = payload
|
|
204
|
-
const { entry } = serviceSettings
|
|
205
|
+
const { entry, proactiveMessages } = serviceSettings
|
|
205
206
|
const { upload } = entry.options
|
|
206
207
|
|
|
207
208
|
dispatch(
|
|
@@ -211,6 +212,10 @@ const SeamlyEventSubscriber = () => {
|
|
|
211
212
|
}),
|
|
212
213
|
)
|
|
213
214
|
|
|
215
|
+
dispatch(
|
|
216
|
+
setProactiveMessages(proactiveMessages.enabled || false),
|
|
217
|
+
)
|
|
218
|
+
|
|
214
219
|
dispatch(setServiceEntryMetadata(entry))
|
|
215
220
|
if (payload.serviceSessionId) {
|
|
216
221
|
dispatch(setActiveService(payload.serviceSessionId))
|
|
@@ -244,6 +249,7 @@ const SeamlyEventSubscriber = () => {
|
|
|
244
249
|
)
|
|
245
250
|
break
|
|
246
251
|
case 'conversation_erred':
|
|
252
|
+
case 'attach_channel_erred':
|
|
247
253
|
const seamlyGeneralError = new SeamlyGeneralError(event)
|
|
248
254
|
dispatch(
|
|
249
255
|
setInterrupt({
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { useCallback } from 'preact/hooks'
|
|
2
|
+
import { useDispatch } from 'react-redux'
|
|
3
|
+
import SeamlyFileUploadContext from 'ui/components/core/seamly-file-upload-context'
|
|
4
|
+
import { useSeamlyApiContext, useSeamlyCommands } from 'ui/hooks/seamly-hooks'
|
|
5
|
+
import { useI18n } from 'domains/i18n/hooks'
|
|
6
|
+
import {
|
|
7
|
+
doneProcessingImage,
|
|
8
|
+
registerUpload,
|
|
9
|
+
setUploadComplete,
|
|
10
|
+
setUploadError,
|
|
11
|
+
setUploadProgress,
|
|
12
|
+
startProcessingImage,
|
|
13
|
+
} from 'domains/store/slice'
|
|
14
|
+
import { MessageUpload } from 'domains/store/store.types'
|
|
15
|
+
import { randomId } from 'lib/id'
|
|
16
|
+
|
|
17
|
+
const calculateImgSize = (img) => {
|
|
18
|
+
const MAX_WIDTH = 600
|
|
19
|
+
const MAX_HEIGHT = 600
|
|
20
|
+
|
|
21
|
+
const { height, width } = img
|
|
22
|
+
if (width > height && width > MAX_WIDTH) {
|
|
23
|
+
return [MAX_WIDTH, (height * MAX_WIDTH) / width]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (height > MAX_HEIGHT) {
|
|
27
|
+
return [(width * MAX_HEIGHT) / height, MAX_HEIGHT]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return [width, height]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const resizeImage = (img) => {
|
|
34
|
+
const canvas = document.createElement('canvas')
|
|
35
|
+
const ctx = canvas.getContext('2d')
|
|
36
|
+
|
|
37
|
+
const [width, height] = calculateImgSize(img)
|
|
38
|
+
canvas.width = width
|
|
39
|
+
canvas.height = height
|
|
40
|
+
|
|
41
|
+
ctx.drawImage(img, 0, 0, width, height)
|
|
42
|
+
|
|
43
|
+
return canvas.toDataURL()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const toBase64 = (file): Promise<string> =>
|
|
47
|
+
new Promise((resolve, reject) => {
|
|
48
|
+
const reader = new FileReader()
|
|
49
|
+
reader.readAsDataURL(file)
|
|
50
|
+
reader.onload = () =>
|
|
51
|
+
typeof reader.result === 'string' ? resolve(reader.result) : undefined
|
|
52
|
+
reader.onerror = (error) => reject(error)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const SeamlyFileUpload = ({ children }) => {
|
|
56
|
+
const { t } = useI18n()
|
|
57
|
+
const dispatch = useDispatch()
|
|
58
|
+
const api = useSeamlyApiContext()
|
|
59
|
+
const { addUploadBubble } = useSeamlyCommands()
|
|
60
|
+
|
|
61
|
+
const addImageToSessionStorage = useCallback(
|
|
62
|
+
async (file, fileId) => {
|
|
63
|
+
dispatch(startProcessingImage(fileId))
|
|
64
|
+
const base64 = await toBase64(file)
|
|
65
|
+
const img = new Image()
|
|
66
|
+
|
|
67
|
+
img.src = base64
|
|
68
|
+
|
|
69
|
+
await img.decode()
|
|
70
|
+
const newDataUri = resizeImage(img)
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
sessionStorage.setItem(`image-${fileId}`, newDataUri)
|
|
74
|
+
} catch (error) {
|
|
75
|
+
// Nothing to do!
|
|
76
|
+
} finally {
|
|
77
|
+
dispatch(doneProcessingImage(fileId))
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
[dispatch],
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const onUploadFileHandler = useCallback(
|
|
84
|
+
(file) => {
|
|
85
|
+
const fileId = randomId()
|
|
86
|
+
|
|
87
|
+
const uploadHandle = api.uploadFile(
|
|
88
|
+
file,
|
|
89
|
+
(p) => {
|
|
90
|
+
dispatch(setUploadProgress({ fileId, progress: Math.ceil(p) }))
|
|
91
|
+
},
|
|
92
|
+
async (result: MessageUpload) => {
|
|
93
|
+
const {
|
|
94
|
+
id,
|
|
95
|
+
transactionId,
|
|
96
|
+
occurredAt,
|
|
97
|
+
body: { contentType, filename, filesize, url },
|
|
98
|
+
} = result
|
|
99
|
+
|
|
100
|
+
dispatch(setUploadComplete(fileId))
|
|
101
|
+
addUploadBubble(
|
|
102
|
+
id,
|
|
103
|
+
transactionId,
|
|
104
|
+
occurredAt,
|
|
105
|
+
contentType,
|
|
106
|
+
filename,
|
|
107
|
+
filesize,
|
|
108
|
+
url,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
await addImageToSessionStorage(file, id)
|
|
112
|
+
},
|
|
113
|
+
(err) => {
|
|
114
|
+
const errorKey = err?.error || ''
|
|
115
|
+
let errorText
|
|
116
|
+
|
|
117
|
+
switch (errorKey) {
|
|
118
|
+
case 'file_uploads_are_disabled':
|
|
119
|
+
errorText = t('fileUpload.errors.unavailable')
|
|
120
|
+
break
|
|
121
|
+
case 'request_entity_too_large':
|
|
122
|
+
errorText = t('fileUpload.errors.tooLarge')
|
|
123
|
+
break
|
|
124
|
+
case 'file_has_invalid_mime_type':
|
|
125
|
+
errorText = t('fileUpload.errors.wrongType')
|
|
126
|
+
break
|
|
127
|
+
case 'virus_found':
|
|
128
|
+
errorText = t('fileUpload.errors.virusFound')
|
|
129
|
+
break
|
|
130
|
+
default:
|
|
131
|
+
errorText = t('fileUpload.errors.general')
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
dispatch(setUploadError({ fileId, errorText }))
|
|
135
|
+
},
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
dispatch(
|
|
139
|
+
registerUpload({
|
|
140
|
+
fileId,
|
|
141
|
+
fileName: file.name,
|
|
142
|
+
uploadHandle,
|
|
143
|
+
}),
|
|
144
|
+
)
|
|
145
|
+
},
|
|
146
|
+
[addImageToSessionStorage, addUploadBubble, api, dispatch, t],
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<SeamlyFileUploadContext.Provider value={onUploadFileHandler}>
|
|
151
|
+
{children}
|
|
152
|
+
</SeamlyFileUploadContext.Provider>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export default SeamlyFileUpload
|
package/src/javascripts/ui/components/entry/abort-transaction-button/abort-transaction-button.tsx
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useSeamlyApiContext } from 'ui/hooks/seamly-api-hooks'
|
|
2
|
+
import { actionTypes } from 'ui/utils/seamly-utils'
|
|
3
|
+
import { className } from 'lib/css'
|
|
4
|
+
import { useEntryAbortTransaction } from '../text-entry/hooks'
|
|
5
|
+
|
|
6
|
+
export default function AbortTransactionButton() {
|
|
7
|
+
const { abortTransaction, clearEntryAbortTransaction } =
|
|
8
|
+
useEntryAbortTransaction()
|
|
9
|
+
const api = useSeamlyApiContext()
|
|
10
|
+
|
|
11
|
+
if (!abortTransaction) return null
|
|
12
|
+
|
|
13
|
+
const handleAbortTransaction = () => {
|
|
14
|
+
api.send('action', {
|
|
15
|
+
type: actionTypes.setTopic,
|
|
16
|
+
body: {
|
|
17
|
+
name: abortTransaction.topicName,
|
|
18
|
+
fallbackMessage: abortTransaction.topicFallbackMessage,
|
|
19
|
+
},
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
clearEntryAbortTransaction()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<li
|
|
27
|
+
className={className([
|
|
28
|
+
'cvco-conversation__item',
|
|
29
|
+
'cvco-conversation__item--abort-transaction',
|
|
30
|
+
])}
|
|
31
|
+
>
|
|
32
|
+
<button
|
|
33
|
+
className={className([
|
|
34
|
+
'button',
|
|
35
|
+
'button--secondary',
|
|
36
|
+
'abort-transaction__button',
|
|
37
|
+
])}
|
|
38
|
+
type="button"
|
|
39
|
+
onClick={handleAbortTransaction}
|
|
40
|
+
>
|
|
41
|
+
{abortTransaction.label}
|
|
42
|
+
</button>
|
|
43
|
+
</li>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { useEffect, useMemo } from 'preact/hooks'
|
|
2
|
+
import { useDispatch } from 'react-redux'
|
|
3
|
+
import { maxCharacterSrDebounceDelay, maxCharacterWarningLimit } from 'config'
|
|
4
|
+
import { useLiveRegion } from 'ui/hooks/live-region-hooks'
|
|
5
|
+
import {
|
|
6
|
+
useEntryTextLimit,
|
|
7
|
+
useSeamlyStateContext,
|
|
8
|
+
} from 'ui/hooks/seamly-state-hooks'
|
|
9
|
+
import { debounce } from 'ui/utils/general-utils'
|
|
10
|
+
import { ControlState } from 'domains/forms/forms.types'
|
|
11
|
+
import { useFormControl } from 'domains/forms/hooks'
|
|
12
|
+
import { useI18n } from 'domains/i18n/hooks'
|
|
13
|
+
import { clearAbortTransaction } from 'domains/store/slice'
|
|
14
|
+
|
|
15
|
+
export function useCharacterLimit(controlName) {
|
|
16
|
+
const { t } = useI18n()
|
|
17
|
+
const { sendAssertive } = useLiveRegion()
|
|
18
|
+
const { hasLimit, limit } = useEntryTextLimit()
|
|
19
|
+
|
|
20
|
+
const debouncedSendAssertive = useMemo(
|
|
21
|
+
() => debounce(sendAssertive, maxCharacterSrDebounceDelay),
|
|
22
|
+
[sendAssertive],
|
|
23
|
+
)
|
|
24
|
+
const validateLimit = useMemo(() => {
|
|
25
|
+
return debounce((_reachedCharacterWarning, _remainingChars) => {
|
|
26
|
+
if (_reachedCharacterWarning) {
|
|
27
|
+
debouncedSendAssertive(
|
|
28
|
+
t('input.srCharacterLimitText', { limit: _remainingChars }),
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
}, maxCharacterSrDebounceDelay)
|
|
32
|
+
}, [debouncedSendAssertive, t])
|
|
33
|
+
|
|
34
|
+
const [{ value }] = useFormControl(controlName)
|
|
35
|
+
const remainingChars = hasLimit && value ? limit - value.length : limit
|
|
36
|
+
const reachedCharacterWarning = hasLimit
|
|
37
|
+
? remainingChars <= maxCharacterWarningLimit
|
|
38
|
+
: false
|
|
39
|
+
const reachedCharacterLimit = hasLimit ? remainingChars < 0 : false
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
validateLimit(reachedCharacterWarning, remainingChars)
|
|
43
|
+
}, [reachedCharacterWarning, remainingChars, validateLimit])
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
hasCharacterLimit: hasLimit,
|
|
47
|
+
characterLimit: limit,
|
|
48
|
+
reachedCharacterWarning,
|
|
49
|
+
reachedCharacterLimit,
|
|
50
|
+
remainingChars,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const useEntryTextTranslation = (controlName: ControlState['name']) => {
|
|
55
|
+
const { hasCharacterLimit, characterLimit } = useCharacterLimit(controlName)
|
|
56
|
+
|
|
57
|
+
const {
|
|
58
|
+
entryMeta: {
|
|
59
|
+
optionsOverride: { text },
|
|
60
|
+
},
|
|
61
|
+
} = useSeamlyStateContext()
|
|
62
|
+
|
|
63
|
+
const { t } = useI18n()
|
|
64
|
+
|
|
65
|
+
const placeholder: string = useMemo(
|
|
66
|
+
() =>
|
|
67
|
+
t('input.inputPlaceholder', {
|
|
68
|
+
hasLimit: hasCharacterLimit,
|
|
69
|
+
text: text?.placeholder || t('input.inputPlaceholderText'),
|
|
70
|
+
limit: hasCharacterLimit ? characterLimit : null,
|
|
71
|
+
}),
|
|
72
|
+
[t, hasCharacterLimit, characterLimit, text?.placeholder],
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
const label: string = useMemo(
|
|
76
|
+
() =>
|
|
77
|
+
t('input.inputLabel', {
|
|
78
|
+
hasLimit: !text?.label ? hasCharacterLimit : false,
|
|
79
|
+
text: text?.label || t('input.inputLabelText'),
|
|
80
|
+
limit: !text?.label && hasCharacterLimit ? characterLimit : null,
|
|
81
|
+
}),
|
|
82
|
+
[t, hasCharacterLimit, characterLimit, text?.label],
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
const labelClass: string = useMemo(
|
|
86
|
+
() => (text?.label ? 'input__label' : 'visually-hidden'),
|
|
87
|
+
[text?.label],
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return { placeholder, label, labelClass }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const useEntryAbortTransaction = () => {
|
|
94
|
+
const dispatch = useDispatch()
|
|
95
|
+
|
|
96
|
+
const {
|
|
97
|
+
entryMeta: { actions },
|
|
98
|
+
} = useSeamlyStateContext()
|
|
99
|
+
|
|
100
|
+
const clearEntryAbortTransaction = () => {
|
|
101
|
+
dispatch(clearAbortTransaction())
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
abortTransaction: actions?.abortTransaction,
|
|
106
|
+
clearEntryAbortTransaction,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -9,7 +9,7 @@ import { visibilityStates } from 'domains/visibility/constants'
|
|
|
9
9
|
import { useVisibility } from 'domains/visibility/hooks'
|
|
10
10
|
import TextEntryForm from './text-entry-form'
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
export const textEntryControlName = 'textMessageEntry'
|
|
13
13
|
|
|
14
14
|
export default function TextEntry({ ...props }) {
|
|
15
15
|
const { isOpen, setVisibility } = useVisibility()
|
|
@@ -18,8 +18,8 @@ export default function TextEntry({ ...props }) {
|
|
|
18
18
|
const { sendMessage } = useSeamlyCommands()
|
|
19
19
|
const handleSubmit = useCallback(
|
|
20
20
|
(values, { updateControlValue }) => {
|
|
21
|
-
sendMessage({ body: values[
|
|
22
|
-
updateControlValue(
|
|
21
|
+
sendMessage({ body: values[textEntryControlName] })
|
|
22
|
+
updateControlValue(textEntryControlName, '')
|
|
23
23
|
focusSkipLinkTarget()
|
|
24
24
|
|
|
25
25
|
if (!isOpen) {
|
|
@@ -36,7 +36,10 @@ export default function TextEntry({ ...props }) {
|
|
|
36
36
|
persistData={true}
|
|
37
37
|
onSubmit={handleSubmit}
|
|
38
38
|
>
|
|
39
|
-
<TextEntryForm
|
|
39
|
+
<TextEntryForm
|
|
40
|
+
controlName={textEntryControlName}
|
|
41
|
+
skipLinkId={skipLinkId}
|
|
42
|
+
/>
|
|
40
43
|
</FormProvider>
|
|
41
44
|
)
|
|
42
45
|
}
|
package/src/javascripts/ui/components/entry/text-entry/{text-entry-form.js → text-entry-form.tsx}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useLayoutEffect
|
|
1
|
+
import { useCallback, useLayoutEffect } from 'preact/hooks'
|
|
2
2
|
import Form from 'ui/components/form-controls/form'
|
|
3
3
|
import Input from 'ui/components/form-controls/input'
|
|
4
4
|
import Icon from 'ui/components/layout/icon'
|
|
@@ -7,7 +7,7 @@ import { useLiveRegion, useSeamlyCommands } from 'ui/hooks/seamly-hooks'
|
|
|
7
7
|
import { useFormControl } from 'domains/forms/hooks'
|
|
8
8
|
import { useI18n } from 'domains/i18n/hooks'
|
|
9
9
|
import { className } from 'lib/css'
|
|
10
|
-
import { useCharacterLimit } from './hooks'
|
|
10
|
+
import { useCharacterLimit, useEntryTextTranslation } from './hooks'
|
|
11
11
|
|
|
12
12
|
export default function TextEntryForm({ controlName, skipLinkId }) {
|
|
13
13
|
const { t } = useI18n()
|
|
@@ -15,16 +15,17 @@ export default function TextEntryForm({ controlName, skipLinkId }) {
|
|
|
15
15
|
const { emitEvent } = useSeamlyCommands()
|
|
16
16
|
const handleKeyUp = useSeamlyTyping()
|
|
17
17
|
const { setBlockAutoEntrySwitch } = useSeamlyEntry()
|
|
18
|
-
|
|
18
|
+
const { placeholder, label, labelClass } =
|
|
19
|
+
useEntryTextTranslation(controlName)
|
|
19
20
|
// TODO: Standardize the validation on form fields
|
|
20
21
|
const {
|
|
21
22
|
hasCharacterLimit,
|
|
22
|
-
characterLimit,
|
|
23
23
|
reachedCharacterWarning,
|
|
24
24
|
reachedCharacterLimit,
|
|
25
25
|
remainingChars,
|
|
26
26
|
} = useCharacterLimit(controlName)
|
|
27
27
|
const [{ value }] = useFormControl(controlName)
|
|
28
|
+
|
|
28
29
|
const hasValue = !!value
|
|
29
30
|
|
|
30
31
|
const handleFocus = useCallback(() => {
|
|
@@ -34,22 +35,7 @@ export default function TextEntryForm({ controlName, skipLinkId }) {
|
|
|
34
35
|
|
|
35
36
|
emitEvent('ui.inputFocus')
|
|
36
37
|
}, [t, sendAssertive, reachedCharacterWarning, remainingChars, emitEvent])
|
|
37
|
-
|
|
38
|
-
() =>
|
|
39
|
-
t('input.inputPlaceholder', {
|
|
40
|
-
hasLimit: hasCharacterLimit,
|
|
41
|
-
limit: hasCharacterLimit ? characterLimit : null,
|
|
42
|
-
}),
|
|
43
|
-
[t, hasCharacterLimit, characterLimit],
|
|
44
|
-
)
|
|
45
|
-
const labelText = useMemo(
|
|
46
|
-
() =>
|
|
47
|
-
t('input.inputLabel', {
|
|
48
|
-
hasLimit: hasCharacterLimit,
|
|
49
|
-
limit: hasCharacterLimit ? characterLimit : null,
|
|
50
|
-
}),
|
|
51
|
-
[t, hasCharacterLimit, characterLimit],
|
|
52
|
-
)
|
|
38
|
+
|
|
53
39
|
// When the input holds a value, the component should be blocked from switching
|
|
54
40
|
// to file upload form.
|
|
55
41
|
useLayoutEffect(() => {
|
|
@@ -87,8 +73,8 @@ export default function TextEntryForm({ controlName, skipLinkId }) {
|
|
|
87
73
|
className={className('input__text')}
|
|
88
74
|
autocomplete="off"
|
|
89
75
|
placeholder={placeholder}
|
|
90
|
-
labelText={
|
|
91
|
-
labelClass={className(
|
|
76
|
+
labelText={label}
|
|
77
|
+
labelClass={className(labelClass)}
|
|
92
78
|
aria-invalid={hasCharacterLimit ? reachedCharacterLimit : null}
|
|
93
79
|
onKeyUp={handleKeyUp}
|
|
94
80
|
onFocus={handleFocus}
|
|
@@ -1,16 +1,27 @@
|
|
|
1
|
+
import { HTMLAttributes } from 'preact/compat'
|
|
1
2
|
import { useFormContext, useFormControl } from 'domains/forms/hooks'
|
|
2
3
|
import FormControlWrapper from './wrapper'
|
|
3
4
|
|
|
5
|
+
type InputProps = HTMLAttributes<HTMLInputElement> & {
|
|
6
|
+
id: string
|
|
7
|
+
name: string
|
|
8
|
+
type: string
|
|
9
|
+
labelText: string
|
|
10
|
+
labelClass: string
|
|
11
|
+
contentHint?: string
|
|
12
|
+
'aria-describedby'?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
4
15
|
function Input({
|
|
5
16
|
id,
|
|
6
17
|
name,
|
|
7
18
|
type,
|
|
8
19
|
labelText,
|
|
9
20
|
labelClass,
|
|
10
|
-
contentHint,
|
|
21
|
+
contentHint = null,
|
|
11
22
|
'aria-describedby': ariaDescribedBy,
|
|
12
23
|
...props
|
|
13
|
-
}) {
|
|
24
|
+
}: InputProps) {
|
|
14
25
|
const { isSubmitted } = useFormContext()
|
|
15
26
|
const [field, { error }] = useFormControl(name)
|
|
16
27
|
const hasError = isSubmitted && error
|
|
@@ -12,9 +12,6 @@ const FormControlWrapper = ({
|
|
|
12
12
|
}) => {
|
|
13
13
|
return (
|
|
14
14
|
<>
|
|
15
|
-
<label htmlFor={id} className={labelClass}>
|
|
16
|
-
{labelText}
|
|
17
|
-
</label>
|
|
18
15
|
{contentHint && (
|
|
19
16
|
<span
|
|
20
17
|
id={`${id}-content-hint`}
|
|
@@ -23,8 +20,15 @@ const FormControlWrapper = ({
|
|
|
23
20
|
{contentHint}
|
|
24
21
|
</span>
|
|
25
22
|
)}
|
|
23
|
+
|
|
26
24
|
<Error id={`${id}-error`} error={!validity && errorText} />
|
|
27
|
-
|
|
25
|
+
|
|
26
|
+
<div className={className('form-control__wrapper')}>
|
|
27
|
+
<label htmlFor={id} className={labelClass}>
|
|
28
|
+
{labelText}
|
|
29
|
+
</label>
|
|
30
|
+
{children}
|
|
31
|
+
</div>
|
|
28
32
|
</>
|
|
29
33
|
)
|
|
30
34
|
}
|