@electerm/electerm-react 3.1.26 → 3.3.8

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 (61) hide show
  1. package/client/common/constants.js +1 -3
  2. package/client/common/db.js +4 -2
  3. package/client/components/ai/ai-history.jsx +4 -4
  4. package/client/components/batch-op/batch-op-alert.jsx +42 -0
  5. package/client/components/batch-op/batch-op-editor.jsx +202 -0
  6. package/client/components/batch-op/batch-op-logs.jsx +53 -0
  7. package/client/components/batch-op/batch-op-runner.jsx +315 -0
  8. package/client/components/bookmark-form/ai-bookmark-form.jsx +2 -1
  9. package/client/components/bookmark-form/bookmark-from-history-modal.jsx +2 -1
  10. package/client/components/bookmark-form/common/bookmark-select.jsx +18 -2
  11. package/client/components/bookmark-form/common/connection-hopping-form.jsx +153 -0
  12. package/client/components/bookmark-form/common/connection-hopping.jsx +136 -129
  13. package/client/components/common/auto-check-update.jsx +31 -0
  14. package/client/components/common/notification.styl +1 -1
  15. package/client/components/file-transfer/conflict-resolve.jsx +3 -0
  16. package/client/components/footer/batch-input.jsx +10 -7
  17. package/client/components/main/error-wrapper.jsx +18 -7
  18. package/client/components/main/main.jsx +6 -7
  19. package/client/components/quick-commands/qm.styl +0 -2
  20. package/client/components/quick-commands/quick-commands-list-form.jsx +1 -1
  21. package/client/components/setting-panel/hotkey.jsx +9 -1
  22. package/client/components/setting-panel/list.jsx +0 -1
  23. package/client/components/setting-panel/list.styl +4 -0
  24. package/client/components/setting-panel/setting-modal.jsx +53 -47
  25. package/client/components/setting-sync/auto-sync.jsx +53 -0
  26. package/client/components/setting-sync/data-import.jsx +69 -8
  27. package/client/components/sftp/address-bar.jsx +7 -1
  28. package/client/components/shortcuts/shortcut-editor.jsx +4 -2
  29. package/client/components/sidebar/bookmark-select.jsx +3 -2
  30. package/client/components/sidebar/history-item.jsx +3 -1
  31. package/client/components/sidebar/history.jsx +1 -0
  32. package/client/components/sidebar/index.jsx +0 -9
  33. package/client/components/tabs/add-btn-menu.jsx +1 -1
  34. package/client/components/tabs/add-btn.jsx +9 -15
  35. package/client/components/tabs/quick-connect.jsx +6 -10
  36. package/client/components/terminal/attach-addon-custom.js +86 -0
  37. package/client/components/terminal/cmd-item.jsx +13 -3
  38. package/client/components/terminal/drop-file-modal.jsx +57 -0
  39. package/client/components/terminal/terminal-command-dropdown.jsx +91 -13
  40. package/client/components/terminal/terminal.jsx +107 -10
  41. package/client/components/terminal/terminal.styl +9 -0
  42. package/client/components/tree-list/tree-list-item.jsx +0 -1
  43. package/client/components/tree-list/tree-list.jsx +115 -10
  44. package/client/components/tree-list/tree-list.styl +3 -0
  45. package/client/components/tree-list/tree-search.jsx +9 -1
  46. package/client/components/vnc/vnc-session.jsx +2 -0
  47. package/client/components/widgets/widget-control.jsx +3 -0
  48. package/client/components/widgets/widget-form.jsx +6 -0
  49. package/client/components/widgets/widget-instance.jsx +26 -7
  50. package/client/css/includes/box.styl +3 -0
  51. package/client/store/common.js +0 -28
  52. package/client/store/init-state.js +2 -1
  53. package/client/store/load-data.js +6 -4
  54. package/client/store/mcp-handler.js +20 -2
  55. package/client/store/sync.js +25 -1
  56. package/client/store/tab.js +1 -1
  57. package/client/store/watch.js +10 -18
  58. package/client/store/widgets.js +54 -0
  59. package/client/views/index.pug +1 -2
  60. package/package.json +1 -1
  61. package/client/components/batch-op/batch-op.jsx +0 -694
@@ -3,7 +3,8 @@
3
3
  */
4
4
 
5
5
  import React from 'react'
6
- import { Button, message } from 'antd'
6
+ import { Button } from 'antd'
7
+ import message from '../common/message'
7
8
  import { PlusOutlined } from '@ant-design/icons'
8
9
  import Modal from '../common/modal'
9
10
  import { refsStatic } from '../common/ref'
@@ -4,6 +4,16 @@ import { TreeSelect } from 'antd'
4
4
 
5
5
  const e = window.translate
6
6
 
7
+ const hoppingProps = [
8
+ 'host',
9
+ 'port',
10
+ 'username',
11
+ 'password',
12
+ 'privateKey',
13
+ 'passphrase',
14
+ 'certificate'
15
+ ]
16
+
7
17
  function buildTreeData (bookmarkGroups, tree) {
8
18
  const cats = bookmarkGroups
9
19
  const btree = cats.reduce((p, k) => ({ ...p, [k.id]: k }), {})
@@ -53,8 +63,14 @@ export default function BookmarkSelect (props) {
53
63
  function onSelect (id) {
54
64
  const item = tree[id]
55
65
  if (item) {
56
- item.bookmarkId = item.id
57
- props.onSelect(item)
66
+ const selected = hoppingProps.reduce((p, k) => {
67
+ if (item[k] !== undefined) {
68
+ p[k] = item[k]
69
+ }
70
+ return p
71
+ }, {})
72
+ selected.bookmarkId = item.id
73
+ props.onSelect(selected)
58
74
  }
59
75
  }
60
76
 
@@ -0,0 +1,153 @@
1
+ import {
2
+ Form,
3
+ InputNumber,
4
+ Input,
5
+ Radio,
6
+ Button
7
+ } from 'antd'
8
+ import {
9
+ formItemLayout,
10
+ tailFormItemLayout
11
+ } from '../../../common/form-layout'
12
+ import {
13
+ PlusOutlined,
14
+ SaveOutlined
15
+ } from '@ant-design/icons'
16
+ import RenderAuth from './render-auth-ssh'
17
+ import {
18
+ authTypeMap
19
+ } from '../../../common/constants'
20
+ import { useState } from 'react'
21
+ import BookmarkSelect from './bookmark-select'
22
+
23
+ const FormItem = Form.Item
24
+ const RadioButton = Radio.Button
25
+ const RadioGroup = Radio.Group
26
+ const e = window.translate
27
+
28
+ export default function ConnectionHoppingForm (props) {
29
+ const {
30
+ store,
31
+ formChild,
32
+ initialValues,
33
+ onFinish,
34
+ authTypes: authTypesProp,
35
+ trim,
36
+ isEdit
37
+ } = props
38
+ const [authType, setAuthType] = useState(initialValues.authType || authTypeMap.password)
39
+
40
+ function onChangeAuthType (e) {
41
+ setAuthType(e.target.value)
42
+ }
43
+
44
+ function onSubmit () {
45
+ formChild.submit()
46
+ }
47
+
48
+ const authTypes = authTypesProp || Object.keys(authTypeMap).map(k => k)
49
+
50
+ const treeProps = {
51
+ bookmarks: store.bookmarks.filter(d => {
52
+ return d.host && d.port && d.username
53
+ }),
54
+ bookmarkGroups: store.bookmarkGroups,
55
+ onSelect: onFinish
56
+ }
57
+
58
+ return (
59
+ <Form
60
+ form={formChild}
61
+ onFinish={onFinish}
62
+ initialValues={initialValues}
63
+ component='div'
64
+ >
65
+ <FormItem
66
+ {...formItemLayout}
67
+ label={e('chooseFromBookmarks')}
68
+ className='mg60b'
69
+ style={{ display: isEdit ? 'none' : '' }}
70
+ >
71
+ <BookmarkSelect {...treeProps} />
72
+ </FormItem>
73
+ <FormItem
74
+ {...formItemLayout}
75
+ label={e('host')}
76
+ hasFeedback
77
+ rules={[{
78
+ max: 520, message: '520 chars max'
79
+ }, {
80
+ required: true, message: 'host required'
81
+ }]}
82
+ normalize={trim}
83
+ name='host'
84
+ >
85
+ <Input />
86
+ </FormItem>
87
+ <FormItem
88
+ {...formItemLayout}
89
+ label={e('port')}
90
+ hasFeedback
91
+ name='port'
92
+ rules={[{
93
+ required: true, message: 'port required'
94
+ }]}
95
+ >
96
+ <InputNumber
97
+ placeholder={e('port')}
98
+ min={1}
99
+ max={65535}
100
+ step={1}
101
+ />
102
+ </FormItem>
103
+ <FormItem
104
+ {...formItemLayout}
105
+ label={e('username')}
106
+ hasFeedback
107
+ name='username'
108
+ rules={[{
109
+ max: 128, message: '128 chars max'
110
+ }]}
111
+ normalize={trim}
112
+ >
113
+ <Input />
114
+ </FormItem>
115
+ <FormItem
116
+ {...tailFormItemLayout}
117
+ className='mg1b'
118
+ name='authType'
119
+ >
120
+ <RadioGroup
121
+ size='small'
122
+ onChange={onChangeAuthType}
123
+ buttonStyle='solid'
124
+ >
125
+ {
126
+ authTypes.map(t => {
127
+ return (
128
+ <RadioButton value={t} key={t}>
129
+ {e(t)}
130
+ </RadioButton>
131
+ )
132
+ })
133
+ }
134
+ </RadioGroup>
135
+ </FormItem>
136
+ <RenderAuth
137
+ form={formChild}
138
+ store={store}
139
+ authType={authType}
140
+ />
141
+ <FormItem {...tailFormItemLayout} className='mg60b'>
142
+ <Button
143
+ type='default'
144
+ htmlType='button'
145
+ icon={isEdit ? <SaveOutlined /> : <PlusOutlined />}
146
+ onClick={onSubmit}
147
+ >
148
+ {isEdit ? e('save') : e('connectionHopping')}
149
+ </Button>
150
+ </FormItem>
151
+ </Form>
152
+ )
153
+ }
@@ -1,33 +1,28 @@
1
1
  import {
2
2
  Form,
3
- InputNumber,
4
3
  Input,
5
- Radio,
6
- Button,
7
4
  Table
8
5
  } from 'antd'
9
6
  import {
10
- formItemLayout,
11
7
  tailFormItemLayout
12
8
  } from '../../../common/form-layout'
13
9
  import {
14
10
  MinusCircleFilled,
15
- PlusOutlined
11
+ EditOutlined,
12
+ HolderOutlined
16
13
  } from '@ant-design/icons'
17
- import RenderAuth from './render-auth-ssh'
18
14
  import uid from '../../../common/uid'
19
15
  import {
20
16
  authTypeMap,
21
17
  connectionHoppingWarnKey
22
18
  } from '../../../common/constants'
23
- import { useState } from 'react'
19
+ import { useState, useRef, useCallback } from 'react'
24
20
  import ConnectionHoppingWarningText from '../../common/connection-hopping-warning-text'
25
- import BookmarkSelect from './bookmark-select'
26
21
  import * as ls from '../../../common/safe-local-storage'
22
+ import Modal from '../../common/modal'
23
+ import ConnectionHoppingForm from './connection-hopping-form'
27
24
 
28
25
  const FormItem = Form.Item
29
- const RadioButton = Radio.Button
30
- const RadioGroup = Radio.Group
31
26
  const e = window.translate
32
27
 
33
28
  export default function renderConnectionHopping (props) {
@@ -37,28 +32,54 @@ export default function renderConnectionHopping (props) {
37
32
  formData
38
33
  } = props
39
34
  const [formChild] = Form.useForm()
40
- const [initialValues, editState] = useState({
35
+ const [editFormChild] = Form.useForm()
36
+ const [initialValues] = useState({
41
37
  port: 22,
42
38
  authType: authTypeMap.password
43
39
  })
44
40
  const [showWarn, setShowWarn] = useState(
45
41
  window.store.hasOldConnectionHoppingBookmark && ls.getItem(connectionHoppingWarnKey) !== 'yes'
46
42
  )
43
+ const [editModalVisible, setEditModalVisible] = useState(false)
44
+ const [editingItem, setEditingItem] = useState(null)
45
+
47
46
  function closeWarn () {
48
47
  setShowWarn(false)
49
48
  }
50
49
  const [list, setList] = useState(formData.connectionHoppings || [])
51
- function onChangeAuthType (e) {
52
- editState(old => {
53
- return {
54
- ...old,
55
- authType: e.target.value
56
- }
50
+ const dragItem = useRef(null)
51
+ const dragOverItem = useRef(null)
52
+
53
+ const handleDragStart = useCallback((index) => {
54
+ dragItem.current = index
55
+ }, [])
56
+
57
+ const handleDragEnter = useCallback((index) => {
58
+ dragOverItem.current = index
59
+ }, [])
60
+
61
+ const handleDragEnd = useCallback(() => {
62
+ if (dragItem.current === null || dragOverItem.current === null) {
63
+ return
64
+ }
65
+ if (dragItem.current === dragOverItem.current) {
66
+ dragItem.current = null
67
+ dragOverItem.current = null
68
+ return
69
+ }
70
+ setList(old => {
71
+ const newList = [...old]
72
+ const [removed] = newList.splice(dragItem.current, 1)
73
+ newList.splice(dragOverItem.current, 0, removed)
74
+ form.setFieldsValue({
75
+ connectionHoppings: newList
76
+ })
77
+ dragItem.current = null
78
+ dragOverItem.current = null
79
+ return newList
57
80
  })
58
- }
59
- function onSubmit () {
60
- formChild.submit()
61
- }
81
+ }, [form])
82
+
62
83
  function handleFinish (data) {
63
84
  const nd = {
64
85
  ...data,
@@ -74,14 +95,11 @@ export default function renderConnectionHopping (props) {
74
95
  setList(old => {
75
96
  return [
76
97
  ...old,
77
- data
98
+ nd
78
99
  ]
79
100
  })
80
101
  formChild.resetFields()
81
102
  }
82
- const authTypes = props.authTypes || Object.keys(authTypeMap).map(k => {
83
- return k
84
- })
85
103
 
86
104
  function remove (id) {
87
105
  setList(old => {
@@ -93,7 +111,51 @@ export default function renderConnectionHopping (props) {
93
111
  })
94
112
  formChild.resetFields()
95
113
  }
114
+
115
+ function openEdit (record) {
116
+ setEditingItem(record)
117
+ setEditModalVisible(true)
118
+ setTimeout(() => {
119
+ editFormChild.setFieldsValue(record)
120
+ }, 100)
121
+ }
122
+
123
+ function handleEditFinish (data) {
124
+ const updatedItem = {
125
+ ...data,
126
+ id: editingItem.id
127
+ }
128
+ setList(old => {
129
+ return old.map(item => item.id === editingItem.id ? updatedItem : item)
130
+ })
131
+ const v = (form.getFieldValue('connectionHoppings') || []).map(
132
+ item => item.id === editingItem.id ? updatedItem : item
133
+ )
134
+ form.setFieldsValue({
135
+ connectionHoppings: v
136
+ })
137
+ setEditModalVisible(false)
138
+ setEditingItem(null)
139
+ editFormChild.resetFields()
140
+ }
141
+
142
+ function closeEditModal () {
143
+ setEditModalVisible(false)
144
+ setEditingItem(null)
145
+ editFormChild.resetFields()
146
+ }
147
+
96
148
  const cols = [
149
+ {
150
+ title: '',
151
+ key: 'drag',
152
+ width: 30,
153
+ render: () => (
154
+ <HolderOutlined
155
+ className='drag'
156
+ />
157
+ )
158
+ },
97
159
  {
98
160
  title: 'NO.',
99
161
  dataIndex: 'index',
@@ -110,15 +172,21 @@ export default function renderConnectionHopping (props) {
110
172
  return <span>{useProfile}{item.username}{pass}@{item.host}:{item.port}{pk}{ph}</span>
111
173
  }
112
174
  }, {
113
- title: e('del'),
175
+ title: e('op'),
114
176
  key: 'op',
115
177
  dataIndex: 'id',
116
- render: (id) => {
178
+ render: (id, record) => {
117
179
  return (
118
- <MinusCircleFilled
119
- className='pointer'
120
- onClick={() => remove(id)}
121
- />
180
+ <span>
181
+ <EditOutlined
182
+ className='pointer mg1r'
183
+ onClick={() => openEdit(record)}
184
+ />
185
+ <MinusCircleFilled
186
+ className='pointer'
187
+ onClick={() => remove(id)}
188
+ />
189
+ </span>
122
190
  )
123
191
  }
124
192
  }
@@ -140,6 +208,13 @@ export default function renderConnectionHopping (props) {
140
208
  className='mg3b'
141
209
  pagination={false}
142
210
  size='small'
211
+ onRow={(record, index) => ({
212
+ draggable: true,
213
+ onDragStart: () => handleDragStart(index),
214
+ onDragEnter: () => handleDragEnter(index),
215
+ onDragEnd: handleDragEnd,
216
+ onDragOver: (e) => e.preventDefault()
217
+ })}
143
218
  dataSource={list.map((d, i) => {
144
219
  return {
145
220
  ...d,
@@ -161,13 +236,15 @@ export default function renderConnectionHopping (props) {
161
236
  </FormItem>
162
237
  )
163
238
  }
164
- const treeProps = {
165
- bookmarks: store.bookmarks.filter(d => {
166
- return d.host && d.port && d.username
167
- }),
168
- bookmarkGroups: store.bookmarkGroups,
169
- onSelect: handleFinish
239
+
240
+ const editModalProps = {
241
+ open: editModalVisible,
242
+ onCancel: closeEditModal,
243
+ footer: null,
244
+ title: e('edit') + ' ' + e('connectionHopping'),
245
+ width: 600
170
246
  }
247
+
171
248
  return (
172
249
  <>
173
250
  <FormItem
@@ -176,100 +253,30 @@ export default function renderConnectionHopping (props) {
176
253
  >
177
254
  <Input />
178
255
  </FormItem>
179
- <Form
180
- form={formChild}
181
- onFinish={handleFinish}
256
+ {renderList()}
257
+ {renderWarn()}
258
+ <ConnectionHoppingForm
259
+ store={store}
260
+ formChild={formChild}
182
261
  initialValues={initialValues}
183
- component='div'
184
- >
185
- {renderList()}
186
- {renderWarn()}
187
- <FormItem
188
- {...formItemLayout}
189
- label={e('chooseFromBookmarks')}
190
- className='mg60b'
191
- >
192
- <BookmarkSelect {...treeProps} />
193
- </FormItem>
194
- <FormItem
195
- {...formItemLayout}
196
- label={e('host')}
197
- hasFeedback
198
- rules={[{
199
- max: 520, message: '520 chars max'
200
- }, {
201
- required: true, message: 'host required'
202
- }]}
203
- normalize={props.trim}
204
- name='host'
205
- >
206
- <Input />
207
- </FormItem>
208
- <FormItem
209
- {...formItemLayout}
210
- label={e('port')}
211
- hasFeedback
212
- name='port'
213
- rules={[{
214
- required: true, message: 'port required'
215
- }]}
216
- >
217
- <InputNumber
218
- placeholder={e('port')}
219
- min={1}
220
- max={65535}
221
- step={1}
262
+ onFinish={handleFinish}
263
+ authTypes={props.authTypes}
264
+ trim={props.trim}
265
+ />
266
+ {editModalVisible && (
267
+ <Modal {...editModalProps}>
268
+ <ConnectionHoppingForm
269
+ key={editingItem?.id}
270
+ store={store}
271
+ formChild={editFormChild}
272
+ initialValues={editingItem}
273
+ onFinish={handleEditFinish}
274
+ authTypes={props.authTypes}
275
+ trim={props.trim}
276
+ isEdit
222
277
  />
223
- </FormItem>
224
- <FormItem
225
- {...formItemLayout}
226
- label={e('username')}
227
- hasFeedback
228
- name='username'
229
- rules={[{
230
- max: 128, message: '128 chars max'
231
- }]}
232
- normalize={props.trim}
233
- >
234
- <Input />
235
- </FormItem>
236
- <FormItem
237
- {...tailFormItemLayout}
238
- className='mg1b'
239
- name='authType'
240
- >
241
- <RadioGroup
242
- size='small'
243
- onChange={onChangeAuthType}
244
- buttonStyle='solid'
245
- >
246
- {
247
- authTypes.map(t => {
248
- return (
249
- <RadioButton value={t} key={t}>
250
- {e(t)}
251
- </RadioButton>
252
- )
253
- })
254
- }
255
- </RadioGroup>
256
- </FormItem>
257
- <RenderAuth
258
- form={formChild}
259
- store={store}
260
- authType={initialValues.authType}
261
- />
262
- <FormItem {...tailFormItemLayout} className='mg60b'>
263
- <Button
264
- type='default'
265
- htmlType='button'
266
- icon={<PlusOutlined />}
267
- onClick={onSubmit}
268
- >
269
- {e('connectionHopping')}
270
- </Button>
271
- </FormItem>
272
- </Form>
278
+ </Modal>
279
+ )}
273
280
  </>
274
281
  )
275
282
  }
@@ -0,0 +1,31 @@
1
+ import { useEffect, useRef } from 'react'
2
+
3
+ export default function AutoCheckUpdate ({ config }) {
4
+ const lastCheckTimeRef = useRef(0)
5
+ const intervalIdRef = useRef(null)
6
+
7
+ useEffect(() => {
8
+ if (!config.checkUpdateOnStart) {
9
+ clearInterval(intervalIdRef.current)
10
+ return
11
+ }
12
+
13
+ const checkForUpdate = () => {
14
+ const { store } = window
15
+ if (store.config.checkUpdateOnStart) {
16
+ store.onCheckUpdate(false)
17
+ }
18
+ lastCheckTimeRef.current = Date.now()
19
+ }
20
+
21
+ intervalIdRef.current = setInterval(checkForUpdate, 60 * 60 * 1000)
22
+
23
+ return () => {
24
+ if (intervalIdRef.current) {
25
+ clearInterval(intervalIdRef.current)
26
+ }
27
+ }
28
+ }, [config.checkUpdateOnStart])
29
+
30
+ return null
31
+ }
@@ -2,7 +2,7 @@
2
2
  position fixed
3
3
  bottom 20px
4
4
  right 20px
5
- z-index 1000
5
+ z-index 9999
6
6
 
7
7
  .notification
8
8
  background var(--main-lighter)
@@ -21,6 +21,9 @@ function formatTimeAuto (strOrDigit) {
21
21
  if (isString(strOrDigit)) {
22
22
  return formatTime(strOrDigit)
23
23
  }
24
+ if (strOrDigit > 9999999999) {
25
+ return formatTime(strOrDigit)
26
+ }
24
27
  return formatTime(strOrDigit * 1000)
25
28
  }
26
29
 
@@ -136,12 +136,10 @@ export default class BatchInput extends Component {
136
136
  } = this.props
137
137
  const opts = {
138
138
  options: this.buildOptions(),
139
- placeholder: e('batchInput'),
140
139
  value: cmd,
141
140
  onChange: this.handleChange,
142
141
  defaultOpen: false,
143
142
  open,
144
- allowClear: true,
145
143
  className: 'batch-input-wrap'
146
144
  }
147
145
  const cls = classNames(
@@ -155,6 +153,15 @@ export default class BatchInput extends Component {
155
153
  placeholder: e('batchInput'),
156
154
  className: 'batch-input-holder'
157
155
  }
156
+ const textAreaProps = {
157
+ onPressEnter: this.handleEnter,
158
+ onClick: this.handleClick,
159
+ onBlur: this.handleBlur,
160
+ size: 'small',
161
+ autoSize: { minRows: 1 },
162
+ placeholder: e('batchInput'),
163
+ allowClear: true
164
+ }
158
165
  const tabSelectProps = {
159
166
  activeTabId: this.props.activeTabId,
160
167
  tabs: this.getTabs(),
@@ -179,11 +186,7 @@ export default class BatchInput extends Component {
179
186
  {...opts}
180
187
  >
181
188
  <Input.TextArea
182
- onPressEnter={this.handleEnter}
183
- onClick={this.handleClick}
184
- onBlur={this.handleBlur}
185
- size='small'
186
- autoSize={{ minRows: 1 }}
189
+ {...textAreaProps}
187
190
  />
188
191
  </AutoComplete>
189
192
  <TabSelect {...tabSelectProps} />
@@ -77,12 +77,9 @@ export default class ErrorBoundary extends React.PureComponent {
77
77
  }
78
78
 
79
79
  renderTroubleShoot = () => {
80
- const {
81
- bugs: {
82
- url: bugReportLink
83
- }
84
- } = packInfo
85
- const bugUrl = `${bugReportLink}/new/choose`
80
+ if (window.et.isWebApp) {
81
+ return this.renderContacts()
82
+ }
86
83
  return (
87
84
  <div className='pd1y wordbreak'>
88
85
  <h2>{e('troubleShoot')}</h2>
@@ -99,6 +96,20 @@ export default class ErrorBoundary extends React.PureComponent {
99
96
  )
100
97
  })
101
98
  }
99
+ {this.renderContacts()}
100
+ </div>
101
+ )
102
+ }
103
+
104
+ renderContacts () {
105
+ const {
106
+ bugs: {
107
+ url: bugReportLink
108
+ }
109
+ } = packInfo
110
+ const bugUrl = `${bugReportLink}/new/choose`
111
+ return (
112
+ <>
102
113
  <div className='pd1b'>
103
114
  <Link to={bugUrl}>{e('bugReport')}</Link>
104
115
  </div>
@@ -112,7 +123,7 @@ export default class ErrorBoundary extends React.PureComponent {
112
123
  className='mwm-100'
113
124
  />
114
125
  </div>
115
- </div>
126
+ </>
116
127
  )
117
128
  }
118
129