@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.
Files changed (66) hide show
  1. package/build/dist/lib/components.js +698 -318
  2. package/build/dist/lib/components.js.map +1 -1
  3. package/build/dist/lib/components.min.js +1 -1
  4. package/build/dist/lib/components.min.js.LICENSE.txt +2 -2
  5. package/build/dist/lib/components.min.js.map +1 -1
  6. package/build/dist/lib/hooks.js +301 -60
  7. package/build/dist/lib/hooks.js.map +1 -1
  8. package/build/dist/lib/hooks.min.js +1 -1
  9. package/build/dist/lib/hooks.min.js.map +1 -1
  10. package/build/dist/lib/index.debug.js +80 -58
  11. package/build/dist/lib/index.debug.js.map +1 -1
  12. package/build/dist/lib/index.debug.min.js +1 -1
  13. package/build/dist/lib/index.debug.min.js.LICENSE.txt +12 -4
  14. package/build/dist/lib/index.debug.min.js.map +1 -1
  15. package/build/dist/lib/index.js +718 -325
  16. package/build/dist/lib/index.js.map +1 -1
  17. package/build/dist/lib/index.min.js +1 -1
  18. package/build/dist/lib/index.min.js.LICENSE.txt +2 -2
  19. package/build/dist/lib/index.min.js.map +1 -1
  20. package/build/dist/lib/standalone.js +803 -348
  21. package/build/dist/lib/standalone.js.map +1 -1
  22. package/build/dist/lib/standalone.min.js +1 -1
  23. package/build/dist/lib/standalone.min.js.LICENSE.txt +1 -1
  24. package/build/dist/lib/standalone.min.js.map +1 -1
  25. package/build/dist/lib/style-guide.js +830 -323
  26. package/build/dist/lib/style-guide.js.map +1 -1
  27. package/build/dist/lib/style-guide.min.js +1 -1
  28. package/build/dist/lib/style-guide.min.js.LICENSE.txt +2 -2
  29. package/build/dist/lib/style-guide.min.js.map +1 -1
  30. package/build/dist/lib/styles-default-implementation.js +1 -1
  31. package/build/dist/lib/styles.css +1 -1
  32. package/build/dist/lib/styles.js +1 -1
  33. package/build/dist/lib/utils.js +783 -360
  34. package/build/dist/lib/utils.js.map +1 -1
  35. package/build/dist/lib/utils.min.js +1 -1
  36. package/build/dist/lib/utils.min.js.LICENSE.txt +1 -1
  37. package/build/dist/lib/utils.min.js.map +1 -1
  38. package/package.json +28 -28
  39. package/src/javascripts/api/errors/seamly-api-error.ts +0 -1
  40. package/src/javascripts/api/index.ts +29 -9
  41. package/src/javascripts/domains/app/actions.ts +8 -3
  42. package/src/javascripts/domains/config/slice.ts +2 -1
  43. package/src/javascripts/domains/forms/selectors.ts +6 -8
  44. package/src/javascripts/domains/forms/slice.ts +1 -1
  45. package/src/javascripts/domains/interrupt/selectors.ts +3 -2
  46. package/src/javascripts/domains/interrupt/slice.ts +2 -0
  47. package/src/javascripts/domains/redux/create-debounced-async-thunk.ts +109 -0
  48. package/src/javascripts/domains/redux/redux.types.ts +2 -1
  49. package/src/javascripts/domains/store/actions.ts +38 -0
  50. package/src/javascripts/domains/translations/components/options-dialog/translation-option.tsx +3 -1
  51. package/src/javascripts/domains/translations/components/options-dialog/translation-options.tsx +62 -35
  52. package/src/javascripts/domains/translations/slice.ts +8 -1
  53. package/src/javascripts/domains/visibility/actions.ts +4 -1
  54. package/src/javascripts/lib/engine/index.tsx +3 -1
  55. package/src/javascripts/style-guide/states.js +65 -1
  56. package/src/javascripts/ui/components/conversation/event/{card-component.js → card-component.tsx} +6 -4
  57. package/src/javascripts/ui/components/conversation/event/event-participant.js +1 -1
  58. package/src/javascripts/ui/components/core/seamly-event-subscriber.ts +14 -30
  59. package/src/javascripts/ui/components/entry/text-entry/hooks.ts +2 -2
  60. package/src/javascripts/ui/components/form-controls/wrapper.tsx +13 -3
  61. package/src/javascripts/ui/components/view/window-view/window-open-button.js +8 -3
  62. package/src/javascripts/ui/hooks/use-session-expired-command.ts +31 -2
  63. package/src/stylesheets/5-components/_input.scss +0 -5
  64. package/src/stylesheets/5-components/_message-count.scss +11 -9
  65. package/src/stylesheets/5-components/_options.scss +2 -2
  66. 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.1.0",
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.30.2",
27
+ "core-js": "^3.31.1",
28
28
  "deep-keys": "^0.5.0",
29
- "focus-trap": "^7.4.3",
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.3",
34
- "react-redux": "^8.0.7"
33
+ "phoenix": "^1.7.6",
34
+ "react-redux": "^8.1.1"
35
35
  },
36
36
  "devDependencies": {
37
- "@babel/core": "^7.22.1",
38
- "@babel/plugin-transform-react-jsx": "^7.22.3",
39
- "@babel/plugin-transform-runtime": "^7.22.4",
40
- "@babel/preset-env": "^7.22.4",
41
- "@babel/preset-react": "^7.22.3",
42
- "@babel/preset-typescript": "^7.21.5",
43
- "@babel/runtime-corejs3": "^7.22.3",
44
- "@playwright/test": "^1.34.3",
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.59.8",
56
- "@typescript-eslint/parser": "^5.59.8",
57
- "babel-jest": "^29.5.0",
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.41.0",
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.1",
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.2.6",
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.5.0",
80
- "jest-environment-jsdom": "^29.5.0",
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.3",
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.6.2",
94
- "ts-loader": "^9.4.3",
95
- "typescript": "^5.0.4",
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.85.0",
97
+ "webpack": "^5.88.1",
98
98
  "webpack-bundle-analyzer": "^4.9.0",
99
- "webpack-cli": "^5.1.1",
100
- "webpack-dev-server": "^4.15.0",
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": {
@@ -1,4 +1,3 @@
1
- // @ts-ignore
2
1
  // eslint-disable-next-line no-undef
3
2
  interface ApiErrorOptions extends ErrorOptions {
4
3
  status?: number
@@ -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
- return this.store.get('accessToken')
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
- return this.store.get('conversationUrl')
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 this.store.get('channelTopic') || this.store.get('channelName')
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
- this.send('context', payload, false)
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 = createAsyncThunk<unknown, void, ThunkAPI>(
77
+ export const resetApp = createDebouncedAsyncThunk<unknown, void, ThunkAPI>(
77
78
  'resetApp',
78
79
  async (_, { dispatch, extra: { api } }) => {
79
80
  await api.disconnect()
80
- await api.clearStore()
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 (rejectedValueOrSerializedError) {
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
- export const getState = ({ forms }) => forms
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
- export const getFormControlsByFormId = createSelector(
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
  )
@@ -2,7 +2,7 @@ import { createSlice } from '@reduxjs/toolkit'
2
2
  import { resetApp } from 'domains/app/actions'
3
3
  import { ControlState } from 'domains/forms/forms.types'
4
4
 
5
- const initialFormState = {
5
+ const initialFormState: Record<string, ControlState> = {
6
6
  controls: {},
7
7
  }
8
8
 
@@ -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
@@ -1,10 +1,11 @@
1
+ import { API } from 'api'
1
2
  import { Config } from 'config.types'
2
3
  import type { RootState } from 'domains/store'
3
4
 
4
5
  export type ThunkAPI = {
5
6
  state: RootState
6
7
  extra: {
7
- api: any
8
+ api: API
8
9
  config: Config
9
10
  eventBus: any
10
11
  }
@@ -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
+ )
@@ -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}
@@ -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
- ({ locale }: Language) =>
33
- () => {
34
- if (locale === currentLocale || defaultLocale === locale) {
35
- disableTranslations()
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
- const sortedLanguages = useMemo(() => {
45
- return [...languages].sort((a, b) => {
46
- if (a.locale === defaultLocale) return -1
47
- if (b.locale === defaultLocale) return 1
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
- return a.nativeName.localeCompare(b.nativeName, undefined, {
50
- sensitivity: 'base',
51
- })
52
- })
53
- }, [languages, defaultLocale])
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
- {sortedLanguages.map((language, idx) => {
63
- const isOriginal = idx === 0
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={language.locale}
71
- id={language.locale}
72
- label={language.nativeName}
83
+ key={locale}
84
+ id={locale}
85
+ label={nativeName}
73
86
  checked={checked}
74
87
  description={isOriginal && t('translations.settings.original')}
75
- onChange={handleChange(language)}
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
  }