@electerm/electerm-react 2.3.198 → 2.4.18

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 (72) hide show
  1. package/client/common/clipboard.js +1 -1
  2. package/client/common/constants.js +2 -2
  3. package/client/common/download.jsx +3 -2
  4. package/client/common/error-handler.jsx +5 -9
  5. package/client/common/fetch-from-server.js +1 -1
  6. package/client/common/fetch.jsx +5 -5
  7. package/client/common/icon-helpers.jsx +16 -0
  8. package/client/common/parse-json-safe.js +1 -1
  9. package/client/common/pre.js +0 -7
  10. package/client/common/sftp.js +1 -1
  11. package/client/common/terminal-theme.js +1 -1
  12. package/client/common/transfer.js +2 -2
  13. package/client/common/upgrade.js +2 -2
  14. package/client/components/ai/ai-chat.jsx +10 -1
  15. package/client/components/auth/login.jsx +1 -1
  16. package/client/components/bg/css-overwrite.jsx +1 -1
  17. package/client/components/bookmark-form/form-renderer.jsx +3 -2
  18. package/client/components/common/input-auto-focus.jsx +1 -1
  19. package/client/components/common/message.jsx +156 -0
  20. package/client/components/common/message.styl +56 -0
  21. package/client/components/common/modal.jsx +176 -0
  22. package/client/components/common/modal.styl +22 -0
  23. package/client/components/common/notification-with-details.jsx +1 -1
  24. package/client/components/common/notification.jsx +118 -0
  25. package/client/components/common/notification.styl +51 -0
  26. package/client/components/main/connection-hopping-warnning.jsx +1 -3
  27. package/client/components/main/error-wrapper.jsx +3 -2
  28. package/client/components/main/main.jsx +4 -11
  29. package/client/components/main/upgrade.jsx +6 -4
  30. package/client/components/profile/profile-form-elem.jsx +1 -1
  31. package/client/components/quick-commands/quick-commands-box.jsx +5 -2
  32. package/client/components/quick-commands/quick-commands-form-elem.jsx +1 -1
  33. package/client/components/rdp/rdp-session.jsx +2 -2
  34. package/client/components/session/session.jsx +4 -9
  35. package/client/components/setting-panel/deep-link-control.jsx +2 -1
  36. package/client/components/setting-panel/keyword-input.jsx +60 -0
  37. package/client/components/setting-panel/keywords-form.jsx +2 -7
  38. package/client/components/setting-panel/setting-common.jsx +1 -1
  39. package/client/components/setting-panel/setting-terminal.jsx +1 -1
  40. package/client/components/setting-panel/tab-settings.jsx +1 -1
  41. package/client/components/setting-sync/setting-sync-form.jsx +53 -3
  42. package/client/components/setting-sync/setting-sync.jsx +2 -1
  43. package/client/components/sftp/owner-list.js +6 -6
  44. package/client/components/sftp/sftp-entry.jsx +6 -4
  45. package/client/components/shortcuts/shortcut-editor.jsx +2 -2
  46. package/client/components/ssh-config/ssh-config-load-notify.jsx +3 -2
  47. package/client/components/tabs/tab.jsx +1 -1
  48. package/client/components/tabs/workspace-save-modal.jsx +2 -1
  49. package/client/components/terminal/attach-addon-custom.js +142 -26
  50. package/client/components/terminal/command-tracker-addon.js +164 -53
  51. package/client/components/terminal/highlight-addon.js +84 -43
  52. package/client/components/terminal/shell.js +164 -0
  53. package/client/components/terminal/terminal-command-dropdown.jsx +3 -0
  54. package/client/components/terminal/terminal.jsx +276 -118
  55. package/client/components/theme/theme-form.jsx +2 -1
  56. package/client/components/tree-list/bookmark-transport.jsx +27 -5
  57. package/client/components/vnc/vnc-session.jsx +1 -1
  58. package/client/components/widgets/widget-notification-with-details.jsx +1 -1
  59. package/client/store/common.js +5 -2
  60. package/client/store/db-upgrade.js +1 -1
  61. package/client/store/init-state.js +2 -1
  62. package/client/store/load-data.js +2 -2
  63. package/client/store/mcp-handler.js +9 -50
  64. package/client/store/setting.js +1 -3
  65. package/client/store/store.js +2 -1
  66. package/client/store/sync.js +14 -8
  67. package/client/store/system-menu.js +2 -1
  68. package/client/store/tab.js +1 -1
  69. package/client/store/widgets.js +1 -3
  70. package/package.json +1 -1
  71. package/client/common/track.js +0 -7
  72. package/client/components/batch-op/batch-op-entry.jsx +0 -13
@@ -5,6 +5,8 @@
5
5
 
6
6
  import { CloseOutlined } from '@ant-design/icons'
7
7
  import classnames from 'classnames'
8
+ import React, { useEffect } from 'react'
9
+ import { createRoot } from 'react-dom/client'
8
10
  import './modal.styl'
9
11
 
10
12
  export default function Modal (props) {
@@ -51,6 +53,36 @@ export default function Modal (props) {
51
53
  className
52
54
  )
53
55
 
56
+ useEffect(() => {
57
+ if (!open) return
58
+
59
+ const handleKeyDown = (e) => {
60
+ const isConfirm = !!document.querySelector('.custom-modal-cancel-btn')
61
+ if (e.key === 'Escape') {
62
+ if (onCancel) {
63
+ onCancel()
64
+ e.preventDefault()
65
+ }
66
+ } else if ((e.key === 'Enter' || e.key === ' ') && !isConfirm) {
67
+ // For info, Enter/Space closes
68
+ if (onCancel) {
69
+ onCancel()
70
+ e.preventDefault()
71
+ }
72
+ } else if ((e.key === 'Enter' || e.key === ' ') && isConfirm) {
73
+ // For confirm, Enter/Space confirms
74
+ const okBtn = document.querySelector('.custom-modal-ok-btn')
75
+ if (okBtn) {
76
+ okBtn.click()
77
+ e.preventDefault()
78
+ }
79
+ }
80
+ }
81
+
82
+ document.addEventListener('keydown', handleKeyDown)
83
+ return () => document.removeEventListener('keydown', handleKeyDown)
84
+ }, [open, onCancel])
85
+
54
86
  return (
55
87
  <div className={cls} style={modalStyle}>
56
88
  <div
@@ -87,3 +119,147 @@ export default function Modal (props) {
87
119
  </div>
88
120
  )
89
121
  }
122
+
123
+ function createModalInstance (type, options) {
124
+ const {
125
+ title,
126
+ content,
127
+ okText = 'OK',
128
+ cancelText = 'Cancel',
129
+ onOk,
130
+ onCancel,
131
+ ...rest
132
+ } = options
133
+
134
+ const container = document.createElement('div')
135
+ document.body.appendChild(container)
136
+
137
+ const root = createRoot(container)
138
+
139
+ const destroy = () => {
140
+ if (root && container && container.parentNode) {
141
+ root.unmount()
142
+ document.body.removeChild(container)
143
+ }
144
+ }
145
+
146
+ const handleOk = () => {
147
+ if (onOk) {
148
+ onOk()
149
+ }
150
+ destroy()
151
+ }
152
+
153
+ const handleCancel = () => {
154
+ if (onCancel) {
155
+ onCancel()
156
+ }
157
+ destroy()
158
+ }
159
+
160
+ const hasCancel = type === 'confirm'
161
+
162
+ const footer = (
163
+ <div className='custom-modal-footer-buttons'>
164
+ {hasCancel && (
165
+ <button
166
+ type='button'
167
+ className='custom-modal-cancel-btn'
168
+ onClick={handleCancel}
169
+ >
170
+ {cancelText}
171
+ </button>
172
+ )}
173
+ <button
174
+ type='button'
175
+ className='custom-modal-ok-btn'
176
+ onClick={handleOk}
177
+ >
178
+ {okText}
179
+ </button>
180
+ </div>
181
+ )
182
+
183
+ const modalProps = {
184
+ ...rest,
185
+ title,
186
+ open: true,
187
+ onCancel: hasCancel ? handleCancel : destroy,
188
+ footer,
189
+ children: content
190
+ }
191
+
192
+ root.render(<Modal {...modalProps} />)
193
+
194
+ const update = (newOptions) => {
195
+ const updatedOptions = { ...options, ...newOptions }
196
+ const {
197
+ title: newTitle,
198
+ content: newContent,
199
+ okText: newOkText = 'OK',
200
+ cancelText: newCancelText = 'Cancel',
201
+ onOk: newOnOk,
202
+ onCancel: newOnCancel,
203
+ ...newRest
204
+ } = updatedOptions
205
+
206
+ const newHandleOk = () => {
207
+ if (newOnOk) {
208
+ newOnOk()
209
+ }
210
+ destroy()
211
+ }
212
+
213
+ const newHandleCancel = () => {
214
+ if (newOnCancel) {
215
+ newOnCancel()
216
+ }
217
+ destroy()
218
+ }
219
+
220
+ const newFooter = (
221
+ <div className='custom-modal-footer-buttons'>
222
+ {hasCancel && (
223
+ <button
224
+ type='button'
225
+ className='custom-modal-cancel-btn'
226
+ onClick={newHandleCancel}
227
+ >
228
+ {newCancelText}
229
+ </button>
230
+ )}
231
+ <button
232
+ type='button'
233
+ className='custom-modal-ok-btn'
234
+ onClick={newHandleOk}
235
+ >
236
+ {newOkText}
237
+ </button>
238
+ </div>
239
+ )
240
+
241
+ const newModalProps = {
242
+ ...newRest,
243
+ title: newTitle,
244
+ open: true,
245
+ onCancel: hasCancel ? newHandleCancel : destroy,
246
+ footer: newFooter,
247
+ children: newContent
248
+ }
249
+
250
+ root.render(<Modal {...newModalProps} />)
251
+ }
252
+
253
+ return {
254
+ destroy,
255
+ update
256
+ }
257
+ }
258
+
259
+ Modal.info = (options) => {
260
+ return createModalInstance('info', options)
261
+ }
262
+
263
+ Modal.confirm = (options) => {
264
+ return createModalInstance('confirm', options)
265
+ }
@@ -75,3 +75,25 @@
75
75
  padding 12px 24px
76
76
  border-top 1px solid var(--main-darker)
77
77
  text-align right
78
+
79
+ .custom-modal-footer-buttons
80
+ display flex
81
+ justify-content flex-end
82
+ gap 8px
83
+
84
+ .custom-modal-ok-btn,
85
+ .custom-modal-cancel-btn
86
+ padding 6px 16px
87
+ border none
88
+ border-radius 4px
89
+ cursor pointer
90
+ font-size 14px
91
+ transition background-color 0.2s
92
+
93
+ .custom-modal-ok-btn
94
+ background var(--primary)
95
+ color var(--primary-contrast)
96
+
97
+ .custom-modal-cancel-btn
98
+ background var(--main)
99
+ color var(--text)
@@ -1,4 +1,4 @@
1
- import { notification } from 'antd'
1
+ import { notification } from './notification'
2
2
  import { CopyOutlined } from '@ant-design/icons'
3
3
  import { copy } from '../../common/clipboard'
4
4
 
@@ -0,0 +1,118 @@
1
+ import React, { useState, useEffect, useRef } from 'react'
2
+ import { CloseOutlined } from '@ant-design/icons'
3
+ import classnames from 'classnames'
4
+ import generateId from '../../common/uid'
5
+ import { messageIcons } from '../../common/icon-helpers.jsx'
6
+ import './notification.styl'
7
+
8
+ const notifications = []
9
+ const NOTIFICATION_EVENT = 'notification-update'
10
+
11
+ function addNotification (notif) {
12
+ notifications.push(notif)
13
+ window.dispatchEvent(new CustomEvent(NOTIFICATION_EVENT))
14
+ }
15
+
16
+ function removeNotification (key) {
17
+ const index = notifications.findIndex(n => n.key === key)
18
+ if (index > -1) {
19
+ notifications.splice(index, 1)
20
+ window.dispatchEvent(new CustomEvent(NOTIFICATION_EVENT))
21
+ }
22
+ }
23
+
24
+ export const notification = {
25
+ success: (options) => {
26
+ addNotification({ ...options, type: 'success', key: options.key || generateId() })
27
+ },
28
+ error: (options) => {
29
+ addNotification({ ...options, type: 'error', key: options.key || generateId() })
30
+ },
31
+ warning: (options) => {
32
+ addNotification({ ...options, type: 'warning', key: options.key || generateId() })
33
+ },
34
+ info: (options) => {
35
+ addNotification({ ...options, type: 'info', key: options.key || generateId() })
36
+ },
37
+ destroy: (key) => {
38
+ if (key) {
39
+ removeNotification(key)
40
+ } else {
41
+ notifications.length = 0
42
+ window.dispatchEvent(new CustomEvent(NOTIFICATION_EVENT))
43
+ }
44
+ }
45
+ }
46
+
47
+ export function NotificationContainer () {
48
+ const [nots, setNots] = useState([...notifications])
49
+
50
+ useEffect(() => {
51
+ const handler = () => setNots([...notifications])
52
+ window.addEventListener(NOTIFICATION_EVENT, handler)
53
+ return () => window.removeEventListener(NOTIFICATION_EVENT, handler)
54
+ }, [])
55
+
56
+ return (
57
+ <div className='notification-container'>
58
+ {nots.map(notif => (
59
+ <NotificationItem
60
+ key={notif.key}
61
+ message={notif.message}
62
+ description={notif.description}
63
+ type={notif.type}
64
+ duration={notif.duration}
65
+ onClose={() => removeNotification(notif.key)}
66
+ />
67
+ ))}
68
+ </div>
69
+ )
70
+ }
71
+
72
+ function NotificationItem ({ message, description, type, onClose, duration = 18.5 }) {
73
+ const timeoutRef = useRef(null)
74
+
75
+ useEffect(() => {
76
+ if (duration > 0) {
77
+ timeoutRef.current = setTimeout(onClose, duration * 1000)
78
+ }
79
+ return () => {
80
+ if (timeoutRef.current) {
81
+ clearTimeout(timeoutRef.current)
82
+ timeoutRef.current = null
83
+ }
84
+ }
85
+ }, [])
86
+
87
+ const handleMouseEnter = () => {
88
+ if (timeoutRef.current) {
89
+ clearTimeout(timeoutRef.current)
90
+ timeoutRef.current = null
91
+ }
92
+ }
93
+
94
+ const handleMouseLeave = () => {
95
+ if (duration > 0 && !timeoutRef.current) {
96
+ timeoutRef.current = setTimeout(onClose, duration * 1000)
97
+ }
98
+ }
99
+
100
+ const className = classnames('notification', type)
101
+
102
+ return (
103
+ <div
104
+ className={className}
105
+ onMouseEnter={handleMouseEnter}
106
+ onMouseLeave={handleMouseLeave}
107
+ >
108
+ <div className='notification-content'>
109
+ <div className='notification-message'>
110
+ <div className='notification-icon'>{messageIcons[type]}</div>
111
+ <div className='notification-title' title={message}>{message}</div>
112
+ </div>
113
+ {description && <div className='notification-description'>{description}</div>}
114
+ </div>
115
+ <CloseOutlined className='notification-close' onClick={onClose} />
116
+ </div>
117
+ )
118
+ }
@@ -0,0 +1,51 @@
1
+ .notification-container
2
+ position fixed
3
+ bottom 20px
4
+ right 20px
5
+ z-index 1000
6
+
7
+ .notification
8
+ background var(--main-lighter)
9
+ color var(--text)
10
+ border-radius 4px
11
+ margin-bottom 10px
12
+ padding 12px 16px
13
+ box-shadow 0 4px 12px rgba(0, 0, 0, 0.15)
14
+ width 400px
15
+ position relative
16
+
17
+ .notification-content
18
+ width 100%
19
+
20
+ .notification-message
21
+ font-weight bold
22
+ margin-bottom 10px
23
+ .notification-title
24
+ overflow hidden
25
+ text-overflow ellipsis
26
+ white-space nowrap
27
+ padding 0 30px 0 20px
28
+
29
+ .notification-icon
30
+ font-size 16px
31
+ position absolute
32
+ left 12px
33
+ top 10px
34
+
35
+ .notification-description
36
+ word-wrap break-all
37
+ max-height 200px
38
+ overflow auto
39
+
40
+ .notification-close
41
+ position absolute
42
+ top 12px
43
+ right 12px
44
+ background none
45
+ border none
46
+ color var(--text-dark)
47
+ font-size 18px
48
+ cursor pointer
49
+ padding 0
50
+ &:hover
51
+ color var(--text)
@@ -1,8 +1,6 @@
1
1
  import { useEffect } from 'react'
2
2
  import ConnectionHoppingWarningText from '../common/connection-hopping-warning-text'
3
- import {
4
- notification
5
- } from 'antd'
3
+ import { notification } from '../common/notification'
6
4
  import * as ls from '../../common/safe-local-storage'
7
5
  import {
8
6
  connectionHoppingWarnKey
@@ -1,6 +1,7 @@
1
1
  import React from 'react'
2
2
  import { FrownOutlined, ReloadOutlined } from '@ant-design/icons'
3
- import { Button, message } from 'antd'
3
+ import { Button } from 'antd'
4
+ import message from '../common/message'
4
5
  import {
5
6
  logoPath1,
6
7
  packInfo,
@@ -41,7 +42,7 @@ export default class ErrorBoundary extends React.PureComponent {
41
42
  }
42
43
 
43
44
  componentDidCatch (error) {
44
- log.error(error)
45
+ console.error(error)
45
46
  this.setState({
46
47
  hasError: true,
47
48
  error
@@ -6,7 +6,7 @@ import UpdateCheck from './upgrade'
6
6
  import SettingModal from '../setting-panel/setting-modal'
7
7
  import TextEditor from '../text-editor/text-editor'
8
8
  import Sidebar from '../sidebar'
9
- import BatchOp from '../batch-op/batch-op-entry'
9
+ import BatchOp from '../batch-op/batch-op'
10
10
  import CssOverwrite from '../bg/css-overwrite'
11
11
  import UiTheme from './ui-theme'
12
12
  import CustomCss from '../bg/custom-css.jsx'
@@ -21,7 +21,8 @@ import ShortcutControl from '../shortcuts/shortcut-control.jsx'
21
21
  import { isMac, isWin, textTerminalBgValue } from '../../common/constants'
22
22
  import TermFullscreenControl from './term-fullscreen-control'
23
23
  import TerminalInfo from '../terminal-info/terminal-info'
24
- import { ConfigProvider, notification, message } from 'antd'
24
+ import { ConfigProvider } from 'antd'
25
+ import { NotificationContainer } from '../common/notification'
25
26
  import InfoModal from '../sidebar/info-modal.jsx'
26
27
  import RightSidePanel from '../side-panel-r/side-panel-r'
27
28
  import ConnectionHoppingWarning from './connection-hopping-warnning'
@@ -37,19 +38,10 @@ import deepCopy from 'json-deep-copy'
37
38
  import './wrapper.styl'
38
39
 
39
40
  function setupGlobalMessageDismiss () {
40
- document.addEventListener('click', (event) => {
41
- const messageElement = event.target.closest('.ant-message-notice')
42
- if (messageElement) {
43
- message.destroy()
44
- }
45
- })
46
41
  }
47
42
 
48
43
  export default auto(function Index (props) {
49
44
  useEffect(() => {
50
- notification.config({
51
- placement: 'bottomRight'
52
- })
53
45
  setupGlobalMessageDismiss()
54
46
  const { store } = props
55
47
  window.addEventListener('resize', store.onResize)
@@ -307,6 +299,7 @@ export default auto(function Index (props) {
307
299
  <TerminalCmdSuggestions {...cmdSuggestionsProps} />
308
300
  <TransferQueue />
309
301
  <WorkspaceSaveModal store={store} />
302
+ <NotificationContainer />
310
303
  </div>
311
304
  </ConfigProvider>
312
305
  )
@@ -37,9 +37,6 @@ export default class Upgrade extends PureComponent {
37
37
  if (window.et.isWebApp) {
38
38
  return
39
39
  }
40
- setTimeout(() => {
41
- getLatestReleaseVersion(1)
42
- }, 5000)
43
40
  this.id = 'upgrade'
44
41
  refsStatic.add(this.id, this)
45
42
  }
@@ -119,7 +116,12 @@ export default class Upgrade extends PureComponent {
119
116
  return window.store.addTab(
120
117
  {
121
118
  ...newTerm(undefined, true),
122
- loginScript: 'npm i -g electerm'
119
+ runScripts: [
120
+ {
121
+ script: 'npm install -g electerm',
122
+ delay: 500
123
+ }
124
+ ]
123
125
  }
124
126
  )
125
127
  }
@@ -1,10 +1,10 @@
1
1
  import { useState } from 'react'
2
2
  import {
3
3
  Form,
4
- message,
5
4
  Switch,
6
5
  Button
7
6
  } from 'antd'
7
+ import message from '../common/message'
8
8
  import InputAutoFocus from '../common/input-auto-focus'
9
9
  import { formItemLayout } from '../../common/form-layout'
10
10
  import HelpIcon from '../common/help-icon'
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { useState, useRef } from 'react'
6
- import { quickCommandLabelsLsKey } from '../../common/constants'
6
+ import { quickCommandLabelsLsKey, pinnedQuickCommandBarKey } from '../../common/constants'
7
7
  import { sortBy } from 'lodash-es'
8
8
  import { Button, Input, Select, Space, Flex } from 'antd'
9
9
  import * as ls from '../../common/safe-local-storage'
@@ -41,7 +41,9 @@ export default function QuickCommandsFooterBox (props) {
41
41
  }
42
42
 
43
43
  function handleTogglePinned () {
44
- window.store.pinnedQuickCommandBar = !window.store.pinnedQuickCommandBar
44
+ const current = !window.store.pinnedQuickCommandBar
45
+ ls.setItem(pinnedQuickCommandBarKey, current ? 'y' : 'n')
46
+ window.store.pinnedQuickCommandBar = current
45
47
  }
46
48
 
47
49
  async function handleSelect (id) {
@@ -56,6 +58,7 @@ export default function QuickCommandsFooterBox (props) {
56
58
  }
57
59
 
58
60
  function handleClose () {
61
+ ls.setItem(pinnedQuickCommandBarKey, 'n')
59
62
  window.store.pinnedQuickCommandBar = false
60
63
  window.store.openQuickCommandBar = false
61
64
  }
@@ -2,10 +2,10 @@ import {
2
2
  Button,
3
3
  Switch,
4
4
  Form,
5
- message,
6
5
  Select,
7
6
  Input
8
7
  } from 'antd'
8
+ import message from '../common/message'
9
9
  import { useState } from 'react'
10
10
  import generate from '../../common/uid'
11
11
  import InputAutoFocus from '../common/input-auto-focus'
@@ -7,11 +7,11 @@ import {
7
7
  statusMap
8
8
  } from '../../common/constants'
9
9
  import {
10
- notification,
11
10
  Spin,
12
11
  // Button,
13
12
  Select
14
13
  } from 'antd'
14
+ import { notification } from '../common/notification'
15
15
  import {
16
16
  ReloadOutlined,
17
17
  EditOutlined
@@ -246,7 +246,7 @@ export default class RdpSession extends PureComponent {
246
246
 
247
247
  onerrorSocket = err => {
248
248
  this.setStatus(statusMap.error)
249
- log.error('socket error', err)
249
+ console.error('socket error', err)
250
250
  }
251
251
 
252
252
  closeMsg = () => {
@@ -17,7 +17,6 @@ import {
17
17
  } from '@ant-design/icons'
18
18
  import {
19
19
  Tooltip,
20
- message,
21
20
  Splitter
22
21
  } from 'antd'
23
22
  import { pick } from 'lodash-es'
@@ -206,13 +205,9 @@ export default class SessionWrapper extends Component {
206
205
  }
207
206
 
208
207
  toggleCheckSftpPathFollowSsh = () => {
209
- const nv = !this.state.sftpPathFollowSsh
210
- if (nv) {
211
- message.warning(e('sftpPathFollowSshTip'), 8)
212
- }
213
- this.setState({
214
- sftpPathFollowSsh: nv
215
- })
208
+ this.setState(prevState => ({
209
+ sftpPathFollowSsh: !prevState.sftpPathFollowSsh
210
+ }))
216
211
  }
217
212
 
218
213
  editTab = (up) => {
@@ -660,7 +655,7 @@ export default class SessionWrapper extends Component {
660
655
  const termType = tab?.type
661
656
  const isSsh = tab.authType
662
657
  const isLocal = !isSsh && (termType === connectionMap.local || !termType)
663
- const checkTxt = e('sftpPathFollowSsh') + ' [Beta]'
658
+ const checkTxt = e('sftpPathFollowSsh')
664
659
  const checkProps = {
665
660
  onClick: this.toggleCheckSftpPathFollowSsh,
666
661
  className: classnames(
@@ -1,5 +1,6 @@
1
1
  import React, { useState, useEffect } from 'react'
2
- import { Button, message, Tooltip, Tag, Space } from 'antd'
2
+ import { Button, Tooltip, Tag, Space } from 'antd'
3
+ import message from '../common/message'
3
4
  import { CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons'
4
5
 
5
6
  const e = window.translate
@@ -0,0 +1,60 @@
1
+ import { Input } from 'antd'
2
+ import { CheckOutlined, CloseOutlined } from '@ant-design/icons'
3
+ import { useState, useEffect } from 'react'
4
+
5
+ export default function KeywordInput (props) {
6
+ const { value, onChange, addonBefore, ...rest } = props
7
+ const [localValue, setLocalValue] = useState(value || '')
8
+ const [isEditing, setIsEditing] = useState(false)
9
+
10
+ useEffect(() => {
11
+ setLocalValue(value || '')
12
+ }, [value])
13
+
14
+ function handleChange (e) {
15
+ const newValue = e.target.value
16
+ setLocalValue(newValue)
17
+ setIsEditing(true)
18
+ }
19
+
20
+ function handleConfirm () {
21
+ onChange(localValue)
22
+ setIsEditing(false)
23
+ }
24
+
25
+ function handleCancel () {
26
+ setLocalValue(value || '')
27
+ setIsEditing(false)
28
+ }
29
+
30
+ function handleBlur () {
31
+ // Optionally hide icons on blur, but since user might click icons, maybe not
32
+ // setIsEditing(false)
33
+ }
34
+
35
+ const addonAfter = isEditing
36
+ ? (
37
+ <div>
38
+ <CheckOutlined
39
+ onClick={handleConfirm}
40
+ className='mg1r pointer'
41
+ />
42
+ <CloseOutlined
43
+ onClick={handleCancel}
44
+ className='pointer'
45
+ />
46
+ </div>
47
+ )
48
+ : null
49
+
50
+ return (
51
+ <Input
52
+ value={localValue}
53
+ onChange={handleChange}
54
+ onBlur={handleBlur}
55
+ addonBefore={addonBefore}
56
+ addonAfter={addonAfter}
57
+ {...rest}
58
+ />
59
+ )
60
+ }