@tanstack/devtools 0.6.24 → 0.8.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.
@@ -1,6 +1,10 @@
1
1
  import { beforeEach, describe, expect, it } from 'vitest'
2
2
  import { TANSTACK_DEVTOOLS_STATE } from '../utils/storage'
3
- import { getStateFromLocalStorage } from './devtools-context'
3
+ import {
4
+ getExistingStateFromStorage,
5
+ getStateFromLocalStorage,
6
+ } from './devtools-context'
7
+ import type { TanStackDevtoolsPlugin } from './devtools-context'
4
8
 
5
9
  describe('getStateFromLocalStorage', () => {
6
10
  beforeEach(() => {
@@ -56,4 +60,267 @@ describe('getStateFromLocalStorage', () => {
56
60
  const state = getStateFromLocalStorage(undefined)
57
61
  expect(state).toEqual(undefined)
58
62
  })
63
+
64
+ it('should return undefined when no localStorage state exists (allowing defaultOpen to be applied)', () => {
65
+ // No existing state in localStorage - this allows defaultOpen logic to trigger
66
+ const plugins: Array<TanStackDevtoolsPlugin> = [
67
+ {
68
+ id: 'plugin1',
69
+ render: () => {},
70
+ name: 'Plugin 1',
71
+ defaultOpen: true,
72
+ },
73
+ {
74
+ id: 'plugin2',
75
+ render: () => {},
76
+ name: 'Plugin 2',
77
+ defaultOpen: false,
78
+ },
79
+ {
80
+ id: 'plugin3',
81
+ render: () => {},
82
+ name: 'Plugin 3',
83
+ defaultOpen: true,
84
+ },
85
+ ]
86
+
87
+ // When undefined is returned, getExistingStateFromStorage will fill activePlugins with defaultOpen plugins
88
+ const state = getStateFromLocalStorage(plugins)
89
+ expect(state).toEqual(undefined)
90
+ })
91
+
92
+ it('should preserve existing activePlugins from localStorage (defaultOpen should not override)', () => {
93
+ const mockState = {
94
+ activePlugins: ['plugin2'],
95
+ settings: {
96
+ theme: 'dark',
97
+ },
98
+ }
99
+ localStorage.setItem(TANSTACK_DEVTOOLS_STATE, JSON.stringify(mockState))
100
+
101
+ const plugins: Array<TanStackDevtoolsPlugin> = [
102
+ {
103
+ id: 'plugin1',
104
+ render: () => {},
105
+ name: 'Plugin 1',
106
+ defaultOpen: true,
107
+ },
108
+ {
109
+ id: 'plugin2',
110
+ render: () => {},
111
+ name: 'Plugin 2',
112
+ defaultOpen: false,
113
+ },
114
+ ]
115
+
116
+ const state = getStateFromLocalStorage(plugins)
117
+ // Should keep existing activePlugins - defaultOpen logic won't override in getExistingStateFromStorage
118
+ expect(state?.activePlugins).toEqual(['plugin2'])
119
+ })
120
+
121
+ it('should automatically activate a single plugin when no active plugins exist', () => {
122
+ // No existing state in localStorage
123
+ const plugins: Array<TanStackDevtoolsPlugin> = [
124
+ {
125
+ id: 'only-plugin',
126
+ render: () => {},
127
+ name: 'Only Plugin',
128
+ },
129
+ ]
130
+
131
+ const state = getStateFromLocalStorage(plugins)
132
+ // Should return undefined - the single plugin activation happens in getExistingStateFromStorage
133
+ expect(state).toEqual(undefined)
134
+ })
135
+ })
136
+
137
+ describe('getExistingStateFromStorage - integration tests', () => {
138
+ beforeEach(() => {
139
+ localStorage.clear()
140
+ })
141
+
142
+ it('should automatically activate a single plugin when no localStorage state exists', () => {
143
+ const plugins: Array<TanStackDevtoolsPlugin> = [
144
+ {
145
+ id: 'only-plugin',
146
+ render: () => {},
147
+ name: 'Only Plugin',
148
+ },
149
+ ]
150
+
151
+ const state = getExistingStateFromStorage(undefined, plugins)
152
+ expect(state.state.activePlugins).toEqual(['only-plugin'])
153
+ expect(state.plugins).toHaveLength(1)
154
+ expect(state.plugins![0]?.id).toBe('only-plugin')
155
+ })
156
+
157
+ it('should activate plugins with defaultOpen: true when no localStorage state exists', () => {
158
+ const plugins: Array<TanStackDevtoolsPlugin> = [
159
+ {
160
+ id: 'plugin1',
161
+ render: () => {},
162
+ name: 'Plugin 1',
163
+ defaultOpen: true,
164
+ },
165
+ {
166
+ id: 'plugin2',
167
+ render: () => {},
168
+ name: 'Plugin 2',
169
+ defaultOpen: false,
170
+ },
171
+ {
172
+ id: 'plugin3',
173
+ render: () => {},
174
+ name: 'Plugin 3',
175
+ defaultOpen: true,
176
+ },
177
+ ]
178
+
179
+ const state = getExistingStateFromStorage(undefined, plugins)
180
+ expect(state.state.activePlugins).toEqual(['plugin1', 'plugin3'])
181
+ expect(state.plugins).toHaveLength(3)
182
+ })
183
+
184
+ it('should limit defaultOpen plugins to MAX_ACTIVE_PLUGINS (3) when 5 have defaultOpen: true', () => {
185
+ const plugins: Array<TanStackDevtoolsPlugin> = [
186
+ {
187
+ id: 'plugin1',
188
+ render: () => {},
189
+ name: 'Plugin 1',
190
+ defaultOpen: true,
191
+ },
192
+ {
193
+ id: 'plugin2',
194
+ render: () => {},
195
+ name: 'Plugin 2',
196
+ defaultOpen: true,
197
+ },
198
+ {
199
+ id: 'plugin3',
200
+ render: () => {},
201
+ name: 'Plugin 3',
202
+ defaultOpen: true,
203
+ },
204
+ {
205
+ id: 'plugin4',
206
+ render: () => {},
207
+ name: 'Plugin 4',
208
+ defaultOpen: true,
209
+ },
210
+ {
211
+ id: 'plugin5',
212
+ render: () => {},
213
+ name: 'Plugin 5',
214
+ defaultOpen: true,
215
+ },
216
+ ]
217
+
218
+ const state = getExistingStateFromStorage(undefined, plugins)
219
+ // Should only activate first 3 plugins
220
+ expect(state.state.activePlugins).toEqual(['plugin1', 'plugin2', 'plugin3'])
221
+ expect(state.state.activePlugins).toHaveLength(3)
222
+ expect(state.state.activePlugins).not.toContain('plugin4')
223
+ expect(state.state.activePlugins).not.toContain('plugin5')
224
+ // All 5 plugins should still be in the plugins array
225
+ expect(state.plugins).toHaveLength(5)
226
+ })
227
+
228
+ it('should preserve existing activePlugins from localStorage even when plugins have defaultOpen', () => {
229
+ const mockState = {
230
+ activePlugins: ['plugin2', 'plugin4'],
231
+ settings: {
232
+ theme: 'dark',
233
+ },
234
+ }
235
+ localStorage.setItem(TANSTACK_DEVTOOLS_STATE, JSON.stringify(mockState))
236
+
237
+ const plugins: Array<TanStackDevtoolsPlugin> = [
238
+ {
239
+ id: 'plugin1',
240
+ render: () => {},
241
+ name: 'Plugin 1',
242
+ defaultOpen: true,
243
+ },
244
+ {
245
+ id: 'plugin2',
246
+ render: () => {},
247
+ name: 'Plugin 2',
248
+ defaultOpen: false,
249
+ },
250
+ {
251
+ id: 'plugin3',
252
+ render: () => {},
253
+ name: 'Plugin 3',
254
+ defaultOpen: true,
255
+ },
256
+ {
257
+ id: 'plugin4',
258
+ render: () => {},
259
+ name: 'Plugin 4',
260
+ defaultOpen: false,
261
+ },
262
+ ]
263
+
264
+ const state = getExistingStateFromStorage(undefined, plugins)
265
+ // Should preserve the localStorage state, not use defaultOpen
266
+ expect(state.state.activePlugins).toEqual(['plugin2', 'plugin4'])
267
+ expect(state.plugins).toHaveLength(4)
268
+ })
269
+
270
+ it('should return empty activePlugins when no defaultOpen and multiple plugins', () => {
271
+ const plugins: Array<TanStackDevtoolsPlugin> = [
272
+ {
273
+ id: 'plugin1',
274
+ render: () => {},
275
+ name: 'Plugin 1',
276
+ },
277
+ {
278
+ id: 'plugin2',
279
+ render: () => {},
280
+ name: 'Plugin 2',
281
+ },
282
+ {
283
+ id: 'plugin3',
284
+ render: () => {},
285
+ name: 'Plugin 3',
286
+ },
287
+ ]
288
+
289
+ const state = getExistingStateFromStorage(undefined, plugins)
290
+ expect(state.state.activePlugins).toEqual([])
291
+ expect(state.plugins).toHaveLength(3)
292
+ })
293
+
294
+ it('should handle single plugin with defaultOpen: false by activating it anyway', () => {
295
+ const plugins: Array<TanStackDevtoolsPlugin> = [
296
+ {
297
+ id: 'only-plugin',
298
+ render: () => {},
299
+ name: 'Only Plugin',
300
+ defaultOpen: false,
301
+ },
302
+ ]
303
+
304
+ const state = getExistingStateFromStorage(undefined, plugins)
305
+ // Single plugin should be activated regardless of defaultOpen flag
306
+ expect(state.state.activePlugins).toEqual(['only-plugin'])
307
+ })
308
+
309
+ it('should merge config settings into the returned state', () => {
310
+ const plugins: Array<TanStackDevtoolsPlugin> = [
311
+ {
312
+ id: 'plugin1',
313
+ render: () => {},
314
+ name: 'Plugin 1',
315
+ },
316
+ ]
317
+
318
+ const config = {
319
+ theme: 'light' as const,
320
+ }
321
+
322
+ const state = getExistingStateFromStorage(config as any, plugins)
323
+ expect(state.settings.theme).toBe('light')
324
+ expect(state.state.activePlugins).toEqual(['plugin1'])
325
+ })
59
326
  })
@@ -1,5 +1,6 @@
1
1
  import { createContext, createEffect } from 'solid-js'
2
2
  import { createStore } from 'solid-js/store'
3
+ import { getDefaultActivePlugins } from '../utils/get-default-active-plugins'
3
4
  import { tryParseJson } from '../utils/sanitize'
4
5
  import {
5
6
  TANSTACK_DEVTOOLS_SETTINGS,
@@ -49,6 +50,12 @@ export interface TanStackDevtoolsPlugin {
49
50
  * If not provided, it will be generated based on the name.
50
51
  */
51
52
  id?: string
53
+ /**
54
+ * Whether the plugin should be open by default when there are no active plugins.
55
+ * If true, this plugin will be added to activePlugins on initial load when activePlugins is empty.
56
+ * @default false
57
+ */
58
+ defaultOpen?: boolean
52
59
  /**
53
60
  * Render the plugin UI by using the provided element. This function will be called
54
61
  * when the plugin tab is clicked and expected to be mounted.
@@ -127,26 +134,39 @@ export function getStateFromLocalStorage(
127
134
  return existingState
128
135
  }
129
136
 
130
- const getExistingStateFromStorage = (
137
+ export const getExistingStateFromStorage = (
131
138
  config?: TanStackDevtoolsConfig,
132
139
  plugins?: Array<TanStackDevtoolsPlugin>,
133
140
  ) => {
134
141
  const existingState = getStateFromLocalStorage(plugins)
135
142
  const settings = getSettings()
136
143
 
144
+ const pluginsWithIds =
145
+ plugins?.map((plugin, i) => {
146
+ const id = generatePluginId(plugin, i)
147
+ return {
148
+ ...plugin,
149
+ id,
150
+ }
151
+ }) || []
152
+
153
+ // If no active plugins exist, add plugins with defaultOpen: true
154
+ // Or if there's only 1 plugin, activate it automatically
155
+ let activePlugins = existingState?.activePlugins || []
156
+
157
+ const shouldFillWithDefaultOpenPlugins =
158
+ activePlugins.length === 0 && pluginsWithIds.length > 0
159
+ if (shouldFillWithDefaultOpenPlugins) {
160
+ activePlugins = getDefaultActivePlugins(pluginsWithIds)
161
+ }
162
+
137
163
  const state: DevtoolsStore = {
138
164
  ...initialState,
139
- plugins:
140
- plugins?.map((plugin, i) => {
141
- const id = generatePluginId(plugin, i)
142
- return {
143
- ...plugin,
144
- id,
145
- }
146
- }) || [],
165
+ plugins: pluginsWithIds,
147
166
  state: {
148
167
  ...initialState.state,
149
168
  ...existingState,
169
+ activePlugins,
150
170
  },
151
171
  settings: {
152
172
  ...initialState.settings,
@@ -18,6 +18,10 @@ type TriggerPosition =
18
18
  | 'middle-left'
19
19
  | 'middle-right'
20
20
 
21
+ type TriggerProps = {
22
+ theme: 'light' | 'dark'
23
+ }
24
+
21
25
  export type DevtoolsStore = {
22
26
  settings: {
23
27
  /**
@@ -61,15 +65,17 @@ export type DevtoolsStore = {
61
65
  * @default "dark"
62
66
  */
63
67
  theme: 'light' | 'dark'
64
- /**
65
- * The image used for the dev tools trigger
66
- * @default TanStackLogo
67
- */
68
- triggerImage: string
68
+
69
69
  /**
70
70
  * Whether the trigger should be completely hidden or not (you can still open with the hotkey)
71
71
  */
72
72
  triggerHidden?: boolean
73
+ /**
74
+ * An optional custom function to render the dev tools trigger component.
75
+ * If provided, it replaces the default trigger button.
76
+ * @default undefined
77
+ */
78
+ customTrigger?: (el: HTMLElement, props: TriggerProps) => void
73
79
  }
74
80
  state: {
75
81
  activeTab: TabName
@@ -95,8 +101,8 @@ export const initialState: DevtoolsStore = {
95
101
  window.matchMedia('(prefers-color-scheme: dark)').matches
96
102
  ? 'dark'
97
103
  : 'light',
98
- triggerImage: '',
99
104
  triggerHidden: false,
105
+ customTrigger: undefined,
100
106
  },
101
107
  state: {
102
108
  activeTab: 'plugins',
@@ -1,4 +1,5 @@
1
1
  import { createEffect, createMemo, useContext } from 'solid-js'
2
+ import { MAX_ACTIVE_PLUGINS } from '../utils/constants.js'
2
3
  import { DevtoolsContext } from './devtools-context.jsx'
3
4
  import { useDrawContext } from './draw-context.jsx'
4
5
 
@@ -50,7 +51,7 @@ export const usePlugins = () => {
50
51
  const updatedPlugins = isActive
51
52
  ? prev.state.activePlugins.filter((id) => id !== pluginId)
52
53
  : [...prev.state.activePlugins, pluginId]
53
- if (updatedPlugins.length > 3) return prev
54
+ if (updatedPlugins.length > MAX_ACTIVE_PLUGINS) return prev
54
55
  return {
55
56
  ...prev,
56
57
  state: {
package/src/devtools.tsx CHANGED
@@ -1,7 +1,8 @@
1
- import { Show, createEffect, createSignal } from 'solid-js'
1
+ import { Show, createEffect, createSignal, onCleanup } from 'solid-js'
2
2
  import { createShortcut } from '@solid-primitives/keyboard'
3
3
  import { Portal } from 'solid-js/web'
4
4
  import { ThemeContextProvider } from '@tanstack/devtools-ui'
5
+ import { devtoolsEventClient } from '@tanstack/devtools-client'
5
6
  import {
6
7
  useDevtoolsSettings,
7
8
  useHeight,
@@ -31,14 +32,36 @@ export default function DevTools() {
31
32
  const pip = usePiPWindow()
32
33
  let panelRef: HTMLDivElement | undefined = undefined
33
34
  const [isResizing, setIsResizing] = createSignal(false)
35
+
34
36
  const toggleOpen = () => {
35
37
  if (pip().pipWindow) {
36
38
  return
37
39
  }
38
40
  const open = isOpen()
39
- setIsOpen(!open)
40
- setPersistOpen(!open)
41
+ const newState = !open
42
+ setIsOpen(newState)
43
+ setPersistOpen(newState)
44
+
45
+ // Emit event when user toggles devtools
46
+ devtoolsEventClient.emit('trigger-toggled', { isOpen: newState })
41
47
  }
48
+
49
+ // Listen for trigger-toggled events to control devtools
50
+ createEffect(() => {
51
+ const unsubscribe = devtoolsEventClient.on('trigger-toggled', (event) => {
52
+ if (pip().pipWindow) {
53
+ return
54
+ }
55
+ const payload = event.payload as unknown as { isOpen: boolean }
56
+ const shouldBeOpen = payload.isOpen
57
+ if (shouldBeOpen !== isOpen()) {
58
+ setIsOpen(shouldBeOpen)
59
+ setPersistOpen(shouldBeOpen)
60
+ }
61
+ })
62
+
63
+ onCleanup(unsubscribe)
64
+ })
42
65
  // Used to resize the panel
43
66
  const handleDragStart = (
44
67
  panelElement: HTMLDivElement | undefined,
@@ -175,11 +198,7 @@ export default function DevTools() {
175
198
  : true
176
199
  }
177
200
  >
178
- <Trigger
179
- isOpen={isOpen}
180
- setIsOpen={toggleOpen}
181
- image={settings().triggerImage}
182
- />
201
+ <Trigger isOpen={isOpen} setIsOpen={toggleOpen} />
183
202
  <MainPanel isResizing={isResizing} isOpen={isOpen}>
184
203
  <ContentPanel
185
204
  ref={(ref) => (panelRef = ref)}
@@ -79,13 +79,7 @@ export const SettingsTab = () => {
79
79
  }
80
80
  checked={settings().triggerHidden}
81
81
  />
82
- <Input
83
- label="Trigger Image"
84
- description="Specify the URL of the image to use for the trigger"
85
- value={settings().triggerImage}
86
- placeholder="Default TanStack Logo"
87
- onChange={(value) => setSettings({ triggerImage: value })}
88
- />
82
+
89
83
  <Select
90
84
  label="Theme"
91
85
  description="Choose the theme for the devtools panel"
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Maximum number of plugins that can be active simultaneously in the devtools
3
+ */
4
+ export const MAX_ACTIVE_PLUGINS = 3
@@ -0,0 +1,194 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getDefaultActivePlugins } from './get-default-active-plugins'
3
+ import type { PluginWithId } from './get-default-active-plugins'
4
+
5
+ describe('getDefaultActivePlugins', () => {
6
+ it('should return empty array when no plugins provided', () => {
7
+ const result = getDefaultActivePlugins([])
8
+ expect(result).toEqual([])
9
+ })
10
+
11
+ it('should automatically activate a single plugin', () => {
12
+ const plugins: Array<PluginWithId> = [
13
+ {
14
+ id: 'only-plugin',
15
+ },
16
+ ]
17
+
18
+ const result = getDefaultActivePlugins(plugins)
19
+ expect(result).toEqual(['only-plugin'])
20
+ })
21
+
22
+ it('should automatically activate a single plugin even if defaultOpen is false', () => {
23
+ const plugins: Array<PluginWithId> = [
24
+ {
25
+ id: 'only-plugin',
26
+ defaultOpen: false,
27
+ },
28
+ ]
29
+
30
+ const result = getDefaultActivePlugins(plugins)
31
+ expect(result).toEqual(['only-plugin'])
32
+ })
33
+
34
+ it('should return empty array when multiple plugins without defaultOpen', () => {
35
+ const plugins: Array<PluginWithId> = [
36
+ {
37
+ id: 'plugin1',
38
+ },
39
+ {
40
+ id: 'plugin2',
41
+ },
42
+ {
43
+ id: 'plugin3',
44
+ },
45
+ ]
46
+
47
+ const result = getDefaultActivePlugins(plugins)
48
+ expect(result).toEqual([])
49
+ })
50
+
51
+ it('should activate plugins with defaultOpen: true', () => {
52
+ const plugins: Array<PluginWithId> = [
53
+ {
54
+ id: 'plugin1',
55
+ defaultOpen: true,
56
+ },
57
+ {
58
+ id: 'plugin2',
59
+ defaultOpen: false,
60
+ },
61
+ {
62
+ id: 'plugin3',
63
+ defaultOpen: true,
64
+ },
65
+ ]
66
+
67
+ const result = getDefaultActivePlugins(plugins)
68
+ expect(result).toEqual(['plugin1', 'plugin3'])
69
+ })
70
+
71
+ it('should limit defaultOpen plugins to MAX_ACTIVE_PLUGINS (3)', () => {
72
+ const plugins: Array<PluginWithId> = [
73
+ {
74
+ id: 'plugin1',
75
+ defaultOpen: true,
76
+ },
77
+ {
78
+ id: 'plugin2',
79
+ defaultOpen: true,
80
+ },
81
+ {
82
+ id: 'plugin3',
83
+ defaultOpen: true,
84
+ },
85
+ {
86
+ id: 'plugin4',
87
+ defaultOpen: true,
88
+ },
89
+ {
90
+ id: 'plugin5',
91
+ defaultOpen: true,
92
+ },
93
+ ]
94
+
95
+ const result = getDefaultActivePlugins(plugins)
96
+ // Should only return first 3
97
+ expect(result).toEqual(['plugin1', 'plugin2', 'plugin3'])
98
+ expect(result.length).toBe(3)
99
+ })
100
+
101
+ it('should activate exactly MAX_ACTIVE_PLUGINS when that many have defaultOpen', () => {
102
+ const plugins: Array<PluginWithId> = [
103
+ {
104
+ id: 'plugin1',
105
+ defaultOpen: true,
106
+ },
107
+ {
108
+ id: 'plugin2',
109
+ defaultOpen: true,
110
+ },
111
+ {
112
+ id: 'plugin3',
113
+ defaultOpen: true,
114
+ },
115
+ {
116
+ id: 'plugin4',
117
+ defaultOpen: false,
118
+ },
119
+ ]
120
+
121
+ const result = getDefaultActivePlugins(plugins)
122
+ expect(result).toEqual(['plugin1', 'plugin2', 'plugin3'])
123
+ expect(result.length).toBe(3)
124
+ })
125
+
126
+ it('should handle mix of defaultOpen true/false/undefined', () => {
127
+ const plugins: Array<PluginWithId> = [
128
+ {
129
+ id: 'plugin1',
130
+ defaultOpen: true,
131
+ },
132
+ {
133
+ id: 'plugin2',
134
+ // undefined defaultOpen
135
+ },
136
+ {
137
+ id: 'plugin3',
138
+ defaultOpen: false,
139
+ },
140
+ {
141
+ id: 'plugin4',
142
+ defaultOpen: true,
143
+ },
144
+ ]
145
+
146
+ const result = getDefaultActivePlugins(plugins)
147
+ // Only plugin1 and plugin4 have defaultOpen: true
148
+ expect(result).toEqual(['plugin1', 'plugin4'])
149
+ })
150
+
151
+ it('should return single plugin even if it has defaultOpen: true', () => {
152
+ const plugins: Array<PluginWithId> = [
153
+ {
154
+ id: 'only-plugin',
155
+ defaultOpen: true,
156
+ },
157
+ ]
158
+
159
+ const result = getDefaultActivePlugins(plugins)
160
+ expect(result).toEqual(['only-plugin'])
161
+ })
162
+
163
+ it('should stop at MAX_ACTIVE_PLUGINS limit when 5 plugins have defaultOpen: true', () => {
164
+ const plugins: Array<PluginWithId> = [
165
+ {
166
+ id: 'plugin1',
167
+ defaultOpen: true,
168
+ },
169
+ {
170
+ id: 'plugin2',
171
+ defaultOpen: true,
172
+ },
173
+ {
174
+ id: 'plugin3',
175
+ defaultOpen: true,
176
+ },
177
+ {
178
+ id: 'plugin4',
179
+ defaultOpen: true,
180
+ },
181
+ {
182
+ id: 'plugin5',
183
+ defaultOpen: true,
184
+ },
185
+ ]
186
+
187
+ const result = getDefaultActivePlugins(plugins)
188
+ // Should only activate the first 3, plugin4 and plugin5 should be ignored
189
+ expect(result).toEqual(['plugin1', 'plugin2', 'plugin3'])
190
+ expect(result.length).toBe(3)
191
+ expect(result).not.toContain('plugin4')
192
+ expect(result).not.toContain('plugin5')
193
+ })
194
+ })