@seamly/web-ui 20.7.0 → 20.8.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 (226) hide show
  1. package/build/dist/lib/hooks.js +1 -1
  2. package/build/dist/lib/hooks.min.js +1 -1
  3. package/build/dist/lib/index.debug.js +945 -790
  4. package/build/dist/lib/index.debug.min.js +1 -1
  5. package/build/dist/lib/index.debug.min.js.LICENSE.txt +187 -131
  6. package/build/dist/lib/index.js +24800 -19606
  7. package/build/dist/lib/index.min.js +1 -1
  8. package/build/dist/lib/index.min.js.LICENSE.txt +38 -4
  9. package/build/dist/lib/standalone.js +32920 -26742
  10. package/build/dist/lib/standalone.min.js +1 -1
  11. package/build/dist/lib/standalone.min.js.LICENSE.txt +39 -0
  12. package/build/dist/lib/storage.js +2 -2
  13. package/build/dist/lib/storage.min.js +1 -1
  14. package/build/dist/lib/style-guide.js +8780 -7907
  15. package/build/dist/lib/style-guide.min.js +2 -1
  16. package/build/dist/lib/style-guide.min.js.LICENSE.txt +38 -0
  17. package/build/dist/lib/styles.css +1 -1
  18. package/build/dist/lib/utils.js +1 -2
  19. package/build/dist/lib/utils.min.js +1 -1
  20. package/package.json +19 -9
  21. package/src/icons/avatar_agent-32.svg +7 -0
  22. package/src/icons/avatar_bot-32.svg +6 -1
  23. package/src/javascripts/api/index.js +1 -1
  24. package/src/javascripts/{config.js → config.ts} +3 -1
  25. package/src/javascripts/config.types.ts +96 -0
  26. package/src/javascripts/domains/app/actions.ts +83 -0
  27. package/src/javascripts/domains/app/app.types.ts +3 -0
  28. package/src/javascripts/domains/app/hooks.js +3 -5
  29. package/src/javascripts/domains/app/selectors.ts +6 -0
  30. package/src/javascripts/domains/app/slice.ts +30 -0
  31. package/src/javascripts/domains/config/actions.ts +45 -0
  32. package/src/javascripts/domains/config/hooks.ts +19 -0
  33. package/src/javascripts/domains/config/selectors.ts +24 -0
  34. package/src/javascripts/domains/config/slice.ts +113 -0
  35. package/src/javascripts/domains/errors/index.js +13 -9
  36. package/src/javascripts/domains/forms/context.ts +14 -0
  37. package/src/javascripts/domains/forms/forms.types.ts +24 -0
  38. package/src/javascripts/domains/forms/{hooks.js → hooks.ts} +23 -26
  39. package/src/javascripts/domains/forms/{provider.js → provider.tsx} +20 -14
  40. package/src/javascripts/domains/forms/{selectors.js → selectors.ts} +7 -8
  41. package/src/javascripts/domains/forms/slice.ts +84 -0
  42. package/src/javascripts/domains/forms/utils.ts +15 -0
  43. package/src/javascripts/domains/i18n/actions.ts +24 -0
  44. package/src/javascripts/domains/i18n/{hooks.js → hooks.ts} +2 -2
  45. package/src/javascripts/domains/i18n/i18n.types.ts +6 -0
  46. package/src/javascripts/domains/i18n/selectors.ts +16 -0
  47. package/src/javascripts/domains/i18n/{reducer.js → slice.ts} +40 -37
  48. package/src/javascripts/domains/interrupt/{hooks.js → hooks.ts} +2 -2
  49. package/src/javascripts/domains/interrupt/{middleware.js → middleware.ts} +11 -8
  50. package/src/javascripts/domains/interrupt/selectors.ts +6 -0
  51. package/src/javascripts/domains/interrupt/slice.ts +40 -0
  52. package/src/javascripts/domains/options/middleware.js +9 -6
  53. package/src/javascripts/domains/redux/redux.types.ts +11 -0
  54. package/src/javascripts/domains/store/index.ts +53 -0
  55. package/src/javascripts/domains/store/slice.ts +642 -0
  56. package/src/javascripts/domains/store/store.types.ts +146 -0
  57. package/src/javascripts/domains/translations/components/chat-status.js +2 -2
  58. package/src/javascripts/domains/translations/components/options-button.js +1 -1
  59. package/src/javascripts/domains/translations/components/options-dialog/form.js +5 -5
  60. package/src/javascripts/domains/translations/components/options-dialog/index.js +2 -2
  61. package/src/javascripts/domains/translations/hooks.ts +114 -0
  62. package/src/javascripts/domains/translations/middleware.js +29 -27
  63. package/src/javascripts/domains/translations/selectors.ts +12 -0
  64. package/src/javascripts/domains/translations/slice.ts +120 -0
  65. package/src/javascripts/domains/translations/translations.types.ts +19 -0
  66. package/src/javascripts/domains/visibility/{actions.js → actions.ts} +25 -19
  67. package/src/javascripts/domains/visibility/{hooks.js → hooks.ts} +13 -10
  68. package/src/javascripts/domains/visibility/{selectors.js → selectors.ts} +3 -6
  69. package/src/javascripts/domains/visibility/slice.ts +38 -0
  70. package/src/javascripts/domains/visibility/utils.js +0 -9
  71. package/src/javascripts/domains/visibility/visibility.types.ts +6 -0
  72. package/src/javascripts/index.ts +92 -0
  73. package/src/javascripts/lib/engine/index.js +15 -11
  74. package/src/javascripts/lib/external-api/initialize-api.js +1 -1
  75. package/src/javascripts/lib/id.js +5 -8
  76. package/src/javascripts/lib/mutex.js +3 -1
  77. package/src/javascripts/lib/store/providers/cookie-storage.js +1 -1
  78. package/src/javascripts/lib/store/providers/session-storage.js +1 -1
  79. package/src/javascripts/package/hooks.js +2 -2
  80. package/src/javascripts/package/utils.js +0 -1
  81. package/src/javascripts/schema.ts +1448 -0
  82. package/src/javascripts/style-guide/components/app.js +6 -6
  83. package/src/javascripts/style-guide/components/static-core.js +87 -65
  84. package/src/javascripts/style-guide/components/view.js +4 -4
  85. package/src/javascripts/style-guide/state-helpers/index.js +5 -5
  86. package/src/javascripts/style-guide/states.js +67 -7
  87. package/src/javascripts/style-guide.ts +5 -0
  88. package/src/javascripts/ui/components/app-options/index.js +2 -4
  89. package/src/javascripts/ui/components/conversation/component-filter.js +1 -1
  90. package/src/javascripts/ui/components/conversation/conversation.js +11 -7
  91. package/src/javascripts/ui/components/conversation/event/card-message.js +2 -2
  92. package/src/javascripts/ui/components/conversation/event/carousel-component/components/controls.js +1 -1
  93. package/src/javascripts/ui/components/conversation/event/carousel-message/components/slide.js +1 -1
  94. package/src/javascripts/ui/components/conversation/event/carousel-message/index.js +2 -2
  95. package/src/javascripts/ui/components/conversation/event/choice-prompt.js +3 -3
  96. package/src/javascripts/ui/components/conversation/event/conversation-suggestions.js +19 -15
  97. package/src/javascripts/ui/components/conversation/event/cta.js +2 -2
  98. package/src/javascripts/ui/components/conversation/event/divider/variants/default.js +1 -1
  99. package/src/javascripts/ui/components/conversation/event/divider/variants/new-translation.js +44 -5
  100. package/src/javascripts/ui/components/conversation/event/event-participant.js +2 -2
  101. package/src/javascripts/ui/components/conversation/event/hooks/use-formatted-date.js +2 -2
  102. package/src/javascripts/ui/components/conversation/event/image-lightbox.js +1 -1
  103. package/src/javascripts/ui/components/conversation/event/image.js +6 -8
  104. package/src/javascripts/ui/components/conversation/event/participant.js +2 -2
  105. package/src/javascripts/ui/components/conversation/event/splash.js +4 -4
  106. package/src/javascripts/ui/components/conversation/event/text.js +2 -2
  107. package/src/javascripts/ui/components/conversation/event/translation.js +3 -3
  108. package/src/javascripts/ui/components/conversation/event/upload.js +3 -3
  109. package/src/javascripts/ui/components/conversation/event/video.js +2 -2
  110. package/src/javascripts/ui/components/conversation/message-container.js +4 -26
  111. package/src/javascripts/ui/components/core/seamly-api-context.js +1 -1
  112. package/src/javascripts/ui/components/core/seamly-core.js +15 -14
  113. package/src/javascripts/ui/components/core/seamly-event-subscriber.js +98 -92
  114. package/src/javascripts/ui/components/core/seamly-file-upload.js +20 -24
  115. package/src/javascripts/ui/components/core/seamly-initializer.js +1 -1
  116. package/src/javascripts/ui/components/core/seamly-instance-functions-loader.js +5 -4
  117. package/src/javascripts/ui/components/core/seamly-new-notifications.js +2 -2
  118. package/src/javascripts/ui/components/core/seamly-read-state.js +10 -17
  119. package/src/javascripts/ui/components/entry/deprecated-toggle-button.js +3 -3
  120. package/src/javascripts/ui/components/entry/entry-container.js +4 -6
  121. package/src/javascripts/ui/components/entry/text-entry/hooks.js +3 -3
  122. package/src/javascripts/ui/components/entry/text-entry/index.js +3 -2
  123. package/src/javascripts/ui/components/entry/text-entry/text-entry-form.js +6 -10
  124. package/src/javascripts/ui/components/entry/upload/file-upload-form.js +2 -2
  125. package/src/javascripts/ui/components/entry/upload/index.js +10 -9
  126. package/src/javascripts/ui/components/entry/upload-toggle.js +2 -2
  127. package/src/javascripts/ui/components/faq/faq.js +9 -7
  128. package/src/javascripts/ui/components/form-controls/file-input.js +1 -1
  129. package/src/javascripts/ui/components/form-controls/form.js +1 -1
  130. package/src/javascripts/ui/components/form-controls/input.js +1 -1
  131. package/src/javascripts/ui/components/form-controls/select.js +1 -1
  132. package/src/javascripts/ui/components/layout/agent-info.js +4 -4
  133. package/src/javascripts/ui/components/layout/chat-frame.js +3 -3
  134. package/src/javascripts/ui/components/layout/chat.js +11 -12
  135. package/src/javascripts/ui/components/layout/deprecated-app-frame.js +10 -9
  136. package/src/javascripts/ui/components/layout/header.js +1 -1
  137. package/src/javascripts/ui/components/layout/interrupt.js +23 -24
  138. package/src/javascripts/ui/components/layout/pre-chat-messages.js +11 -11
  139. package/src/javascripts/ui/components/layout/privacy-disclaimer.js +2 -2
  140. package/src/javascripts/ui/components/options/options-button.js +14 -10
  141. package/src/javascripts/ui/components/options/transcript/index.js +2 -2
  142. package/src/javascripts/ui/components/options/transcript/transcript-form.js +1 -1
  143. package/src/javascripts/ui/components/suggestions/index.js +14 -10
  144. package/src/javascripts/ui/components/view/deprecated-view.js +19 -16
  145. package/src/javascripts/ui/components/view/index.js +12 -12
  146. package/src/javascripts/ui/components/view/inline-view.js +2 -2
  147. package/src/javascripts/ui/components/view/window-view/collapse-button.js +3 -3
  148. package/src/javascripts/ui/components/view/window-view/index.js +13 -13
  149. package/src/javascripts/ui/components/view/window-view/window-open-button.js +13 -13
  150. package/src/javascripts/ui/components/warnings/idle-detach-warning.js +1 -1
  151. package/src/javascripts/ui/components/warnings/resume-conversation-prompt.js +1 -1
  152. package/src/javascripts/ui/components/widgets/lightbox.js +2 -2
  153. package/src/javascripts/ui/components/widgets/upload-progress.js +1 -1
  154. package/src/javascripts/ui/hooks/component-helper-hooks.js +1 -1
  155. package/src/javascripts/ui/hooks/file-upload-hooks.js +4 -6
  156. package/src/javascripts/ui/hooks/focus-helper-hooks.js +14 -12
  157. package/src/javascripts/ui/hooks/live-region-hooks.js +2 -0
  158. package/src/javascripts/ui/hooks/seamly-api-hooks.js +8 -3
  159. package/src/javascripts/ui/hooks/seamly-entry-hooks.js +28 -25
  160. package/src/javascripts/ui/hooks/seamly-hooks.js +25 -25
  161. package/src/javascripts/ui/hooks/seamly-option-hooks.js +17 -19
  162. package/src/javascripts/ui/hooks/seamly-state-hooks.js +20 -13
  163. package/src/javascripts/ui/hooks/use-seamly-chat.js +15 -25
  164. package/src/javascripts/ui/hooks/use-seamly-commands.js +46 -46
  165. package/src/javascripts/ui/hooks/use-seamly-idle-detach-countdown.js +22 -24
  166. package/src/javascripts/ui/hooks/use-seamly-resume-conversation-prompt.js +8 -9
  167. package/src/javascripts/ui/hooks/use-single-file-upload.js +4 -6
  168. package/src/javascripts/ui/hooks/utility-hooks.js +4 -4
  169. package/src/javascripts/ui/utils/form-utils.js +0 -145
  170. package/src/javascripts/ui/utils/general-utils.js +3 -4
  171. package/src/javascripts/ui/utils/seamly-utils.ts +73 -0
  172. package/src/stylesheets/5-components/_message-carousel.scss +10 -8
  173. package/webpack/config.common.js +16 -0
  174. package/webpack/config.dev.js +1 -0
  175. package/webpack/config.package.js +26 -5
  176. package/webpack/defaults.js +7 -2
  177. package/webpack/parts/babel-loader-browser-plugins.js +1 -0
  178. package/webpack/parts/dev-server.js +4 -3
  179. package/CHANGELOG.md +0 -791
  180. package/src/javascripts/domains/app/actions.js +0 -112
  181. package/src/javascripts/domains/app/index.js +0 -7
  182. package/src/javascripts/domains/app/reducer.js +0 -16
  183. package/src/javascripts/domains/app/selectors.js +0 -8
  184. package/src/javascripts/domains/app/utils.js +0 -4
  185. package/src/javascripts/domains/config/actions.js +0 -7
  186. package/src/javascripts/domains/config/hooks.js +0 -23
  187. package/src/javascripts/domains/config/index.js +0 -7
  188. package/src/javascripts/domains/config/reducer.js +0 -79
  189. package/src/javascripts/domains/config/selectors.js +0 -23
  190. package/src/javascripts/domains/config/utils.js +0 -4
  191. package/src/javascripts/domains/forms/actions.js +0 -21
  192. package/src/javascripts/domains/forms/context.js +0 -6
  193. package/src/javascripts/domains/forms/index.js +0 -8
  194. package/src/javascripts/domains/forms/reducer.js +0 -84
  195. package/src/javascripts/domains/forms/utils.js +0 -20
  196. package/src/javascripts/domains/i18n/actions.js +0 -20
  197. package/src/javascripts/domains/i18n/index.js +0 -7
  198. package/src/javascripts/domains/i18n/selectors.js +0 -15
  199. package/src/javascripts/domains/i18n/utils.js +0 -4
  200. package/src/javascripts/domains/interrupt/actions.js +0 -4
  201. package/src/javascripts/domains/interrupt/index.js +0 -9
  202. package/src/javascripts/domains/interrupt/reducer.js +0 -22
  203. package/src/javascripts/domains/interrupt/selectors.js +0 -6
  204. package/src/javascripts/domains/interrupt/utils.js +0 -4
  205. package/src/javascripts/domains/options/index.js +0 -1
  206. package/src/javascripts/domains/redux/context.js +0 -6
  207. package/src/javascripts/domains/redux/create-redux-store.js +0 -21
  208. package/src/javascripts/domains/redux/hooks.js +0 -80
  209. package/src/javascripts/domains/redux/index.js +0 -19
  210. package/src/javascripts/domains/redux/provider.js +0 -5
  211. package/src/javascripts/domains/redux/utils.js +0 -12
  212. package/src/javascripts/domains/store/index.js +0 -46
  213. package/src/javascripts/domains/store/state-reducer.js +0 -56
  214. package/src/javascripts/domains/translations/actions.js +0 -11
  215. package/src/javascripts/domains/translations/hooks.js +0 -103
  216. package/src/javascripts/domains/translations/index.js +0 -10
  217. package/src/javascripts/domains/translations/reducer.js +0 -69
  218. package/src/javascripts/domains/translations/selectors.js +0 -16
  219. package/src/javascripts/domains/translations/utils.js +0 -4
  220. package/src/javascripts/domains/visibility/index.js +0 -8
  221. package/src/javascripts/domains/visibility/reducer.js +0 -24
  222. package/src/javascripts/index.js +0 -153
  223. package/src/javascripts/lib/redux-helpers/index.js +0 -99
  224. package/src/javascripts/style-guide.js +0 -5
  225. package/src/javascripts/ui/hooks/use-seamly-dispatch.js +0 -3
  226. package/src/javascripts/ui/utils/seamly-utils.js +0 -832
@@ -0,0 +1,45 @@
1
+ import { createAsyncThunk } from '@reduxjs/toolkit'
2
+ import SeamlyUnavailableError from 'api/errors/seamly-unavailable-error'
3
+ import { ThunkAPI } from 'domains/redux/redux.types'
4
+
5
+ export const initializeConfig = createAsyncThunk<any, void, ThunkAPI>(
6
+ 'initializeConfig',
7
+ async (_, { extra: { api, config }, rejectWithValue }) => {
8
+ try {
9
+ const {
10
+ features,
11
+ defaultLocale,
12
+ preChat,
13
+ agentParticipant,
14
+ userParticipant,
15
+ startChatIcon,
16
+ } = await api.getConfig()
17
+
18
+ const locale = config?.context?.locale || defaultLocale
19
+
20
+ return {
21
+ features,
22
+ defaultLocale,
23
+ preChat,
24
+ agentParticipant,
25
+ userParticipant,
26
+ startChatIcon,
27
+ locale,
28
+ }
29
+ } catch (e) {
30
+ const err = new SeamlyUnavailableError()
31
+ return rejectWithValue({
32
+ name: err.name,
33
+ langKey: err.langKey,
34
+ message: err.message,
35
+ })
36
+ }
37
+ },
38
+ )
39
+
40
+ export const resetConfig = createAsyncThunk<unknown, void, ThunkAPI>(
41
+ 'resetConfig',
42
+ async (_, { extra: { config } }) => {
43
+ return config
44
+ },
45
+ )
@@ -0,0 +1,19 @@
1
+ import { selectConfig } from 'domains/config/selectors'
2
+ import { useSelector } from 'react-redux'
3
+
4
+ export const useConfig = () => useSelector(selectConfig)
5
+
6
+ export function useParticipants() {
7
+ const { agentParticipant, userParticipant } = useConfig()
8
+
9
+ return {
10
+ agent: agentParticipant,
11
+ user: userParticipant,
12
+ }
13
+ }
14
+
15
+ export function useStartChatIcon() {
16
+ const { startChatIcon } = useConfig()
17
+
18
+ return startChatIcon
19
+ }
@@ -0,0 +1,24 @@
1
+ import { createSelector } from '@reduxjs/toolkit'
2
+ import { Config } from 'config.types'
3
+ import { visibilityStates } from 'domains/visibility/constants'
4
+
5
+ export const selectConfig = createSelector(
6
+ ({ config }): Config => config,
7
+ (config) => {
8
+ let newConfig = {
9
+ visible:
10
+ config?.layoutMode === 'inline'
11
+ ? visibilityStates.open
12
+ : visibilityStates.minimized,
13
+ appContainerClassNames: config.appContainerClassNames || [],
14
+ ...config,
15
+ }
16
+ if (typeof newConfig.appContainerClassNames === 'function') {
17
+ newConfig = {
18
+ ...newConfig,
19
+ appContainerClassNames: newConfig.appContainerClassNames(newConfig),
20
+ }
21
+ }
22
+ return newConfig
23
+ },
24
+ )
@@ -0,0 +1,113 @@
1
+ import { createSlice, PayloadAction } from '@reduxjs/toolkit'
2
+ import { defaultConfig } from 'config'
3
+ import { Config } from 'config.types'
4
+ import { initializeConfig, resetConfig } from 'domains/config/actions'
5
+ import { pick } from 'ui/utils/general-utils'
6
+
7
+ export const initialConfigState: Config = {
8
+ ...defaultConfig,
9
+ api: {
10
+ domain: '',
11
+ key: '',
12
+ secure: true,
13
+ },
14
+ hideOnNoUserResponse: false,
15
+ connectWhenInView: true,
16
+ showDisclaimer: false,
17
+ showFaq: false,
18
+ showSuggestions: true,
19
+ customComponents: {},
20
+ defaults: {
21
+ visible: null,
22
+ },
23
+ preChatEvents: [],
24
+ }
25
+
26
+ const configKeys: (keyof Config)[] = [
27
+ 'hideOnNoUserResponse',
28
+ 'connectWhenInView',
29
+ 'showDisclaimer',
30
+ 'showFaq',
31
+ 'showSuggestions',
32
+ 'namespace',
33
+ 'customComponents',
34
+ 'defaults',
35
+ 'layoutMode',
36
+ 'api',
37
+ 'zIndex',
38
+ 'context',
39
+ 'appContainerClassNames',
40
+ 'messages',
41
+ 'visible',
42
+ 'visibilityCallback',
43
+ 'errorCallback',
44
+ 'agentParticipant',
45
+ 'userParticipant',
46
+ 'startChatIcon',
47
+ ]
48
+
49
+ const updateState = (state, config: Config): Config => {
50
+ const { messages, ...partialConfig } = pick(config, configKeys)
51
+ let newState = state
52
+ if (Object.keys(partialConfig).length > 0) {
53
+ newState = {
54
+ ...newState,
55
+ ...partialConfig,
56
+ }
57
+ }
58
+ if (messages) {
59
+ newState = {
60
+ ...newState,
61
+ messages: {
62
+ ...newState.messages,
63
+ ...messages,
64
+ },
65
+ }
66
+ }
67
+ return newState
68
+ }
69
+
70
+ export const configSlice = createSlice({
71
+ name: 'config',
72
+ initialState: initialConfigState,
73
+ reducers: {
74
+ setConfig: (state, { payload }: PayloadAction<Config>) =>
75
+ updateState(state, payload),
76
+ updateConfig: (state, { payload }: PayloadAction<Config>) =>
77
+ updateState(state, payload),
78
+ setPreChatEvents: (state, { payload: { events } }) => {
79
+ state.preChatEvents = events
80
+ },
81
+ },
82
+ extraReducers: (builder) => {
83
+ builder
84
+ .addCase(resetConfig.fulfilled, (config) => config)
85
+ .addCase(
86
+ initializeConfig.fulfilled,
87
+ (
88
+ state,
89
+ {
90
+ payload: {
91
+ preChat,
92
+ agentParticipant,
93
+ userParticipant,
94
+ startChatIcon,
95
+ },
96
+ },
97
+ ) => {
98
+ state.preChatEvents = preChat.map((payload) => ({
99
+ type: 'message',
100
+ payload,
101
+ }))
102
+
103
+ state.agentParticipant = agentParticipant
104
+ state.userParticipant = userParticipant
105
+ state.startChatIcon = startChatIcon
106
+ },
107
+ )
108
+ },
109
+ })
110
+
111
+ export const { setConfig, updateConfig, setPreChatEvents } = configSlice.actions
112
+
113
+ export default configSlice.reducer
@@ -1,15 +1,15 @@
1
- import { createDomain } from 'lib/redux-helpers'
2
- import { Selectors as ConfigSelectors } from 'domains/config'
3
-
4
- const { createAction } = createDomain('errors')
1
+ import { createAction } from '@reduxjs/toolkit'
2
+ import { selectConfig } from 'domains/config/selectors'
5
3
 
6
4
  export const catchError = createAction('catch-error', (error) => ({ error }))
7
5
 
8
- export function createMiddleware({ api: seamlyApi }) {
6
+ export function createErrorsMiddleware({ api: seamlyApi }) {
9
7
  return ({ getState }) => {
10
8
  const handleError = (action) => {
11
- const { errorCallback, namespace, api, layoutMode } =
12
- ConfigSelectors.selectConfig(getState())
9
+ const { errorCallback, namespace, api, layoutMode } = selectConfig(
10
+ getState(),
11
+ )
12
+
13
13
  errorCallback?.(action.error, {
14
14
  namespace,
15
15
  api,
@@ -18,10 +18,14 @@ export function createMiddleware({ api: seamlyApi }) {
18
18
  action: action.type ? action : undefined,
19
19
  })
20
20
  }
21
+
21
22
  return (next) => (action) => {
22
23
  try {
23
- if (action.error) {
24
- handleError(action)
24
+ if (action.payload?.originalEvent?.payload) {
25
+ handleError({
26
+ error: action.payload,
27
+ type: action.payload?.originalEvent?.payload?.type,
28
+ })
25
29
  }
26
30
  return next(action)
27
31
  } catch (error) {
@@ -0,0 +1,14 @@
1
+ import type { FormContextType } from 'domains/forms/forms.types'
2
+ import { createContext } from 'preact'
3
+
4
+ const FormContext = createContext<FormContextType>({
5
+ handleSubmit: () => undefined,
6
+ isSubmitted: false,
7
+ isValid: false,
8
+ updateControlValue: () => undefined,
9
+ updateControlTouched: () => undefined,
10
+ errors: undefined,
11
+ })
12
+
13
+ export default FormContext
14
+ export const { Provider, Consumer } = FormContext
@@ -0,0 +1,24 @@
1
+ export type FormContextType = {
2
+ handleSubmit: (e: Event) => void
3
+ isSubmitted: boolean
4
+ isValid: boolean
5
+ formId?: string
6
+ updateControlValue: (name: string, value: string) => void
7
+ updateControlTouched: (name: string, value: boolean) => void
8
+ errors?: Record<string, unknown>
9
+ validationSchema?: Record<string, unknown>
10
+ values?: Record<string, string>
11
+ }
12
+
13
+ export type ControlState = {
14
+ value?: string
15
+ touched?: boolean
16
+ name?: string
17
+ }
18
+
19
+ export interface FormState {
20
+ [key: string]: {
21
+ persistData?: boolean
22
+ controls: Record<string, ControlState>
23
+ }
24
+ }
@@ -1,22 +1,23 @@
1
+ import { deregisterControl, registerControl } from 'domains/forms/slice'
2
+ import {
3
+ getFormById,
4
+ getControlValueByName,
5
+ getControlTouchedByName,
6
+ } from 'domains/forms/selectors'
1
7
  import {
2
- useCallback,
3
8
  useContext,
9
+ useMemo,
4
10
  useEffect,
5
11
  useLayoutEffect,
6
- useMemo,
7
- useRef,
12
+ useCallback,
8
13
  } from 'preact/hooks'
9
- import { useSelectorWithProps, useStoreDispatch } from 'domains/redux'
14
+ import { useDispatch, useSelector } from 'react-redux'
10
15
  import FormContext from './context'
11
- import * as Actions from './actions'
12
- import {
13
- getControlValueByName,
14
- getFormById,
15
- getControlTouchedByName,
16
- } from './selectors'
17
16
  import { validate } from './utils'
17
+ import type { RootState } from 'domains/store'
18
+ import type { FormContextType } from 'domains/forms/forms.types'
18
19
 
19
- export function useFormContext() {
20
+ export function useFormContext(): FormContextType {
20
21
  return useContext(FormContext)
21
22
  }
22
23
 
@@ -42,21 +43,17 @@ export function useValidations(values, validationSchema) {
42
43
  }
43
44
 
44
45
  export function useFormControl(name) {
45
- const dispatch = useStoreDispatch()
46
+ const dispatch = useDispatch()
46
47
  const { formId, updateControlValue, updateControlTouched, errors } =
47
48
  useFormContext()
48
- const form = useSelectorWithProps(getFormById, { formId }, [formId])
49
+ const form = useSelector((state: RootState) => getFormById(state, { formId }))
49
50
  const isRegistered = !!form
50
- const isRegisteredRef = useRef()
51
- isRegisteredRef.current = isRegistered
52
- const value = useSelectorWithProps(getControlValueByName, { formId, name }, [
53
- formId,
54
- name,
55
- ])
56
- const touched = useSelectorWithProps(
57
- getControlTouchedByName,
58
- { formId, name },
59
- [formId, name],
51
+ const value = useSelector((state: RootState) => {
52
+ return getControlValueByName(state, { formId, name })
53
+ })
54
+
55
+ const touched = useSelector((state: RootState) =>
56
+ getControlTouchedByName(state, { formId, name }),
60
57
  )
61
58
  const error = errors?.[name]
62
59
  const isValid = !error
@@ -64,13 +61,13 @@ export function useFormControl(name) {
64
61
  useEffect(() => {
65
62
  // Make sure the form is registered
66
63
  // Since child useEffect runs before FormProvider useEffect
67
- if (isRegisteredRef.current) {
68
- dispatch(Actions.registerControl(formId, name))
64
+ if (isRegistered) {
65
+ dispatch(registerControl({ formId, name }))
69
66
  }
70
67
  }, [isRegistered, formId, name, dispatch])
71
68
  useLayoutEffect(() => {
72
69
  return () => {
73
- dispatch(Actions.deregisterControl(formId, name))
70
+ dispatch(deregisterControl({ formId, name }))
74
71
  }
75
72
  }, [isRegistered, formId, name, dispatch])
76
73
 
@@ -1,3 +1,14 @@
1
+ import { setHasResponded } from 'domains/app/slice'
2
+ import { Provider } from 'domains/forms/context'
3
+ import { useValidations } from 'domains/forms/hooks'
4
+ import { getFormValuesByFormId } from 'domains/forms/selectors'
5
+ import {
6
+ deregisterForm,
7
+ registerForm,
8
+ updateControlTouched as dispatchControlTouched,
9
+ updateControlValue as dispatchControlValue,
10
+ } from 'domains/forms/slice'
11
+ import type { RootState } from 'domains/store'
1
12
  import {
2
13
  useCallback,
3
14
  useEffect,
@@ -5,12 +16,7 @@ import {
5
16
  useMemo,
6
17
  useState,
7
18
  } from 'preact/hooks'
8
- import { useSelectorWithProps, useStoreDispatch } from 'domains/redux'
9
- import { setHasResponded } from 'domains/app/actions'
10
- import { Provider } from 'domains/forms/context'
11
- import * as Actions from 'domains/forms/actions'
12
- import { getFormValuesByFormId } from 'domains/forms/selectors'
13
- import { useValidations } from 'domains/forms/hooks'
19
+ import { useDispatch, useSelector } from 'react-redux'
14
20
 
15
21
  export default function FormProvider({
16
22
  children,
@@ -21,10 +27,10 @@ export default function FormProvider({
21
27
  validationSchema,
22
28
  ...props
23
29
  }) {
24
- const dispatch = useStoreDispatch()
25
- const values = useSelectorWithProps(getFormValuesByFormId, { formId }, [
26
- formId,
27
- ])
30
+ const dispatch = useDispatch()
31
+ const values = useSelector((store: RootState) =>
32
+ getFormValuesByFormId(store, { formId }),
33
+ )
28
34
  const [isSubmitted, setIsSubmitted] = useState(false)
29
35
  const [externalErrors, setExternalErrors] = useState({})
30
36
  const { isValid: validationIsValid, errors: validationErrors } =
@@ -40,27 +46,27 @@ export default function FormProvider({
40
46
  // register
41
47
  useLayoutEffect(() => {
42
48
  // register form in redux store
43
- dispatch(Actions.registerForm(formId, persistData))
49
+ dispatch(registerForm({ formId, persistData }))
44
50
  }, [formId, persistData, dispatch])
45
51
 
46
52
  // deregister
47
53
  useEffect(() => {
48
54
  return () => {
49
55
  // deregister form from redux store
50
- dispatch(Actions.deregisterForm(formId))
56
+ dispatch(deregisterForm({ formId }))
51
57
  }
52
58
  }, [formId, persistData, dispatch])
53
59
 
54
60
  const updateControlValue = useCallback(
55
61
  (name, value) => {
56
- dispatch(Actions.updateControlValue(formId, name, value))
62
+ dispatch(dispatchControlValue({ formId, name, value }))
57
63
  },
58
64
  [formId, dispatch],
59
65
  )
60
66
 
61
67
  const updateControlTouched = useCallback(
62
68
  (name, touched) => {
63
- dispatch(Actions.updateControlTouched(formId, name, touched))
69
+ dispatch(dispatchControlTouched({ formId, name, touched }))
64
70
  },
65
71
  [dispatch, formId],
66
72
  )
@@ -1,12 +1,10 @@
1
- import { createSelector } from 'reselect'
2
- import { getPropSelector } from 'domains/redux/utils'
3
- import { selectState } from './utils'
1
+ import { createSelector } from '@reduxjs/toolkit'
4
2
 
5
- export const getState = selectState
3
+ export const getState = ({ forms }) => forms
6
4
 
7
5
  export const getFormById = createSelector(
8
6
  getState,
9
- getPropSelector('formId'),
7
+ (_, { formId }) => formId,
10
8
  (forms, formId) => forms[formId],
11
9
  )
12
10
 
@@ -17,23 +15,24 @@ export const getFormControlsByFormId = createSelector(
17
15
 
18
16
  export const getFormValuesByFormId = createSelector(
19
17
  getFormControlsByFormId,
20
- (controls) => {
18
+ (controls: { [key: string]: { value: string } }) => {
21
19
  const valuesObj = {}
22
20
  Object.entries(controls).forEach(([key, { value }]) => {
23
21
  valuesObj[key] = value
24
22
  })
23
+
25
24
  return valuesObj
26
25
  },
27
26
  )
28
27
 
29
28
  export const getControlValueByName = createSelector(
30
29
  getFormControlsByFormId,
31
- getPropSelector('name'),
30
+ (_, { name }) => name,
32
31
  (controls, name) => controls[name]?.value,
33
32
  )
34
33
 
35
34
  export const getControlTouchedByName = createSelector(
36
35
  getFormControlsByFormId,
37
- getPropSelector('name'),
36
+ (_, { name }) => name,
38
37
  (controls, name) => controls[name]?.touched,
39
38
  )
@@ -0,0 +1,84 @@
1
+ import { createSlice } from '@reduxjs/toolkit'
2
+ import { resetApp } from 'domains/app/actions'
3
+ import { ControlState } from 'domains/forms/forms.types'
4
+
5
+ const initialFormState = {
6
+ controls: {},
7
+ }
8
+
9
+ const initialControlState: ControlState = {
10
+ value: '',
11
+ touched: false,
12
+ }
13
+
14
+ const initialState = {
15
+ controls: initialControlState,
16
+ }
17
+
18
+ export const formsSlice = createSlice({
19
+ name: 'forms',
20
+ initialState,
21
+ reducers: {
22
+ registerForm: (state, { payload: { persistData, formId } }) => {
23
+ const formState = persistData
24
+ ? state[formId] ?? { ...initialFormState, persistData }
25
+ : { ...initialFormState, persistData }
26
+
27
+ state[formId] = formState
28
+ },
29
+ deregisterForm: (state, { payload: { formId } }) => {
30
+ if (!state[formId]?.persistData) {
31
+ delete state[formId]
32
+ }
33
+
34
+ return state
35
+ },
36
+ // Form control handlers
37
+ registerControl: (state, { payload: { name, formId } }) => {
38
+ state[formId].controls = {
39
+ [name]: initialControlState,
40
+ }
41
+ },
42
+ deregisterControl: (state, { payload: { name, formId } }) => {
43
+ const form = state[formId]
44
+ if (!form) {
45
+ return state
46
+ }
47
+ if (form.persistData) {
48
+ return state
49
+ }
50
+ const controls = { ...form.controls }
51
+ delete controls[name]
52
+ return {
53
+ ...state,
54
+ [formId]: {
55
+ ...form,
56
+ controls,
57
+ },
58
+ }
59
+ },
60
+ updateControlValue: (state, { payload: { formId, name, value } }) => {
61
+ // return updateFormControl(state, formId, name, { value })
62
+ if (!state[formId]?.controls) return
63
+ state[formId].controls[name].value = value
64
+ },
65
+ updateControlTouched: (state, { payload: { formId, name, touched } }) => {
66
+ if (!state[formId]?.controls[name]) return
67
+ state[formId].controls[name].touched = touched
68
+ },
69
+ },
70
+ extraReducers: (builder) => {
71
+ builder.addCase(resetApp.pending, () => initialState)
72
+ },
73
+ })
74
+
75
+ export const {
76
+ registerForm,
77
+ deregisterForm,
78
+ registerControl,
79
+ deregisterControl,
80
+ updateControlValue,
81
+ updateControlTouched,
82
+ } = formsSlice.actions
83
+
84
+ export default formsSlice.reducer
@@ -0,0 +1,15 @@
1
+ export function validate(values, schema: Record<string, unknown> = {}) {
2
+ return Object.entries(schema).reduce((errors, [key, validations]) => {
3
+ const validationsArr = !Array.isArray(validations)
4
+ ? [validations]
5
+ : validations
6
+
7
+ for (let i = 0; i < validationsArr?.length ?? 0; i++) {
8
+ if (!validationsArr[i].fn(values[key], validationsArr[i].compareValue)) {
9
+ errors[key] = validationsArr[i].errorText
10
+ break
11
+ }
12
+ }
13
+ return errors
14
+ }, {})
15
+ }
@@ -0,0 +1,24 @@
1
+ import { createAsyncThunk } from '@reduxjs/toolkit'
2
+ import { selectLocale } from 'domains/i18n/selectors'
3
+ import { ThunkAPI } from 'domains/redux/redux.types'
4
+
5
+ export const setLocale = createAsyncThunk<
6
+ {
7
+ translations: Record<string, string>
8
+ locale: string
9
+ },
10
+ string,
11
+ ThunkAPI
12
+ >('setLocale', async (locale, { getState, extra: { api } }) => {
13
+ const stateLocale = selectLocale(getState())
14
+ if (locale === stateLocale) {
15
+ return undefined
16
+ }
17
+
18
+ const response = await api.getTranslations(locale)
19
+
20
+ return {
21
+ translations: response,
22
+ locale,
23
+ }
24
+ })
@@ -1,10 +1,10 @@
1
- import { useCallback } from 'preact/hooks'
2
1
  import {
3
2
  MessageFormatter,
4
3
  pluralTypeHandler,
5
4
  selectTypeHandler,
6
5
  } from '@ultraq/icu-message-formatter'
7
- import { useSelector } from 'domains/redux'
6
+ import { useCallback } from 'preact/hooks'
7
+ import { useSelector } from 'react-redux'
8
8
  import * as Selectors from './selectors'
9
9
 
10
10
  // The passed in locale (en-GB) is only used to call Intl.PluralRules.select() in
@@ -0,0 +1,6 @@
1
+ export interface I18nState {
2
+ translations: Record<string, string>
3
+ isLoading: boolean
4
+ initialLocale?: string
5
+ locale?: string
6
+ }
@@ -0,0 +1,16 @@
1
+ import { createSelector } from 'reselect'
2
+
3
+ export const selectTranslations = createSelector(
4
+ ({ i18n }) => i18n,
5
+ ({ translations }) => translations,
6
+ )
7
+
8
+ export const selectInitialLocale = createSelector(
9
+ ({ i18n }) => i18n,
10
+ ({ initialLocale }) => initialLocale,
11
+ )
12
+
13
+ export const selectLocale = createSelector(
14
+ ({ i18n }) => i18n,
15
+ ({ locale }) => locale,
16
+ )