@electerm/electerm-react 3.1.26 → 3.2.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.
Files changed (29) hide show
  1. package/client/common/db.js +4 -2
  2. package/client/components/ai/ai-history.jsx +4 -4
  3. package/client/components/bookmark-form/common/bookmark-select.jsx +18 -2
  4. package/client/components/bookmark-form/common/connection-hopping-form.jsx +153 -0
  5. package/client/components/bookmark-form/common/connection-hopping.jsx +136 -129
  6. package/client/components/quick-commands/qm.styl +0 -2
  7. package/client/components/quick-commands/quick-commands-list-form.jsx +1 -1
  8. package/client/components/setting-panel/hotkey.jsx +9 -1
  9. package/client/components/setting-panel/list.jsx +0 -1
  10. package/client/components/setting-panel/list.styl +4 -0
  11. package/client/components/setting-panel/setting-modal.jsx +53 -47
  12. package/client/components/shortcuts/shortcut-editor.jsx +4 -2
  13. package/client/components/sidebar/history.jsx +1 -0
  14. package/client/components/terminal/attach-addon-custom.js +86 -0
  15. package/client/components/terminal/cmd-item.jsx +13 -3
  16. package/client/components/terminal/drop-file-modal.jsx +57 -0
  17. package/client/components/terminal/terminal-command-dropdown.jsx +91 -13
  18. package/client/components/terminal/terminal.jsx +103 -5
  19. package/client/components/terminal/terminal.styl +9 -0
  20. package/client/components/tree-list/tree-list-item.jsx +0 -1
  21. package/client/components/vnc/vnc-session.jsx +2 -0
  22. package/client/components/widgets/widget-control.jsx +3 -0
  23. package/client/components/widgets/widget-instance.jsx +26 -7
  24. package/client/css/includes/box.styl +3 -0
  25. package/client/store/init-state.js +2 -1
  26. package/client/store/load-data.js +3 -1
  27. package/client/store/mcp-handler.js +18 -0
  28. package/client/store/widgets.js +54 -0
  29. package/package.json +1 -1
@@ -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)
@@ -392,9 +396,9 @@ class Term extends Component {
392
396
  const dt = e.dataTransfer
393
397
  const fromFile = dt.getData('fromFile')
394
398
  const notSafeMsg = 'File name contains unsafe characters'
399
+ const isSshTerminal = this.props.tab.type === connectionMap.ssh
395
400
 
396
401
  if (fromFile) {
397
- // Handle SFTP file drop
398
402
  try {
399
403
  const fileData = JSON.parse(fromFile)
400
404
  const filePath = resolve(fileData.path, fileData.name)
@@ -402,6 +406,13 @@ class Term extends Component {
402
406
  message.error(notSafeMsg)
403
407
  return
404
408
  }
409
+ if (isSshTerminal) {
410
+ this.setState({
411
+ dropFileModalVisible: true,
412
+ droppedFiles: [{ path: filePath, isRemote: true }]
413
+ })
414
+ return
415
+ }
405
416
  this.attachAddon._sendData(`"${filePath}" `)
406
417
  return
407
418
  } catch (e) {
@@ -409,24 +420,78 @@ class Term extends Component {
409
420
  }
410
421
  }
411
422
 
412
- // Handle regular file drop
413
423
  const files = dt.files
414
424
  if (files && files.length) {
415
425
  const arr = Array.from(files)
416
426
  const filePaths = arr.map(f => getFilePath(f))
417
427
 
418
- // Check each file path individually
419
428
  const hasUnsafeFilename = filePaths.some(path => isUnsafeFilename(path))
420
429
  if (hasUnsafeFilename) {
421
430
  message.error(notSafeMsg)
422
431
  return
423
432
  }
424
433
 
434
+ if (isSshTerminal) {
435
+ this.setState({
436
+ dropFileModalVisible: true,
437
+ droppedFiles: filePaths.map(path => ({ path, isRemote: false }))
438
+ })
439
+ return
440
+ }
441
+
425
442
  const filesAll = filePaths.map(path => `"${path}"`).join(' ')
426
443
  this.attachAddon._sendData(filesAll)
427
444
  }
428
445
  }
429
446
 
447
+ handleDropFileModalCancel = () => {
448
+ this.setState({
449
+ dropFileModalVisible: false,
450
+ droppedFiles: []
451
+ })
452
+ }
453
+
454
+ handleDropFileAction = (action) => {
455
+ const { droppedFiles } = this.state
456
+ if (!droppedFiles || !droppedFiles.length) {
457
+ this.handleDropFileModalCancel()
458
+ return
459
+ }
460
+
461
+ const filePaths = droppedFiles.map(f => f.path)
462
+
463
+ switch (action) {
464
+ case 'trzUpload': {
465
+ if (this.trzszClient && this.trzszClient.isActive) {
466
+ message.warning('A transfer is already in progress')
467
+ this.handleDropFileModalCancel()
468
+ return
469
+ }
470
+ window._apiControlSelectFile = filePaths
471
+ this.attachAddon._sendData('trz\r')
472
+ break
473
+ }
474
+ case 'rzUpload': {
475
+ if (this.zmodemClient && this.zmodemClient.isActive) {
476
+ message.warning('A transfer is already in progress')
477
+ this.handleDropFileModalCancel()
478
+ return
479
+ }
480
+ window._apiControlSelectFile = filePaths
481
+ this.attachAddon._sendData('rz\r')
482
+ break
483
+ }
484
+ case 'inputPath':
485
+ default: {
486
+ const filesAll = filePaths.map(path => `"${path}"`).join(' ')
487
+ this.attachAddon._sendData(filesAll)
488
+ break
489
+ }
490
+ }
491
+
492
+ this.handleDropFileModalCancel()
493
+ }
494
+
430
495
  onSelection = () => {
431
496
  if (
432
497
  !this.props.config.copyWhenSelect ||
@@ -771,8 +836,35 @@ class Term extends Component {
771
836
  }
772
837
  }
773
838
 
839
+ onPasswordPromptDetected = () => {
840
+ if (!this.props.config.showCmdSuggestions) {
841
+ return
842
+ }
843
+ const cursorPos = this.getCursorPosition()
844
+ if (cursorPos) {
845
+ refsStatic
846
+ .get('terminal-suggestions')
847
+ ?.openPasswordSuggestions(cursorPos)
848
+ }
849
+ }
850
+
851
+ onPasswordPromptCancelled = () => {
852
+ const suggestions = refsStatic.get('terminal-suggestions')
853
+ if (suggestions?.state?.passwordMode) {
854
+ suggestions.closeSuggestions()
855
+ }
856
+ }
857
+
774
858
  onData = (d) => {
775
859
  this.handleInputEvent(d)
860
+ // Skip normal suggestion logic when in password mode
861
+ const suggestions = refsStatic.get('terminal-suggestions')
862
+ if (suggestions?.state?.passwordMode) {
863
+ if (d === '\r' || d === '\n') {
864
+ this.closeSuggestions()
865
+ }
866
+ return
867
+ }
776
868
  if (this.props.config.showCmdSuggestions) {
777
869
  const data = this.getCurrentInput()
778
870
  if (data && d !== '\r' && d !== '\n') {
@@ -1459,6 +1551,12 @@ class Term extends Component {
1459
1551
  countdown={this.state.reconnectCountdown}
1460
1552
  onCancel={this.handleCancelAutoReconnect}
1461
1553
  />
1554
+ <DropFileModal
1555
+ visible={this.state.dropFileModalVisible}
1556
+ files={this.state.droppedFiles}
1557
+ onSelect={this.handleDropFileAction}
1558
+ onCancel={this.handleDropFileModalCancel}
1559
+ />
1462
1560
  {spin}
1463
1561
  </div>
1464
1562
  </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
 
@@ -186,6 +186,7 @@ export default class VncSession extends PureComponent {
186
186
  qualityLevel = 3, // 0-9, lower = faster performance
187
187
  compressionLevel = 1, // 0-9, lower = faster performance
188
188
  shared = true,
189
+ showDotCursor = true, // show dot cursor when server sends no cursor image (common on Windows)
189
190
  username,
190
191
  password
191
192
  } = tab
@@ -376,6 +377,7 @@ export default class VncSession extends PureComponent {
376
377
  rfb.qualityLevel = qualityLevel
377
378
  rfb.compressionLevel = compressionLevel
378
379
  rfb.viewOnly = viewOnly
380
+ rfb.showDotCursor = showDotCursor
379
381
  this.rfb = rfb
380
382
  }
381
383
 
@@ -49,6 +49,9 @@ export default function WidgetControl ({ formData, widgetInstancesLength }) {
49
49
  config
50
50
  }
51
51
  window.store.widgetInstances.push(instance)
52
+ if (config.autoRun) {
53
+ window.store.toggleAutoRunWidget(instance)
54
+ }
52
55
  showMsg(msg, 'success', result.serverInfo, 10)
53
56
  } catch (err) {
54
57
  console.error('Failed to run widget:', err)
@@ -1,12 +1,21 @@
1
- import { Popconfirm, Popover } from 'antd'
2
- import { CloseOutlined, CopyOutlined } from '@ant-design/icons'
1
+ import {
2
+ Popconfirm,
3
+ Popover,
4
+ Tooltip,
5
+ Tag
6
+ } from 'antd'
7
+ import { CloseOutlined, CopyOutlined, ThunderboltOutlined } from '@ant-design/icons'
3
8
  import { copy } from '../../common/clipboard'
9
+ import classnames from 'classnames'
10
+ import { auto } from 'manate/react'
4
11
 
5
12
  const e = window.translate
6
13
 
7
- export default function WidgetInstance ({ item }) {
8
- const { id, title, serverInfo } = item
9
- const cls = 'item-list-unit'
14
+ export default auto(function WidgetInstance ({ item }) {
15
+ const { id, title, serverInfo, autoRun } = item
16
+ const cls = classnames('item-list-unit', {
17
+ 'autorun-active': autoRun
18
+ })
10
19
  const delProps = {
11
20
  title: e('del'),
12
21
  className: 'pointer list-item-remove'
@@ -31,6 +40,9 @@ export default function WidgetInstance ({ item }) {
31
40
  copy(serverInfo.url)
32
41
  }
33
42
  }
43
+ const handleToggleAutoRun = () => {
44
+ window.store.toggleAutoRunWidget(item)
45
+ }
34
46
  const popoverContent = serverInfo
35
47
  ? (
36
48
  <div>
@@ -45,12 +57,13 @@ export default function WidgetInstance ({ item }) {
45
57
  </div>
46
58
  )
47
59
  : null
60
+ const tag = autoRun ? <Tag color='green'>{e('autoRun')}</Tag> : null
48
61
  const titleDiv = (
49
62
  <div
50
63
  title={title}
51
64
  className='elli pd1y pd2x list-item-title'
52
65
  >
53
- {title}
66
+ {tag} {title}
54
67
  </div>
55
68
  )
56
69
  return (
@@ -71,6 +84,12 @@ export default function WidgetInstance ({ item }) {
71
84
  )
72
85
  : titleDiv
73
86
  }
87
+ <Tooltip title='Toggle auto-run'>
88
+ <ThunderboltOutlined
89
+ className='pointer list-item-autorun'
90
+ onClick={handleToggleAutoRun}
91
+ />
92
+ </Tooltip>
74
93
  <Popconfirm
75
94
  {...popProps}
76
95
  >
@@ -78,4 +97,4 @@ export default function WidgetInstance ({ item }) {
78
97
  </Popconfirm>
79
98
  </div>
80
99
  )
81
- }
100
+ })
@@ -106,6 +106,9 @@ for $i, $index in 5 16 32
106
106
  position absolute
107
107
  .pointer
108
108
  cursor pointer
109
+ .drag
110
+ cursor move
111
+ user-select none
109
112
  .width-100
110
113
  width 100%
111
114
  .width-40
@@ -156,7 +156,7 @@ export default () => {
156
156
  qmSortByFrequency: ls.getItem(qmSortByFrequencyKey) === 'yes',
157
157
 
158
158
  // sidebar
159
- openedSideBar: ls.getItem(openedSidebarKey),
159
+ openedSideBar: ls.getItem(openedSidebarKey) || '',
160
160
  leftSidebarWidth: parseInt(ls.getItem(leftSidebarWidthKey), 10) || 300,
161
161
  addPanelWidth: parseInt(ls.getItem(addPanelWidthLsKey), 10) || 300,
162
162
  menuOpened: false,
@@ -201,6 +201,7 @@ export default () => {
201
201
  // widgets
202
202
  widgets: [],
203
203
  widgetInstances: [],
204
+ autoRunWidgets: [],
204
205
  // move item
205
206
  openMoveModal: false,
206
207
  moveItem: null,
@@ -208,7 +208,6 @@ export default (Store) => {
208
208
  )
209
209
  setTimeout(
210
210
  () => {
211
- console.log('Auto sync is ready')
212
211
  store.autoSyncReady = true
213
212
  },
214
213
  2000
@@ -216,6 +215,9 @@ export default (Store) => {
216
215
  if (store.config.checkUpdateOnStart) {
217
216
  store.onCheckUpdate(false)
218
217
  }
218
+ store.startAutoRunWidgets().catch(err => {
219
+ console.error('Failed to start autorun widgets:', err)
220
+ })
219
221
  }
220
222
  Store.prototype.initCommandLine = async function () {
221
223
  const opts = await window.pre.runGlobalAsync('initCommandLine')
@@ -130,6 +130,14 @@ export default Store => {
130
130
  result = await store.mcpSftpDownload(args)
131
131
  break
132
132
 
133
+ // Transfer list/history operations
134
+ case 'sftp_transfer_list':
135
+ result = store.mcpSftpTransferList()
136
+ break
137
+ case 'sftp_transfer_history':
138
+ result = store.mcpSftpTransferHistory()
139
+ break
140
+
133
141
  // Zmodem (trzsz/rzsz) operations
134
142
  case 'zmodem_upload':
135
143
  result = store.mcpZmodemUpload(args)
@@ -692,6 +700,16 @@ export default Store => {
692
700
  }
693
701
  }
694
702
 
703
+ // ==================== Transfer List/History APIs ====================
704
+
705
+ Store.prototype.mcpSftpTransferList = function () {
706
+ return deepCopy(window.store.fileTransfers)
707
+ }
708
+
709
+ Store.prototype.mcpSftpTransferHistory = function () {
710
+ return deepCopy(window.store.transferHistory)
711
+ }
712
+
695
713
  // ==================== Zmodem (trzsz/rzsz) APIs ====================
696
714
 
697
715
  Store.prototype.mcpZmodemUpload = function (args) {
@@ -7,6 +7,8 @@ import {
7
7
  settingMap
8
8
  } from '../common/constants'
9
9
  import getInitItem from '../common/init-setting-item'
10
+ import deepCopy from 'json-deep-copy'
11
+ import generate from '../common/uid'
10
12
 
11
13
  export default Store => {
12
14
  Store.prototype.listWidgets = async () => {
@@ -58,4 +60,56 @@ export default Store => {
58
60
  store.settingTab = settingMap.widgets
59
61
  store.openSettingModal()
60
62
  }
63
+
64
+ Store.prototype.toggleAutoRunWidget = (instance) => {
65
+ const { store } = window
66
+ const { widgetId, config } = instance
67
+ if (instance.autoRun) {
68
+ const index = store.autoRunWidgets.findIndex(
69
+ w => w.id === instance.autoRunId
70
+ )
71
+ if (index > -1) {
72
+ store.autoRunWidgets.splice(index, 1)
73
+ }
74
+ instance.autoRun = false
75
+ instance.autoRunId = undefined
76
+ } else {
77
+ const id = generate()
78
+ const item = {
79
+ id,
80
+ widgetId,
81
+ config
82
+ }
83
+ store.autoRunWidgets.push(item)
84
+ instance.autoRun = true
85
+ instance.autoRunId = id
86
+ }
87
+ }
88
+
89
+ Store.prototype.startAutoRunWidgets = async function () {
90
+ const { store } = window
91
+ const items = store.autoRunWidgets
92
+ if (!items || !items.length) {
93
+ return
94
+ }
95
+ for (const item of items) {
96
+ try {
97
+ const result = await store.runWidget(item.widgetId, deepCopy(item.config))
98
+ if (result && result.instanceId) {
99
+ const instance = {
100
+ id: result.instanceId,
101
+ title: `${result.widgetId} (${result.instanceId})`,
102
+ widgetId: result.widgetId,
103
+ serverInfo: result.serverInfo,
104
+ config: item.config,
105
+ autoRun: true,
106
+ autoRunId: item.id
107
+ }
108
+ store.widgetInstances.push(instance)
109
+ }
110
+ } catch (err) {
111
+ console.error(`Failed to autorun widget ${item.widgetId}:`, err)
112
+ }
113
+ }
114
+ }
61
115
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "3.1.26",
3
+ "version": "3.2.0",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",