@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
@@ -19,6 +19,10 @@ export default class AttachAddonCustom {
19
19
  this._lastInputTime = Date.now()
20
20
  this._keepaliveTimer = null
21
21
  this._keepaliveInterval = 3000
22
+ this._lastOutputLine = ''
23
+ this._passwordPromptDetected = false
24
+ this._pendingEchoCheck = null
25
+ this._echoCheckTimer = null
22
26
  }
23
27
 
24
28
  _initBase = async () => {
@@ -101,6 +105,52 @@ export default class AttachAddonCustom {
101
105
  this.writeToTerminal(ev.data)
102
106
  }
103
107
 
108
+ static passwordPromptPatterns = [
109
+ /password\s*[:\]>]\s*$/i,
110
+ /\[sudo\]\s*password\s+for\s+\S+\s*:\s*$/i,
111
+ /enter\s+passphrase/i,
112
+ /enter\s+password/i,
113
+ /密码[::]\s*$/,
114
+ /パスワード[::]\s*$/,
115
+ /mot de passe\s*[:\]]\s*$/i,
116
+ /passwort[:\]]\s*$/i,
117
+ /contraseña[:\]]\s*$/i
118
+ ]
119
+
120
+ _checkPasswordPrompt = (str) => {
121
+ // Extract last non-empty line from the output
122
+ const lines = str.split(/\r?\n|\r/)
123
+ for (let i = lines.length - 1; i >= 0; i--) {
124
+ const line = lines[i].trim()
125
+ if (line) {
126
+ this._lastOutputLine = line
127
+ break
128
+ }
129
+ }
130
+ return AttachAddonCustom.passwordPromptPatterns.some(
131
+ p => p.test(this._lastOutputLine)
132
+ )
133
+ }
134
+
135
+ _onEchoCheckTimeout = () => {
136
+ // No echo received within timeout → confirms password mode
137
+ this._pendingEchoCheck = null
138
+ }
139
+
140
+ _handleEchoDetection = (str) => {
141
+ if (this._pendingEchoCheck) {
142
+ // Server sent data back while we were waiting → echo is ON → not password
143
+ if (str.includes(this._pendingEchoCheck.char)) {
144
+ this._passwordPromptDetected = false
145
+ clearTimeout(this._echoCheckTimer)
146
+ this._pendingEchoCheck = null
147
+ this._echoCheckTimer = null
148
+ // Cancel the password dropdown if it was shown
149
+ this.term?.parent?.onPasswordPromptCancelled?.()
150
+ }
151
+ }
152
+ }
153
+
104
154
  checkForShellIntegration = (str) => {
105
155
  const ESC = String.fromCharCode(27)
106
156
  return str.includes(ESC + ']633;')
@@ -152,6 +202,26 @@ export default class AttachAddonCustom {
152
202
  return
153
203
  }
154
204
 
205
+ // Password prompt detection on output
206
+ let str = data
207
+ if (typeof data !== 'string') {
208
+ try {
209
+ str = this.decoder.decode(
210
+ data instanceof ArrayBuffer ? data : new Uint8Array(data)
211
+ )
212
+ } catch (e) {
213
+ str = ''
214
+ }
215
+ }
216
+ this._handleEchoDetection(str)
217
+ if (this._checkPasswordPrompt(str) && !this._passwordPromptDetected) {
218
+ this._passwordPromptDetected = true
219
+ // Show password dropdown immediately after terminal renders the prompt
220
+ setTimeout(() => {
221
+ this.term?.parent?.onPasswordPromptDetected?.()
222
+ }, 100)
223
+ }
224
+
155
225
  if (typeof data === 'string') {
156
226
  return term.write(data)
157
227
  }
@@ -171,6 +241,20 @@ export default class AttachAddonCustom {
171
241
 
172
242
  sendToServer = (data) => {
173
243
  this._lastInputTime = Date.now()
244
+ // Start echo detection when password prompt is suspected
245
+ if (this._passwordPromptDetected && !this._pendingEchoCheck && data !== '\r' && data !== '\n') {
246
+ this._pendingEchoCheck = { char: data, time: Date.now() }
247
+ clearTimeout(this._echoCheckTimer)
248
+ this._echoCheckTimer = setTimeout(this._onEchoCheckTimeout, 200)
249
+ }
250
+ // Reset password state on Enter
251
+ if (data === '\r' || data === '\n') {
252
+ this._passwordPromptDetected = false
253
+ this._lastOutputLine = ''
254
+ this._pendingEchoCheck = null
255
+ clearTimeout(this._echoCheckTimer)
256
+ this._echoCheckTimer = null
257
+ }
174
258
  this._sendData(data)
175
259
  }
176
260
 
@@ -228,6 +312,8 @@ export default class AttachAddonCustom {
228
312
 
229
313
  dispose = () => {
230
314
  this._stopKeepalive()
315
+ clearTimeout(this._echoCheckTimer)
316
+ this._echoCheckTimer = null
231
317
  this.term = null
232
318
  this._disposables.forEach(d => d.dispose())
233
319
  this._disposables.length = 0
@@ -11,11 +11,21 @@ const SuggestionItem = ({ item, onSelect, onDelete }) => {
11
11
  onDelete(item)
12
12
  }
13
13
 
14
+ const isPassword = item.type === 'PW'
15
+ const displayText = isPassword
16
+ ? '••••••••'
17
+ : item.command
18
+
14
19
  return (
15
- <div className='suggestion-item'>
16
- <span className='suggestion-command' onClick={handleClick}>
17
- {item.command}
20
+ <div className='suggestion-item' onClick={handleClick}>
21
+ <span className='suggestion-command'>
22
+ {displayText}
18
23
  </span>
24
+ {item.hint && (
25
+ <span className='suggestion-hint'>
26
+ {item.hint}
27
+ </span>
28
+ )}
19
29
  <span className='suggestion-type'>
20
30
  {item.type}
21
31
  </span>
@@ -0,0 +1,57 @@
1
+ import { Component } from 'react'
2
+ import Modal from '../common/modal'
3
+
4
+ const e = window.translate
5
+
6
+ export class DropFileModal extends Component {
7
+ render () {
8
+ const {
9
+ visible,
10
+ files,
11
+ onSelect,
12
+ onCancel
13
+ } = this.props
14
+
15
+ if (!visible) {
16
+ return null
17
+ }
18
+
19
+ return (
20
+ <Modal
21
+ title='?'
22
+ open={visible}
23
+ onCancel={onCancel}
24
+ footer={
25
+ <div className='custom-modal-footer-buttons'>
26
+ <button
27
+ type='button'
28
+ className='custom-modal-ok-btn'
29
+ onClick={() => onSelect('trzUpload')}
30
+ >
31
+ trz
32
+ </button>
33
+ <button
34
+ type='button'
35
+ className='custom-modal-cancel-btn'
36
+ onClick={() => onSelect('rzUpload')}
37
+ >
38
+ rz
39
+ </button>
40
+ <button
41
+ type='button'
42
+ className='custom-modal-cancel-btn'
43
+ onClick={() => onSelect('inputPath')}
44
+ >
45
+ {e('inputOnly')}
46
+ </button>
47
+ </div>
48
+ }
49
+ width={400}
50
+ >
51
+ <p>{files?.map(f => f.path).join(', ')}</p>
52
+ </Modal>
53
+ )
54
+ }
55
+ }
56
+
57
+ export default DropFileModal
@@ -16,7 +16,8 @@ export default class TerminalCmdSuggestions extends Component {
16
16
  aiSuggestions: [],
17
17
  cmdIsDescription: false,
18
18
  reverse: false,
19
- cmd: ''
19
+ cmd: '',
20
+ passwordMode: false
20
21
  }
21
22
 
22
23
  componentDidMount () {
@@ -95,6 +96,9 @@ export default class TerminalCmdSuggestions extends Component {
95
96
  }
96
97
 
97
98
  openSuggestions = (cursorPosition, cmd) => {
99
+ if (this.state.passwordMode) {
100
+ return
101
+ }
98
102
  if (!this.state.showSuggestions) {
99
103
  document.addEventListener('click', this.handleClickOutside)
100
104
  document.addEventListener('keydown', this.handleKeyDown)
@@ -128,7 +132,45 @@ export default class TerminalCmdSuggestions extends Component {
128
132
  showSuggestions: true,
129
133
  cursorPosition: position,
130
134
  cmd,
131
- reverse
135
+ reverse,
136
+ passwordMode: false
137
+ })
138
+ }
139
+
140
+ openPasswordSuggestions = (cursorPosition) => {
141
+ if (!this.state.showSuggestions) {
142
+ document.addEventListener('click', this.handleClickOutside)
143
+ document.addEventListener('keydown', this.handleKeyDown)
144
+ }
145
+
146
+ const {
147
+ left,
148
+ top,
149
+ cellHeight
150
+ } = cursorPosition
151
+ const w = window.innerWidth
152
+ const h = window.innerHeight
153
+
154
+ const position = {}
155
+ const reverse = top > h / 2
156
+
157
+ if (left > w / 2) {
158
+ position.right = w - left
159
+ } else {
160
+ position.left = left
161
+ }
162
+
163
+ if (reverse) {
164
+ position.bottom = h - top + cellHeight * 1.5
165
+ } else {
166
+ position.top = top + cellHeight
167
+ }
168
+ this.setState({
169
+ showSuggestions: true,
170
+ cursorPosition: position,
171
+ cmd: '',
172
+ reverse,
173
+ passwordMode: true
132
174
  })
133
175
  }
134
176
 
@@ -146,7 +188,8 @@ export default class TerminalCmdSuggestions extends Component {
146
188
  }
147
189
  this.setState({
148
190
  showSuggestions: false,
149
- aiSuggestions: []
191
+ aiSuggestions: [],
192
+ passwordMode: false
150
193
  })
151
194
  }
152
195
 
@@ -172,18 +215,33 @@ export default class TerminalCmdSuggestions extends Component {
172
215
  const terminal = refs.get('term-' + activeTabId)
173
216
  if (!terminal) {
174
217
  console.log('No active terminal found')
218
+ this.closeSuggestions()
219
+ return
220
+ }
221
+
222
+ if (item.type === 'PW') {
223
+ try {
224
+ // Send password + Enter directly, no backspace needed
225
+ terminal.attachAddon._sendData(item.command + '\r')
226
+ terminal.attachAddon._passwordPromptDetected = false
227
+ terminal.attachAddon._lastOutputLine = ''
228
+ } catch (e) {
229
+ console.error('Failed to send password:', e)
230
+ }
231
+ terminal.term.focus()
232
+ this.closeSuggestions()
175
233
  return
176
234
  }
177
235
 
178
- // const titleElement = domEvent.target.closest('.ant-menu-title-content')
179
- // const command = titleElement?.firstChild?.textContent
180
236
  const { command } = item
181
- const { cmd } = this.state
237
+ // Read current input from buffer directly to avoid stale state
238
+ // (onData fires before echo, so this.state.cmd may lag behind)
239
+ const currentInput = terminal.getCurrentInput() || ''
182
240
  let txt = ''
183
- if (cmd && command.startsWith(cmd)) {
184
- txt = command.slice(cmd.length)
241
+ if (currentInput && command.startsWith(currentInput)) {
242
+ txt = command.slice(currentInput.length)
185
243
  } else {
186
- const pre = '\b'.repeat(cmd.length)
244
+ const pre = '\b'.repeat(currentInput.length)
187
245
  txt = pre + command
188
246
  }
189
247
  terminal.attachAddon._sendData(txt)
@@ -209,6 +267,24 @@ export default class TerminalCmdSuggestions extends Component {
209
267
  })
210
268
  }
211
269
 
270
+ getPasswordSuggestions = () => {
271
+ const bookmarks = window.store.bookmarks || []
272
+ const seen = new Set()
273
+ const res = []
274
+ for (const b of bookmarks) {
275
+ if (b.password && !seen.has(b.password)) {
276
+ seen.add(b.password)
277
+ res.push({
278
+ id: uid(),
279
+ command: b.password,
280
+ type: 'PW',
281
+ hint: [b.username, b.host].filter(Boolean).join('@')
282
+ })
283
+ }
284
+ }
285
+ return this.state.reverse ? res.reverse() : res
286
+ }
287
+
212
288
  getSuggestions = () => {
213
289
  const uniqueCommands = new Set()
214
290
  const {
@@ -274,17 +350,19 @@ export default class TerminalCmdSuggestions extends Component {
274
350
  }
275
351
 
276
352
  render () {
277
- const { showSuggestions, cursorPosition, reverse } = this.state
353
+ const { showSuggestions, cursorPosition, reverse, passwordMode } = this.state
278
354
  if (!showSuggestions) {
279
355
  return null
280
356
  }
281
- const suggestions = this.getSuggestions()
357
+ const suggestions = passwordMode
358
+ ? this.getPasswordSuggestions()
359
+ : this.getSuggestions()
282
360
  const cls = classnames('terminal-suggestions-wrap', {
283
361
  reverse
284
362
  })
285
363
  return (
286
364
  <div className={cls} style={cursorPosition}>
287
- {this.renderSticky('top')}
365
+ {!passwordMode && this.renderSticky('top')}
288
366
  <div className='terminal-suggestions-list'>
289
367
  {
290
368
  suggestions.map(item => {
@@ -299,7 +377,7 @@ export default class TerminalCmdSuggestions extends Component {
299
377
  })
300
378
  }
301
379
  </div>
302
- {this.renderSticky('bottom')}
380
+ {!passwordMode && this.renderSticky('bottom')}
303
381
  </div>
304
382
  )
305
383
  }
@@ -19,7 +19,8 @@ import {
19
19
  isWin,
20
20
  rendererTypes,
21
21
  isMac,
22
- isMacJs
22
+ isMacJs,
23
+ connectionMap
23
24
  } from '../../common/constants.js'
24
25
  import deepCopy from 'json-deep-copy'
25
26
  import { readClipboardAsync, readClipboard, copy } from '../../common/clipboard.js'
@@ -27,6 +28,7 @@ import AttachAddon from './attach-addon-custom.js'
27
28
  import getProxy from '../../common/get-proxy.js'
28
29
  import { ZmodemClient } from './zmodem-client.js'
29
30
  import { TrzszClient } from './trzsz-client.js'
31
+ import DropFileModal from './drop-file-modal.jsx'
30
32
  import keyControlPressed from '../../common/key-control-pressed.js'
31
33
  import NormalBuffer from './normal-buffer.jsx'
32
34
  import { createTerm, resizeTerm } from './terminal-apis.js'
@@ -75,7 +77,9 @@ class Term extends Component {
75
77
  searchResults: [],
76
78
  matchIndex: -1,
77
79
  totalLines: 0,
78
- reconnectCountdown: null
80
+ reconnectCountdown: null,
81
+ dropFileModalVisible: false,
82
+ droppedFiles: []
79
83
  }
80
84
  this.id = `term-${this.props.tab.id}`
81
85
  refs.add(this.id, this)
@@ -374,11 +378,10 @@ class Term extends Component {
374
378
  }
375
379
 
376
380
  runQuickCommand = (cmd, inputOnly = false) => {
377
- this.term && this.attachAddon._sendData(
378
- cmd +
379
- (inputOnly ? '' : '\r')
380
- )
381
- this.term.focus()
381
+ if (this.term && this.attachAddon) {
382
+ this.attachAddon._sendData(cmd + (inputOnly ? '' : '\r'))
383
+ this.term.focus()
384
+ }
382
385
  }
383
386
 
384
387
  cd = (p) => {
@@ -392,9 +395,9 @@ class Term extends Component {
392
395
  const dt = e.dataTransfer
393
396
  const fromFile = dt.getData('fromFile')
394
397
  const notSafeMsg = 'File name contains unsafe characters'
398
+ const isSshTerminal = this.props.tab.type === connectionMap.ssh
395
399
 
396
400
  if (fromFile) {
397
- // Handle SFTP file drop
398
401
  try {
399
402
  const fileData = JSON.parse(fromFile)
400
403
  const filePath = resolve(fileData.path, fileData.name)
@@ -402,6 +405,13 @@ class Term extends Component {
402
405
  message.error(notSafeMsg)
403
406
  return
404
407
  }
408
+ if (isSshTerminal) {
409
+ this.setState({
410
+ dropFileModalVisible: true,
411
+ droppedFiles: [{ path: filePath, isRemote: true }]
412
+ })
413
+ return
414
+ }
405
415
  this.attachAddon._sendData(`"${filePath}" `)
406
416
  return
407
417
  } catch (e) {
@@ -409,24 +419,78 @@ class Term extends Component {
409
419
  }
410
420
  }
411
421
 
412
- // Handle regular file drop
413
422
  const files = dt.files
414
423
  if (files && files.length) {
415
424
  const arr = Array.from(files)
416
425
  const filePaths = arr.map(f => getFilePath(f))
417
426
 
418
- // Check each file path individually
419
427
  const hasUnsafeFilename = filePaths.some(path => isUnsafeFilename(path))
420
428
  if (hasUnsafeFilename) {
421
429
  message.error(notSafeMsg)
422
430
  return
423
431
  }
424
432
 
433
+ if (isSshTerminal) {
434
+ this.setState({
435
+ dropFileModalVisible: true,
436
+ droppedFiles: filePaths.map(path => ({ path, isRemote: false }))
437
+ })
438
+ return
439
+ }
440
+
425
441
  const filesAll = filePaths.map(path => `"${path}"`).join(' ')
426
442
  this.attachAddon._sendData(filesAll)
427
443
  }
428
444
  }
429
445
 
446
+ handleDropFileModalCancel = () => {
447
+ this.setState({
448
+ dropFileModalVisible: false,
449
+ droppedFiles: []
450
+ })
451
+ }
452
+
453
+ handleDropFileAction = (action) => {
454
+ const { droppedFiles } = this.state
455
+ if (!droppedFiles || !droppedFiles.length) {
456
+ this.handleDropFileModalCancel()
457
+ return
458
+ }
459
+
460
+ const filePaths = droppedFiles.map(f => f.path)
461
+
462
+ switch (action) {
463
+ case 'trzUpload': {
464
+ if (this.trzszClient && this.trzszClient.isActive) {
465
+ message.warning('A transfer is already in progress')
466
+ this.handleDropFileModalCancel()
467
+ return
468
+ }
469
+ window._apiControlSelectFile = filePaths
470
+ this.attachAddon._sendData('trz\r')
471
+ break
472
+ }
473
+ case 'rzUpload': {
474
+ if (this.zmodemClient && this.zmodemClient.isActive) {
475
+ message.warning('A transfer is already in progress')
476
+ this.handleDropFileModalCancel()
477
+ return
478
+ }
479
+ window._apiControlSelectFile = filePaths
480
+ this.attachAddon._sendData('rz\r')
481
+ break
482
+ }
483
+ case 'inputPath':
484
+ default: {
485
+ const filesAll = filePaths.map(path => `"${path}"`).join(' ')
486
+ this.attachAddon._sendData(filesAll)
487
+ break
488
+ }
489
+ }
490
+
491
+ this.handleDropFileModalCancel()
492
+ }
493
+
430
494
  onSelection = () => {
431
495
  if (
432
496
  !this.props.config.copyWhenSelect ||
@@ -771,8 +835,35 @@ class Term extends Component {
771
835
  }
772
836
  }
773
837
 
838
+ onPasswordPromptDetected = () => {
839
+ if (!this.props.config.showCmdSuggestions) {
840
+ return
841
+ }
842
+ const cursorPos = this.getCursorPosition()
843
+ if (cursorPos) {
844
+ refsStatic
845
+ .get('terminal-suggestions')
846
+ ?.openPasswordSuggestions(cursorPos)
847
+ }
848
+ }
849
+
850
+ onPasswordPromptCancelled = () => {
851
+ const suggestions = refsStatic.get('terminal-suggestions')
852
+ if (suggestions?.state?.passwordMode) {
853
+ suggestions.closeSuggestions()
854
+ }
855
+ }
856
+
774
857
  onData = (d) => {
775
858
  this.handleInputEvent(d)
859
+ // Skip normal suggestion logic when in password mode
860
+ const suggestions = refsStatic.get('terminal-suggestions')
861
+ if (suggestions?.state?.passwordMode) {
862
+ if (d === '\r' || d === '\n') {
863
+ this.closeSuggestions()
864
+ }
865
+ return
866
+ }
776
867
  if (this.props.config.showCmdSuggestions) {
777
868
  const data = this.getCurrentInput()
778
869
  if (data && d !== '\r' && d !== '\n') {
@@ -1459,6 +1550,12 @@ class Term extends Component {
1459
1550
  countdown={this.state.reconnectCountdown}
1460
1551
  onCancel={this.handleCancelAutoReconnect}
1461
1552
  />
1553
+ <DropFileModal
1554
+ visible={this.state.dropFileModalVisible}
1555
+ files={this.state.droppedFiles}
1556
+ onSelect={this.handleDropFileAction}
1557
+ onCancel={this.handleDropFileModalCancel}
1558
+ />
1462
1559
  {spin}
1463
1560
  </div>
1464
1561
  </Dropdown>
@@ -127,6 +127,15 @@
127
127
  &:hover
128
128
  color var(--success)
129
129
 
130
+ .suggestion-hint
131
+ margin-left 5px
132
+ font-size 0.8em
133
+ color var(--text-light, #888)
134
+ white-space nowrap
135
+ overflow hidden
136
+ text-overflow ellipsis
137
+ max-width 150px
138
+
130
139
  .suggestion-delete
131
140
  margin-left 5px
132
141
  visibility hidden
@@ -21,7 +21,6 @@ import {
21
21
  } from '../../common/constants'
22
22
  import highlight from '../common/highlight'
23
23
  import uid from '../../common/uid'
24
- import './tree-list.styl'
25
24
 
26
25
  const e = window.translate
27
26