@seamly/web-ui 22.1.0 → 22.3.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/dist/lib/components.js +698 -318
- package/build/dist/lib/components.js.map +1 -1
- package/build/dist/lib/components.min.js +1 -1
- package/build/dist/lib/components.min.js.LICENSE.txt +2 -2
- package/build/dist/lib/components.min.js.map +1 -1
- package/build/dist/lib/hooks.js +301 -60
- package/build/dist/lib/hooks.js.map +1 -1
- package/build/dist/lib/hooks.min.js +1 -1
- package/build/dist/lib/hooks.min.js.map +1 -1
- package/build/dist/lib/index.debug.js +80 -58
- package/build/dist/lib/index.debug.js.map +1 -1
- package/build/dist/lib/index.debug.min.js +1 -1
- package/build/dist/lib/index.debug.min.js.LICENSE.txt +12 -4
- package/build/dist/lib/index.debug.min.js.map +1 -1
- package/build/dist/lib/index.js +718 -325
- package/build/dist/lib/index.js.map +1 -1
- package/build/dist/lib/index.min.js +1 -1
- package/build/dist/lib/index.min.js.LICENSE.txt +2 -2
- package/build/dist/lib/index.min.js.map +1 -1
- package/build/dist/lib/standalone.js +803 -348
- package/build/dist/lib/standalone.js.map +1 -1
- package/build/dist/lib/standalone.min.js +1 -1
- package/build/dist/lib/standalone.min.js.LICENSE.txt +1 -1
- package/build/dist/lib/standalone.min.js.map +1 -1
- package/build/dist/lib/style-guide.js +830 -323
- package/build/dist/lib/style-guide.js.map +1 -1
- package/build/dist/lib/style-guide.min.js +1 -1
- package/build/dist/lib/style-guide.min.js.LICENSE.txt +2 -2
- package/build/dist/lib/style-guide.min.js.map +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 +783 -360
- package/build/dist/lib/utils.js.map +1 -1
- package/build/dist/lib/utils.min.js +1 -1
- package/build/dist/lib/utils.min.js.LICENSE.txt +1 -1
- package/build/dist/lib/utils.min.js.map +1 -1
- package/package.json +28 -28
- package/src/javascripts/api/errors/seamly-api-error.ts +0 -1
- package/src/javascripts/api/index.ts +29 -9
- package/src/javascripts/domains/app/actions.ts +8 -3
- package/src/javascripts/domains/config/slice.ts +2 -1
- package/src/javascripts/domains/forms/selectors.ts +6 -8
- package/src/javascripts/domains/forms/slice.ts +1 -1
- package/src/javascripts/domains/interrupt/selectors.ts +3 -2
- package/src/javascripts/domains/interrupt/slice.ts +2 -0
- package/src/javascripts/domains/redux/create-debounced-async-thunk.ts +109 -0
- package/src/javascripts/domains/redux/redux.types.ts +2 -1
- package/src/javascripts/domains/store/actions.ts +38 -0
- package/src/javascripts/domains/translations/components/options-dialog/translation-option.tsx +3 -1
- package/src/javascripts/domains/translations/components/options-dialog/translation-options.tsx +62 -35
- package/src/javascripts/domains/translations/slice.ts +8 -1
- package/src/javascripts/domains/visibility/actions.ts +4 -1
- package/src/javascripts/lib/engine/index.tsx +3 -1
- package/src/javascripts/style-guide/states.js +65 -1
- package/src/javascripts/ui/components/conversation/event/{card-component.js → card-component.tsx} +6 -4
- package/src/javascripts/ui/components/conversation/event/event-participant.js +1 -1
- package/src/javascripts/ui/components/core/seamly-event-subscriber.ts +14 -30
- package/src/javascripts/ui/components/entry/text-entry/hooks.ts +2 -2
- package/src/javascripts/ui/components/form-controls/wrapper.tsx +13 -3
- package/src/javascripts/ui/components/view/window-view/window-open-button.js +8 -3
- package/src/javascripts/ui/hooks/use-session-expired-command.ts +31 -2
- package/src/stylesheets/5-components/_input.scss +0 -5
- package/src/stylesheets/5-components/_message-count.scss +11 -9
- package/src/stylesheets/5-components/_options.scss +2 -2
- package/src/stylesheets/5-components/_translation-options.scss +23 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seamly/web-ui",
|
|
3
|
-
"version": "22.
|
|
3
|
+
"version": "22.3.0-beta.1",
|
|
4
4
|
"main": "build/dist/lib/index.js",
|
|
5
5
|
"types": "build/src/javascripts/index.d.ts",
|
|
6
6
|
"module": "",
|
|
@@ -24,24 +24,24 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@reduxjs/toolkit": "^1.9.5",
|
|
26
26
|
"@ultraq/icu-message-formatter": "^0.12.0",
|
|
27
|
-
"core-js": "^3.
|
|
27
|
+
"core-js": "^3.31.1",
|
|
28
28
|
"deep-keys": "^0.5.0",
|
|
29
|
-
"focus-trap": "^7.
|
|
29
|
+
"focus-trap": "^7.5.2",
|
|
30
30
|
"include-media": "^2.0.0",
|
|
31
31
|
"js-cookie": "^3.0.5",
|
|
32
32
|
"minivents": "^2.2.1",
|
|
33
|
-
"phoenix": "^1.7.
|
|
34
|
-
"react-redux": "^8.
|
|
33
|
+
"phoenix": "^1.7.6",
|
|
34
|
+
"react-redux": "^8.1.1"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@babel/core": "^7.22.
|
|
38
|
-
"@babel/plugin-transform-react-jsx": "^7.22.
|
|
39
|
-
"@babel/plugin-transform-runtime": "^7.22.
|
|
40
|
-
"@babel/preset-env": "^7.22.
|
|
41
|
-
"@babel/preset-react": "^7.22.
|
|
42
|
-
"@babel/preset-typescript": "^7.
|
|
43
|
-
"@babel/runtime-corejs3": "^7.22.
|
|
44
|
-
"@playwright/test": "^1.
|
|
37
|
+
"@babel/core": "^7.22.6",
|
|
38
|
+
"@babel/plugin-transform-react-jsx": "^7.22.5",
|
|
39
|
+
"@babel/plugin-transform-runtime": "^7.22.6",
|
|
40
|
+
"@babel/preset-env": "^7.22.6",
|
|
41
|
+
"@babel/preset-react": "^7.22.5",
|
|
42
|
+
"@babel/preset-typescript": "^7.22.5",
|
|
43
|
+
"@babel/runtime-corejs3": "^7.22.6",
|
|
44
|
+
"@playwright/test": "^1.35.1",
|
|
45
45
|
"@seamly/doc-site": "^2.0.0",
|
|
46
46
|
"@seamly/eslint-config": "^2.3.0",
|
|
47
47
|
"@seamly/prettier-config": "^2.2.0",
|
|
@@ -52,52 +52,52 @@
|
|
|
52
52
|
"@types/core-js": "^2.5.5",
|
|
53
53
|
"@types/jest": "^29.5.2",
|
|
54
54
|
"@types/phoenix": "^1.6.0",
|
|
55
|
-
"@typescript-eslint/eslint-plugin": "^5.
|
|
56
|
-
"@typescript-eslint/parser": "^5.
|
|
57
|
-
"babel-jest": "^29.
|
|
55
|
+
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
|
56
|
+
"@typescript-eslint/parser": "^5.61.0",
|
|
57
|
+
"babel-jest": "^29.6.0",
|
|
58
58
|
"babel-loader": "^9.1.2",
|
|
59
59
|
"babel-plugin-istanbul": "^6.1.1",
|
|
60
60
|
"babel-plugin-react-remove-properties": "^0.3.0",
|
|
61
61
|
"copy-webpack-plugin": "^11.0.0",
|
|
62
62
|
"debug": "^4.3.4",
|
|
63
|
-
"eslint": "^8.
|
|
63
|
+
"eslint": "^8.44.0",
|
|
64
64
|
"eslint-config-prettier": "^8.8.0",
|
|
65
65
|
"eslint-import-resolver-alias": "^1.1.2",
|
|
66
66
|
"eslint-import-resolver-typescript": "^3.5.5",
|
|
67
67
|
"eslint-plugin-filenames": "^1.3.2",
|
|
68
68
|
"eslint-plugin-import": "^2.27.5",
|
|
69
|
-
"eslint-plugin-jest": "^27.2.
|
|
69
|
+
"eslint-plugin-jest": "^27.2.2",
|
|
70
70
|
"eslint-plugin-prettier": "^4.2.1",
|
|
71
71
|
"eslint-plugin-react": "^7.32.2",
|
|
72
72
|
"eslint-plugin-react-hooks": "^4.6.0",
|
|
73
73
|
"file-loader": "^6.2.0",
|
|
74
74
|
"fork-ts-checker-webpack-plugin": "^8.0.0",
|
|
75
|
-
"glob": "^10.
|
|
75
|
+
"glob": "^10.3.1",
|
|
76
76
|
"husky": "^8.0.3",
|
|
77
77
|
"ignore-loader": "^0.1.2",
|
|
78
78
|
"isomorphic-unfetch": "^4.0.2",
|
|
79
|
-
"jest": "^29.
|
|
80
|
-
"jest-environment-jsdom": "^29.
|
|
79
|
+
"jest": "^29.6.0",
|
|
80
|
+
"jest-environment-jsdom": "^29.6.0",
|
|
81
81
|
"jest-watch-typeahead": "^2.2.2",
|
|
82
82
|
"mini-css-extract-plugin": "^2.7.6",
|
|
83
83
|
"nyc": "^15.1.0",
|
|
84
84
|
"playwright-test-coverage": "^1.2.12",
|
|
85
85
|
"postcss": "^8.4.24",
|
|
86
86
|
"preact": "^10.15.1",
|
|
87
|
-
"preact-render-to-string": "^6.0
|
|
87
|
+
"preact-render-to-string": "^6.1.0",
|
|
88
88
|
"prettier": "^2.8.8",
|
|
89
89
|
"raw-loader": "^4.0.2",
|
|
90
90
|
"rimraf": "^5.0.1",
|
|
91
91
|
"start-server-and-test": "^2.0.0",
|
|
92
92
|
"style-loader": "^3.3.3",
|
|
93
|
-
"stylelint": "^15.
|
|
94
|
-
"ts-loader": "^9.4.
|
|
95
|
-
"typescript": "^5.
|
|
93
|
+
"stylelint": "^15.10.1",
|
|
94
|
+
"ts-loader": "^9.4.4",
|
|
95
|
+
"typescript": "^5.1.6",
|
|
96
96
|
"url-loader": "^4.1.1",
|
|
97
|
-
"webpack": "^5.
|
|
97
|
+
"webpack": "^5.88.1",
|
|
98
98
|
"webpack-bundle-analyzer": "^4.9.0",
|
|
99
|
-
"webpack-cli": "^5.1.
|
|
100
|
-
"webpack-dev-server": "^4.15.
|
|
99
|
+
"webpack-cli": "^5.1.4",
|
|
100
|
+
"webpack-dev-server": "^4.15.1",
|
|
101
101
|
"webpack-merge": "^5.9.0"
|
|
102
102
|
},
|
|
103
103
|
"resolutions": {
|
|
@@ -128,7 +128,7 @@ export class API {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
store: {
|
|
131
|
-
get(_key: string): string
|
|
131
|
+
get(_key: string): string | object
|
|
132
132
|
set(_key: string, _value: unknown): string
|
|
133
133
|
delete(_key: string): string
|
|
134
134
|
}
|
|
@@ -194,7 +194,8 @@ export class API {
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
#getAccessToken(): string {
|
|
197
|
-
|
|
197
|
+
const accessToken = this.store.get('accessToken') as string
|
|
198
|
+
return accessToken
|
|
198
199
|
}
|
|
199
200
|
|
|
200
201
|
#setAccessToken(accessToken: string) {
|
|
@@ -202,7 +203,8 @@ export class API {
|
|
|
202
203
|
}
|
|
203
204
|
|
|
204
205
|
getConversationUrl(): string {
|
|
205
|
-
|
|
206
|
+
const conversationUrl = this.store.get('conversationUrl') as string
|
|
207
|
+
return conversationUrl
|
|
206
208
|
}
|
|
207
209
|
|
|
208
210
|
#setConversationUrl(url: { href?: string }) {
|
|
@@ -214,9 +216,11 @@ export class API {
|
|
|
214
216
|
}
|
|
215
217
|
|
|
216
218
|
#getChannelTopic() {
|
|
219
|
+
const channelTopic = (this.store.get('channelTopic') ||
|
|
220
|
+
this.store.get('channelName')) as string
|
|
217
221
|
// The `channelName` fallback is needed for seamless client upgrades.
|
|
218
222
|
// TODO: Remove when all clients have been upgraded past v20.
|
|
219
|
-
return
|
|
223
|
+
return channelTopic
|
|
220
224
|
}
|
|
221
225
|
|
|
222
226
|
#setChannelTopic(topic: string) {
|
|
@@ -417,7 +421,7 @@ export class API {
|
|
|
417
421
|
throw new SeamlyGeneralError(error)
|
|
418
422
|
}
|
|
419
423
|
|
|
420
|
-
throw error
|
|
424
|
+
throw new ApiError(error)
|
|
421
425
|
}
|
|
422
426
|
}
|
|
423
427
|
|
|
@@ -503,7 +507,11 @@ export class API {
|
|
|
503
507
|
return xhr
|
|
504
508
|
}
|
|
505
509
|
|
|
506
|
-
async getConversationIntitialState()
|
|
510
|
+
async getConversationIntitialState(): Promise<
|
|
511
|
+
InitialConversation & {
|
|
512
|
+
userResponded: boolean
|
|
513
|
+
}
|
|
514
|
+
> {
|
|
507
515
|
try {
|
|
508
516
|
const response = await fetchApi(
|
|
509
517
|
`${this.#getUrlPrefix('http')}${this.getConversationUrl()}`,
|
|
@@ -519,7 +527,7 @@ export class API {
|
|
|
519
527
|
|
|
520
528
|
this.#updateUrls(body)
|
|
521
529
|
this.userResponded = body.conversation.userResponded
|
|
522
|
-
return omit(body.conversation, ['accessToken', 'channelTopic'])
|
|
530
|
+
return omit(body.conversation, ['accessToken', 'channelTopic']) as any
|
|
523
531
|
} catch (error) {
|
|
524
532
|
if (error.status === 401) {
|
|
525
533
|
throw new SeamlyUnauthorizedError(error)
|
|
@@ -554,7 +562,7 @@ export class API {
|
|
|
554
562
|
throw new SeamlyGeneralError(error)
|
|
555
563
|
}
|
|
556
564
|
|
|
557
|
-
throw error
|
|
565
|
+
throw new ApiError(error)
|
|
558
566
|
}
|
|
559
567
|
}
|
|
560
568
|
|
|
@@ -608,7 +616,19 @@ export class API {
|
|
|
608
616
|
return
|
|
609
617
|
}
|
|
610
618
|
|
|
611
|
-
|
|
619
|
+
// Destructure the server locale from the payload
|
|
620
|
+
const { locale: _, ...restPayload } = payload
|
|
621
|
+
const configLocale = this.#config.context?.locale
|
|
622
|
+
|
|
623
|
+
this.send(
|
|
624
|
+
'context',
|
|
625
|
+
{
|
|
626
|
+
// Only send locale if explicitly set in the config.
|
|
627
|
+
...(configLocale ? { locale: configLocale } : {}),
|
|
628
|
+
...restPayload,
|
|
629
|
+
},
|
|
630
|
+
false,
|
|
631
|
+
)
|
|
612
632
|
}
|
|
613
633
|
|
|
614
634
|
#getEnvironment() {
|
|
@@ -5,6 +5,7 @@ import SeamlyUnavailableError from 'api/errors/seamly-unavailable-error'
|
|
|
5
5
|
import { actionTypes } from 'ui/utils/seamly-utils'
|
|
6
6
|
import { initializeConfig, resetConfig } from 'domains/config/actions'
|
|
7
7
|
import { setLocale } from 'domains/i18n/actions'
|
|
8
|
+
import createDebouncedAsyncThunk from 'domains/redux/create-debounced-async-thunk'
|
|
8
9
|
import { ThunkAPI } from 'domains/redux/redux.types'
|
|
9
10
|
import type { RootState } from 'domains/store'
|
|
10
11
|
import { initializeVisibility } from 'domains/visibility/actions'
|
|
@@ -73,11 +74,11 @@ export const initializeApp = createAsyncThunk<
|
|
|
73
74
|
}
|
|
74
75
|
})
|
|
75
76
|
|
|
76
|
-
export const resetApp =
|
|
77
|
+
export const resetApp = createDebouncedAsyncThunk<unknown, void, ThunkAPI>(
|
|
77
78
|
'resetApp',
|
|
78
79
|
async (_, { dispatch, extra: { api } }) => {
|
|
79
80
|
await api.disconnect()
|
|
80
|
-
|
|
81
|
+
api.clearStore()
|
|
81
82
|
|
|
82
83
|
dispatch(resetConfig())
|
|
83
84
|
await dispatch(initializeConfig())
|
|
@@ -85,10 +86,14 @@ export const resetApp = createAsyncThunk<unknown, void, ThunkAPI>(
|
|
|
85
86
|
try {
|
|
86
87
|
const { locale } = await dispatch(initializeApp()).unwrap()
|
|
87
88
|
await dispatch(setLocale(locale))
|
|
88
|
-
} catch (
|
|
89
|
+
} catch (e) {
|
|
89
90
|
// nothing to do
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
dispatch(initializeVisibility())
|
|
93
94
|
},
|
|
95
|
+
{
|
|
96
|
+
wait: 2000,
|
|
97
|
+
leading: true,
|
|
98
|
+
},
|
|
94
99
|
)
|
|
@@ -103,6 +103,7 @@ export const configSlice = createSlice({
|
|
|
103
103
|
agentParticipant,
|
|
104
104
|
userParticipant,
|
|
105
105
|
startChatIcon,
|
|
106
|
+
locale,
|
|
106
107
|
},
|
|
107
108
|
},
|
|
108
109
|
) => {
|
|
@@ -110,7 +111,7 @@ export const configSlice = createSlice({
|
|
|
110
111
|
type: 'message',
|
|
111
112
|
payload,
|
|
112
113
|
}))
|
|
113
|
-
|
|
114
|
+
state.context.locale = locale
|
|
114
115
|
state.agentParticipant = agentParticipant
|
|
115
116
|
state.userParticipant = userParticipant
|
|
116
117
|
state.startChatIcon = startChatIcon
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { createSelector } from '@reduxjs/toolkit'
|
|
2
|
+
import type { RootState } from 'domains/store'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
const getState = ({ forms }: RootState) => forms
|
|
4
5
|
|
|
5
6
|
export const getFormById = createSelector(
|
|
6
|
-
getState,
|
|
7
|
-
(_, { formId }) => formId,
|
|
7
|
+
[getState, (_, { formId }): string => formId],
|
|
8
8
|
(forms, formId) => forms[formId],
|
|
9
9
|
)
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
const getFormControlsByFormId = createSelector(
|
|
12
12
|
getFormById,
|
|
13
13
|
(form) => form?.controls || {},
|
|
14
14
|
)
|
|
@@ -26,13 +26,11 @@ export const getFormValuesByFormId = createSelector(
|
|
|
26
26
|
)
|
|
27
27
|
|
|
28
28
|
export const getControlValueByName = createSelector(
|
|
29
|
-
getFormControlsByFormId,
|
|
30
|
-
(_, { name }) => name,
|
|
29
|
+
[getFormControlsByFormId, (_, { name }): string => name],
|
|
31
30
|
(controls, name) => controls[name]?.value,
|
|
32
31
|
)
|
|
33
32
|
|
|
34
33
|
export const getControlTouchedByName = createSelector(
|
|
35
|
-
getFormControlsByFormId,
|
|
36
|
-
(_, { name }) => name,
|
|
34
|
+
[getFormControlsByFormId, (_, { name }): string => name],
|
|
37
35
|
(controls, name) => controls[name]?.touched,
|
|
38
36
|
)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createSelector } from '@reduxjs/toolkit'
|
|
2
|
+
import { RootState } from 'domains/store'
|
|
2
3
|
|
|
3
4
|
export const selectError = createSelector(
|
|
4
|
-
({ interrupt }) => interrupt,
|
|
5
|
-
({ error }) => error,
|
|
5
|
+
({ interrupt }: RootState) => interrupt,
|
|
6
|
+
({ error }: RootState['interrupt']) => error,
|
|
6
7
|
)
|
|
7
8
|
|
|
8
9
|
export const selectHasError = createSelector(selectError, (error) =>
|
|
@@ -2,6 +2,7 @@ import { createSlice, isAnyOf } from '@reduxjs/toolkit'
|
|
|
2
2
|
import { initializeApp } from 'domains/app/actions'
|
|
3
3
|
import { initializeConfig } from 'domains/config/actions'
|
|
4
4
|
import { setLocale } from 'domains/i18n/actions'
|
|
5
|
+
import { getConversation } from 'domains/store/actions'
|
|
5
6
|
import { initializeVisibility, setVisibility } from 'domains/visibility/actions'
|
|
6
7
|
|
|
7
8
|
const initialState = {
|
|
@@ -27,6 +28,7 @@ export const interruptSlice = createSlice({
|
|
|
27
28
|
setLocale.rejected,
|
|
28
29
|
setVisibility.rejected,
|
|
29
30
|
initializeVisibility.rejected,
|
|
31
|
+
getConversation.rejected,
|
|
30
32
|
),
|
|
31
33
|
(state, { payload }) => {
|
|
32
34
|
state.error = payload
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AsyncThunk,
|
|
3
|
+
AsyncThunkPayloadCreator,
|
|
4
|
+
Dispatch,
|
|
5
|
+
createAsyncThunk,
|
|
6
|
+
} from '@reduxjs/toolkit'
|
|
7
|
+
|
|
8
|
+
type DebounceOptionsType = {
|
|
9
|
+
/**
|
|
10
|
+
* The number of milliseconds to delay
|
|
11
|
+
* @defaultValue `300`
|
|
12
|
+
*/
|
|
13
|
+
wait: number
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The maximum time `payloadCreator` is allowed to be delayed before
|
|
17
|
+
* it's invoked.
|
|
18
|
+
* @defaultValue `0`
|
|
19
|
+
*/
|
|
20
|
+
maxWait?: number
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Specify invoking on the leading edge of the timeout.
|
|
24
|
+
* @defaultValue `false`
|
|
25
|
+
*/
|
|
26
|
+
leading?: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type AsyncThunkConfig = {
|
|
30
|
+
state?: unknown
|
|
31
|
+
dispatch?: Dispatch
|
|
32
|
+
extra?: unknown
|
|
33
|
+
rejectValue?: unknown
|
|
34
|
+
serializedErrorType?: unknown
|
|
35
|
+
pendingMeta?: unknown
|
|
36
|
+
fulfilledMeta?: unknown
|
|
37
|
+
rejectedMeta?: unknown
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A debounced analogue of the `createAsyncThunk` from `@reduxjs/toolkit`
|
|
42
|
+
* @param typePrefix - a string action type value
|
|
43
|
+
* @param payloadCreator - a callback function that should return a promise containing the result of some asynchronous logic
|
|
44
|
+
* @param debounceOptions - the debounce options object
|
|
45
|
+
*/
|
|
46
|
+
const createDebouncedAsyncThunk = <
|
|
47
|
+
Returned,
|
|
48
|
+
ThunkArg,
|
|
49
|
+
ThunkApiConfig extends AsyncThunkConfig = {},
|
|
50
|
+
>(
|
|
51
|
+
typePrefix: string,
|
|
52
|
+
payloadCreator: AsyncThunkPayloadCreator<Returned, ThunkArg, ThunkApiConfig>,
|
|
53
|
+
debounceOptions?: DebounceOptionsType,
|
|
54
|
+
): AsyncThunk<Returned, ThunkArg, ThunkApiConfig> => {
|
|
55
|
+
const { wait = 300, maxWait = 0, leading = false } = debounceOptions ?? {}
|
|
56
|
+
|
|
57
|
+
let debounceTimer: ReturnType<typeof setTimeout> = null
|
|
58
|
+
let maxWaitTimer: ReturnType<typeof setTimeout> = null
|
|
59
|
+
let resolve: ((_value: boolean) => void) | undefined
|
|
60
|
+
|
|
61
|
+
const cancel = (): void => {
|
|
62
|
+
if (resolve) {
|
|
63
|
+
resolve(false)
|
|
64
|
+
resolve = undefined
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const invoke = (): void => {
|
|
69
|
+
clearTimeout(maxWaitTimer)
|
|
70
|
+
maxWaitTimer = undefined
|
|
71
|
+
|
|
72
|
+
if (resolve) {
|
|
73
|
+
resolve(true)
|
|
74
|
+
resolve = undefined
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const debounceExecutionCondition = (): Promise<boolean> | boolean => {
|
|
79
|
+
const immediate = leading && !debounceTimer
|
|
80
|
+
|
|
81
|
+
// Start debounced condition resolution
|
|
82
|
+
clearTimeout(debounceTimer)
|
|
83
|
+
debounceTimer = setTimeout(() => {
|
|
84
|
+
invoke()
|
|
85
|
+
debounceTimer = null
|
|
86
|
+
}, wait)
|
|
87
|
+
|
|
88
|
+
if (immediate) {
|
|
89
|
+
return true
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
cancel()
|
|
93
|
+
|
|
94
|
+
// Start max wait condition resolution
|
|
95
|
+
if (maxWait && !maxWaitTimer) {
|
|
96
|
+
maxWaitTimer = setTimeout(invoke, maxWait)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return new Promise((res) => {
|
|
100
|
+
resolve = res
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return createAsyncThunk<Returned, ThunkArg>(typePrefix, payloadCreator, {
|
|
105
|
+
condition: debounceExecutionCondition,
|
|
106
|
+
}) as AsyncThunk<Returned, ThunkArg, ThunkApiConfig>
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default createDebouncedAsyncThunk
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createAsyncThunk } from '@reduxjs/toolkit'
|
|
2
|
+
import { ConversationHistoryResponse } from 'api'
|
|
3
|
+
import { ThunkAPI } from 'domains/redux/redux.types'
|
|
4
|
+
|
|
5
|
+
export const getConversation = createAsyncThunk<
|
|
6
|
+
ConversationHistoryResponse | undefined,
|
|
7
|
+
{
|
|
8
|
+
lastEvent: { id: string; occurredAt: number }
|
|
9
|
+
},
|
|
10
|
+
ThunkAPI
|
|
11
|
+
>(
|
|
12
|
+
'getConversation',
|
|
13
|
+
async (_, { extra: { api }, rejectWithValue }) => {
|
|
14
|
+
try {
|
|
15
|
+
return api.getConversation()
|
|
16
|
+
} catch (error) {
|
|
17
|
+
return rejectWithValue({
|
|
18
|
+
name: error?.name,
|
|
19
|
+
message: error?.message,
|
|
20
|
+
langKey: error?.langKey,
|
|
21
|
+
action: error?.action,
|
|
22
|
+
originalEvent: error?.originalEvent,
|
|
23
|
+
originalError: error?.originalError,
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
condition(payload, { getState }) {
|
|
29
|
+
const {
|
|
30
|
+
state: { events },
|
|
31
|
+
} = getState()
|
|
32
|
+
|
|
33
|
+
const lastEvent = events[events.length - 1]
|
|
34
|
+
const payloadLastEventId = payload?.lastEvent?.id
|
|
35
|
+
return lastEvent && payloadLastEventId !== lastEvent.payload.id
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
)
|
package/src/javascripts/domains/translations/components/options-dialog/translation-option.tsx
CHANGED
|
@@ -8,6 +8,7 @@ type TranslationOptionProps = {
|
|
|
8
8
|
description?: string
|
|
9
9
|
onChange: () => void
|
|
10
10
|
id: string
|
|
11
|
+
itemClassName?: string
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
const TranslationOption: FC<TranslationOptionProps> = ({
|
|
@@ -16,6 +17,7 @@ const TranslationOption: FC<TranslationOptionProps> = ({
|
|
|
16
17
|
description,
|
|
17
18
|
onChange,
|
|
18
19
|
id,
|
|
20
|
+
itemClassName,
|
|
19
21
|
}) => {
|
|
20
22
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
21
23
|
if (e.code === 'Space' || e.code === 'Enter') {
|
|
@@ -26,7 +28,7 @@ const TranslationOption: FC<TranslationOptionProps> = ({
|
|
|
26
28
|
|
|
27
29
|
return (
|
|
28
30
|
<li
|
|
29
|
-
className={className('translation-options__item')}
|
|
31
|
+
className={className([itemClassName, 'translation-options__item'])}
|
|
30
32
|
aria-selected={checked}
|
|
31
33
|
role="option"
|
|
32
34
|
tabIndex={0}
|
package/src/javascripts/domains/translations/components/options-dialog/translation-options.tsx
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { FC } from 'preact/compat'
|
|
2
|
-
import { useMemo } from 'preact/hooks'
|
|
1
|
+
import { FC, useMemo } from 'preact/compat'
|
|
3
2
|
import { useConfig } from 'domains/config/hooks'
|
|
4
3
|
import { useI18n } from 'domains/i18n/hooks'
|
|
5
4
|
import TranslationOption from 'domains/translations/components/options-dialog/translation-option'
|
|
@@ -15,6 +14,9 @@ type TranslationOptionsProps = {
|
|
|
15
14
|
describedById?: string
|
|
16
15
|
}
|
|
17
16
|
|
|
17
|
+
const isChecked = (language, currentLocale, isOriginal): boolean =>
|
|
18
|
+
currentLocale === language.locale || (!currentLocale && isOriginal)
|
|
19
|
+
|
|
18
20
|
const TranslationOptions: FC<TranslationOptionsProps> = ({
|
|
19
21
|
onChange,
|
|
20
22
|
describedById,
|
|
@@ -28,29 +30,45 @@ const TranslationOptions: FC<TranslationOptionsProps> = ({
|
|
|
28
30
|
const { languages, currentLocale, enableTranslations, disableTranslations } =
|
|
29
31
|
useTranslations()
|
|
30
32
|
|
|
31
|
-
const handleChange =
|
|
32
|
-
(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
} else {
|
|
37
|
-
enableTranslations(locale)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
onChange()
|
|
41
|
-
focusContainer()
|
|
33
|
+
const handleChange = (locale: Language['locale']) => () => {
|
|
34
|
+
if (locale === currentLocale || defaultLocale === locale) {
|
|
35
|
+
disableTranslations()
|
|
36
|
+
} else {
|
|
37
|
+
enableTranslations(locale)
|
|
42
38
|
}
|
|
43
39
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
40
|
+
onChange()
|
|
41
|
+
focusContainer()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const { primaryLanguages, remainingLanguages } = useMemo(
|
|
45
|
+
() =>
|
|
46
|
+
languages.reduce(
|
|
47
|
+
(acc, language) => {
|
|
48
|
+
const isOriginal = language.locale === defaultLocale
|
|
49
|
+
const checked = isChecked(language, currentLocale, isOriginal)
|
|
50
|
+
|
|
51
|
+
if (language.locale !== defaultLocale) {
|
|
52
|
+
acc.remainingLanguages.push({ ...language, checked, isOriginal })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const selectedIdx = acc.remainingLanguages.findIndex(
|
|
56
|
+
(l) => l.locale === currentLocale,
|
|
57
|
+
)
|
|
48
58
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
if (isOriginal || (checked && selectedIdx > 4)) {
|
|
60
|
+
acc.primaryLanguages.push({ ...language, checked, isOriginal })
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return acc
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
primaryLanguages: [],
|
|
67
|
+
remainingLanguages: [],
|
|
68
|
+
},
|
|
69
|
+
),
|
|
70
|
+
[currentLocale, defaultLocale, languages],
|
|
71
|
+
)
|
|
54
72
|
|
|
55
73
|
return (
|
|
56
74
|
<ul
|
|
@@ -59,23 +77,32 @@ const TranslationOptions: FC<TranslationOptionsProps> = ({
|
|
|
59
77
|
tabIndex={-1}
|
|
60
78
|
className={className('translation-options')}
|
|
61
79
|
>
|
|
62
|
-
{
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const checked =
|
|
66
|
-
currentLocale === language.locale || (!currentLocale && isOriginal)
|
|
67
|
-
|
|
68
|
-
return (
|
|
80
|
+
{primaryLanguages.map(
|
|
81
|
+
({ locale, nativeName, checked, isOriginal }, idx) => (
|
|
69
82
|
<TranslationOption
|
|
70
|
-
key={
|
|
71
|
-
id={
|
|
72
|
-
label={
|
|
83
|
+
key={locale}
|
|
84
|
+
id={locale}
|
|
85
|
+
label={nativeName}
|
|
73
86
|
checked={checked}
|
|
74
87
|
description={isOriginal && t('translations.settings.original')}
|
|
75
|
-
onChange={handleChange(
|
|
88
|
+
onChange={handleChange(locale)}
|
|
89
|
+
itemClassName={className({
|
|
90
|
+
'translation-options__item--original': isOriginal,
|
|
91
|
+
'translation-options__item--selected': checked && idx !== 0,
|
|
92
|
+
})}
|
|
76
93
|
/>
|
|
77
|
-
)
|
|
78
|
-
|
|
94
|
+
),
|
|
95
|
+
)}
|
|
96
|
+
{remainingLanguages.map(({ locale, nativeName, checked, isOriginal }) => (
|
|
97
|
+
<TranslationOption
|
|
98
|
+
key={locale}
|
|
99
|
+
id={locale}
|
|
100
|
+
label={nativeName}
|
|
101
|
+
checked={checked}
|
|
102
|
+
description={isOriginal && t('translations.settings.original')}
|
|
103
|
+
onChange={handleChange(locale)}
|
|
104
|
+
/>
|
|
105
|
+
))}
|
|
79
106
|
</ul>
|
|
80
107
|
)
|
|
81
108
|
}
|