@electerm/electerm-react 3.0.18 → 3.1.16

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 (35) hide show
  1. package/client/common/constants.js +4 -4
  2. package/client/common/db.js +20 -10
  3. package/client/components/file-transfer/conflict-resolve.jsx +38 -4
  4. package/client/components/file-transfer/transfer-queue.jsx +10 -4
  5. package/client/components/file-transfer/transfer.jsx +7 -4
  6. package/client/components/file-transfer/transports-action-store.jsx +14 -2
  7. package/client/components/footer/cmd-history.jsx +2 -4
  8. package/client/components/quick-commands/qm.styl +8 -0
  9. package/client/components/quick-commands/quick-command-item.jsx +4 -0
  10. package/client/components/quick-commands/quick-commands-box.jsx +10 -0
  11. package/client/components/quick-commands/quick-commands-form-elem.jsx +1 -1
  12. package/client/components/quick-commands/quick-commands-list-form.jsx +59 -4
  13. package/client/components/quick-commands/quick-commands-list.jsx +33 -3
  14. package/client/components/setting-panel/list.styl +5 -1
  15. package/client/components/setting-sync/server-data-status.jsx +2 -1
  16. package/client/components/setting-sync/setting-sync-form.jsx +93 -15
  17. package/client/components/setting-sync/setting-sync.jsx +5 -1
  18. package/client/components/sidebar/transfer-history-modal.jsx +2 -2
  19. package/client/components/tabs/tabs.styl +1 -0
  20. package/client/components/tabs/window-control.jsx +2 -0
  21. package/client/components/terminal/terminal-command-dropdown.jsx +1 -1
  22. package/client/components/terminal/terminal.jsx +14 -1
  23. package/client/components/terminal/transfer-client-base.js +38 -17
  24. package/client/components/terminal-info/network.jsx +0 -1
  25. package/client/components/tree-list/tree-list-item.jsx +5 -0
  26. package/client/components/tree-list/tree-list.jsx +17 -4
  27. package/client/components/tree-list/tree-list.styl +1 -1
  28. package/client/store/common.js +15 -15
  29. package/client/store/init-state.js +6 -31
  30. package/client/store/mcp-handler.js +315 -129
  31. package/client/store/store.js +1 -1
  32. package/client/store/sync.js +129 -3
  33. package/client/store/transfer-list.js +3 -2
  34. package/client/store/watch.js +1 -25
  35. package/package.json +1 -1
@@ -190,13 +190,15 @@ export const syncTypes = buildConst([
190
190
  'github',
191
191
  'gitee',
192
192
  'custom',
193
- 'cloud'
193
+ 'cloud',
194
+ 'webdav'
194
195
  ])
195
196
  export const syncTokenCreateUrls = {
196
197
  gitee: 'https://gitee.com/github-zxdong262/electerm/wikis/Create%20personal%20access%20token?sort_id=3028409',
197
198
  github: 'https://github.com/electerm/electerm/wiki/Create-personal-access-token',
198
199
  custom: 'https://github.com/electerm/electerm/wiki/Custom-sync-server',
199
- cloud: 'https://electerm-cloud.html5beta.com'
200
+ cloud: 'https://electerm-cloud.html5beta.com',
201
+ webdav: 'https://github.com/electerm/electerm/wiki/WebDAV-sync'
200
202
  }
201
203
  export const settingSyncId = 'setting-sync'
202
204
  export const settingTerminalId = 'setting-terminal'
@@ -350,6 +352,4 @@ export const terminalTypes = [
350
352
  export const sshConfigLoadKey = 'ssh-config-loaded'
351
353
  export const sshConfigKey = 'ignore-ssh-config'
352
354
  export const connectionHoppingWarnKey = 'connectionHoppingWarnned'
353
- export const aiChatHistoryKey = 'ai-chat-history'
354
355
  export const syncServerDataKey = 'sync-server-data'
355
- export const cmdHistoryKey = 'cmd-history'
@@ -23,17 +23,27 @@ const dbAction = (...args) => {
23
23
  /**
24
24
  * standalone db names
25
25
  */
26
- export const dbNames = without(
27
- Object.keys(settingMap),
28
- settingMap.setting,
29
- settingMap.widgets
30
- )
26
+ export const dbNames = [
27
+ ...without(
28
+ Object.keys(settingMap),
29
+ settingMap.setting,
30
+ settingMap.widgets
31
+ ),
32
+ 'history',
33
+ 'terminalCommandHistory',
34
+ 'aiChatHistory'
35
+ ]
31
36
 
32
- export const dbNamesForWatch = without(
33
- Object.keys(settingMap),
34
- settingMap.setting,
35
- settingMap.widgets
36
- )
37
+ export const dbNamesForWatch = [
38
+ ...without(
39
+ Object.keys(settingMap),
40
+ settingMap.setting,
41
+ settingMap.widgets
42
+ ),
43
+ 'history',
44
+ 'terminalCommandHistory',
45
+ 'aiChatHistory'
46
+ ]
37
47
 
38
48
  /**
39
49
  * db insert
@@ -31,25 +31,50 @@ export default class ConfirmModalStore extends Component {
31
31
  transferToConfirm: null
32
32
  }
33
33
  this.queue = []
34
+ this.queuedTransferIds = new Set()
35
+ this.activeTransferId = null
34
36
  this.id = 'transfer-conflict'
35
37
  refsStatic.add(this.id, this)
36
38
  }
37
39
 
38
40
  addConflict = (transfer) => {
41
+ const transferId = transfer?.id
42
+ if (!transferId) {
43
+ return
44
+ }
45
+ if (this.activeTransferId === transferId || this.queuedTransferIds.has(transferId)) {
46
+ return
47
+ }
48
+ const globalPolicy = window._transferConflictPolicy
49
+ if (globalPolicy && Object.values(fileActions).includes(globalPolicy)) {
50
+ const { id, transferBatch } = transfer
51
+ const trid = `tr-${transferBatch}-${id}`
52
+ const currentTransfer = refsTransfers.get(trid)
53
+ currentTransfer?.onDecision(globalPolicy)
54
+ return
55
+ }
39
56
  this.queue.push(transfer)
40
- if (!this.state.transferToConfirm) {
57
+ this.queuedTransferIds.add(transferId)
58
+ if (!this.activeTransferId) {
41
59
  this.showNext()
42
60
  }
43
61
  }
44
62
 
45
63
  showNext = () => {
46
64
  const next = this.queue.shift()
65
+ if (next?.id) {
66
+ this.queuedTransferIds.delete(next.id)
67
+ }
68
+ this.activeTransferId = next?.id || null
47
69
  this.setState({
48
70
  transferToConfirm: next
49
71
  })
50
72
  }
51
73
 
52
74
  act = (action) => {
75
+ if (!this.state.transferToConfirm) {
76
+ return
77
+ }
53
78
  const { id, transferBatch } = this.state.transferToConfirm
54
79
  const toAll = action.includes('All')
55
80
  const policy = toAll ? action.replace('All', '') : action
@@ -60,10 +85,17 @@ export default class ConfirmModalStore extends Component {
60
85
  if (doFilter) {
61
86
  // Update all existing transfers with same batch ID in DOM
62
87
  const prefix = `tr-${transferBatch}-`
88
+ const pendingConflictIds = new Set([
89
+ id,
90
+ ...this.queue
91
+ .filter(d => d.transferBatch === transferBatch)
92
+ .map(d => d.id)
93
+ ])
63
94
  for (const [key, r] of window.refsTransfers.entries()) {
64
95
  if (key.startsWith(prefix)) {
65
- if (key !== trid) {
66
- r.resolvePolicy = policy
96
+ r.resolvePolicy = policy
97
+ const transferId = r.props.transfer?.id
98
+ if (key !== trid && pendingConflictIds.has(transferId)) {
67
99
  r.onDecision(policy)
68
100
  }
69
101
  }
@@ -72,9 +104,11 @@ export default class ConfirmModalStore extends Component {
72
104
  }
73
105
 
74
106
  // Resolve current conflict
75
- refsTransfers.get(trid)?.onDecision(policy)
107
+ const currentTransfer = refsTransfers.get(trid)
108
+ currentTransfer?.onDecision(policy)
76
109
 
77
110
  // Move to the next item
111
+ this.activeTransferId = null
78
112
  this.setState({
79
113
  transferToConfirm: null
80
114
  }, this.showNext)
@@ -41,7 +41,12 @@ export default class Queue extends Component {
41
41
  return new Promise((resolve, reject) => {
42
42
  const { fileTransfers } = window.store
43
43
  const [id, updateObj] = args
44
+ let completed = false
44
45
  const end = () => {
46
+ if (completed) {
47
+ return
48
+ }
49
+ completed = true
45
50
  this.currentRun && this.currentRun.stop()
46
51
  resolve()
47
52
  }
@@ -99,10 +104,11 @@ export default class Queue extends Component {
99
104
  })()
100
105
  }
101
106
 
102
- // For non-transfer operations, check immediately
103
- // if (!isTransferInit) {
104
- // checkCompletion()
105
- // }
107
+ // Progress and delete updates are synchronous store mutations.
108
+ // Resolve them here so the queue cannot stall waiting for another reaction.
109
+ if (!isTransferInit) {
110
+ checkCompletion()
111
+ }
106
112
  }
107
113
 
108
114
  function checkCompletion () {
@@ -77,11 +77,11 @@ export default class TransportAction extends Component {
77
77
  }
78
78
 
79
79
  localCheckExist = (path) => {
80
- return getLocalFileInfo(path).catch(console.log)
80
+ return getLocalFileInfo(path)
81
+ .catch(() => null)
81
82
  }
82
83
 
83
84
  remoteCheckExist = (path, tabId) => {
84
- // return true
85
85
  const sftp = refs.get('sftp-' + tabId)?.sftp
86
86
  if (!sftp) {
87
87
  console.log('remoteCheckExist error', 'sftp not exist')
@@ -373,10 +373,12 @@ export default class TransportAction extends Component {
373
373
  toFile
374
374
  })
375
375
  if (transfer.resolvePolicy) {
376
- return this.onDecision(transfer.resolvePolicy)
376
+ this.onDecision(transfer.resolvePolicy)
377
+ return true
377
378
  }
378
379
  if (this.resolvePolicy) {
379
- return this.onDecision(this.resolvePolicy)
380
+ this.onDecision(this.resolvePolicy)
381
+ return true
380
382
  }
381
383
  const transferWithToFile = {
382
384
  ...copy(transfer),
@@ -386,6 +388,7 @@ export default class TransportAction extends Component {
386
388
  refsStatic.get('transfer-conflict')?.addConflict(transferWithToFile)
387
389
  return true
388
390
  }
391
+ return false
389
392
  }
390
393
 
391
394
  onDecision = (policy) => {
@@ -12,6 +12,11 @@ import { refsStatic } from '../common/ref'
12
12
  window.initingFtpTabIds = new Set()
13
13
 
14
14
  export default class TransportsActionStore extends Component {
15
+ constructor (props) {
16
+ super(props)
17
+ this.pendingInitIds = new Set()
18
+ }
19
+
15
20
  componentDidMount () {
16
21
  this.control()
17
22
  }
@@ -29,6 +34,12 @@ export default class TransportsActionStore extends Component {
29
34
  const {
30
35
  fileTransfers
31
36
  } = store
37
+ this.pendingInitIds = new Set(
38
+ Array.from(this.pendingInitIds).filter(id => {
39
+ const transfer = fileTransfers.find(t => t.id === id)
40
+ return transfer && transfer.inited !== true
41
+ })
42
+ )
32
43
 
33
44
  // First loop: Handle same type transfers
34
45
  for (const t of fileTransfers) {
@@ -57,7 +68,7 @@ export default class TransportsActionStore extends Component {
57
68
  inited,
58
69
  pausing
59
70
  } = t
60
- return typeTo !== typeFrom && inited && pausing !== true
71
+ return typeTo !== typeFrom && (inited || this.pendingInitIds.has(t.id)) && pausing !== true
61
72
  }).length
62
73
 
63
74
  if (count >= maxTransport) {
@@ -80,7 +91,7 @@ export default class TransportsActionStore extends Component {
80
91
 
81
92
  const isTransfer = typeTo !== typeFrom
82
93
 
83
- if (inited || !isTransfer) {
94
+ if (inited || this.pendingInitIds.has(id) || !isTransfer) {
84
95
  continue
85
96
  }
86
97
 
@@ -95,6 +106,7 @@ export default class TransportsActionStore extends Component {
95
106
 
96
107
  if (count < maxTransport) {
97
108
  count++
109
+ this.pendingInitIds.add(id)
98
110
  refsStatic.get('transfer-queue')?.addToQueue(
99
111
  'update',
100
112
  id,
@@ -31,7 +31,7 @@ export default auto(function CmdHistory (props) {
31
31
 
32
32
  function handleDeleteCommand (cmd, ev) {
33
33
  ev.stopPropagation()
34
- terminalCommandHistory.delete(cmd)
34
+ window.store.deleteCmdHistory(cmd)
35
35
  }
36
36
 
37
37
  function handleCopyCommand (cmd, ev) {
@@ -54,9 +54,7 @@ export default auto(function CmdHistory (props) {
54
54
  setKeyword(e.target.value)
55
55
  }
56
56
 
57
- const historyArray = Array.from(terminalCommandHistory || [])
58
- .map(([cmd, info]) => ({ cmd, ...info }))
59
- .reverse()
57
+ const historyArray = (terminalCommandHistory || []).slice().reverse()
60
58
 
61
59
  let filtered = filterArray(historyArray, keyword)
62
60
 
@@ -25,6 +25,14 @@
25
25
 
26
26
  .item-list-unit.dragover
27
27
  border: 1px dashed var(--primary)
28
+ .qm-item-dragover
29
+ border-left 2px solid var(--primary)
30
+ .qm-drag-handle
31
+ cursor grab
32
+ .qm-field-dragging
33
+ opacity 0.4
34
+ .qm-field-dragover
35
+ border-top 2px solid var(--primary)
28
36
  .qm-label-select
29
37
  min-width 120px
30
38
 
@@ -23,6 +23,8 @@ export default class QuickCommandsItem extends PureComponent {
23
23
  draggable,
24
24
  handleDragOver,
25
25
  handleDragStart,
26
+ handleDragEnter,
27
+ handleDragLeave,
26
28
  handleDrop
27
29
  } = this.props
28
30
  const cls = classNames('qm-item mg1r mg1b')
@@ -34,6 +36,8 @@ export default class QuickCommandsItem extends PureComponent {
34
36
  draggable,
35
37
  onDragOver: handleDragOver,
36
38
  onDragStart: handleDragStart,
39
+ onDragEnter: handleDragEnter,
40
+ onDragLeave: handleDragLeave,
37
41
  onDrop: handleDrop
38
42
  }
39
43
  return (
@@ -87,6 +87,14 @@ export default function QuickCommandsFooterBox (props) {
87
87
  e.dataTransfer.setData('idDragged', e.target.getAttribute('data-id'))
88
88
  }
89
89
 
90
+ function onDragEnter (e) {
91
+ e.target.closest('.qm-item')?.classList.add('qm-item-dragover')
92
+ }
93
+
94
+ function onDragLeave (e) {
95
+ e.target.closest('.qm-item')?.classList.remove('qm-item-dragover')
96
+ }
97
+
90
98
  function onDrop (e) {
91
99
  onDropFunc(e, '.qm-item')
92
100
  }
@@ -116,6 +124,8 @@ export default function QuickCommandsFooterBox (props) {
116
124
  draggable={!qmSortByFrequency}
117
125
  handleDragOver={onDragOver}
118
126
  handleDragStart={onDragStart}
127
+ handleDragEnter={onDragEnter}
128
+ handleDragLeave={onDragLeave}
119
129
  handleDrop={onDrop}
120
130
  />
121
131
  )
@@ -127,7 +127,7 @@ export default function QuickCommandForm (props) {
127
127
  >
128
128
  <InputAutoFocus />
129
129
  </FormItem>
130
- {renderQm()}
130
+ {renderQm(form)}
131
131
  <FormItem
132
132
  name='labels'
133
133
  label={e('label')}
@@ -5,7 +5,7 @@ import {
5
5
  Button,
6
6
  Input
7
7
  } from 'antd'
8
- import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'
8
+ import { MinusCircleOutlined, PlusOutlined, HolderOutlined } from '@ant-design/icons'
9
9
  import HelpIcon from '../common/help-icon'
10
10
  import { copy } from '../../common/clipboard'
11
11
  import { useRef } from 'react'
@@ -14,15 +14,70 @@ const FormItem = Form.Item
14
14
  const FormList = Form.List
15
15
  const e = window.translate
16
16
 
17
- export default function renderQm () {
17
+ export default function renderQm (form) {
18
18
  const focused = useRef(0)
19
- function renderItem (field, i, add, remove) {
19
+ const dragIndexRef = useRef(null)
20
+
21
+ function handleDragStart (e, index) {
22
+ dragIndexRef.current = index
23
+ e.target.closest('.ant-space-compact')?.classList.add('qm-field-dragging')
24
+ e.dataTransfer.effectAllowed = 'move'
25
+ e.dataTransfer.setData('text/plain', String(index))
26
+ }
27
+
28
+ function handleDragOver (e, index) {
29
+ e.preventDefault()
30
+ e.dataTransfer.dropEffect = 'move'
31
+ const el = e.target.closest('.ant-space-compact')
32
+ if (dragIndexRef.current !== index && el) {
33
+ el.classList.add('qm-field-dragover')
34
+ }
35
+ }
36
+
37
+ function handleDragLeave (e) {
38
+ e.target.closest('.ant-space-compact')?.classList.remove('qm-field-dragover')
39
+ }
40
+
41
+ function handleDrop (e, index, form) {
42
+ e.preventDefault()
43
+ const el = e.target.closest('.ant-space-compact')
44
+ el?.classList.remove('qm-field-dragover')
45
+ const dragIndex = dragIndexRef.current
46
+ if (dragIndex === null || dragIndex === index) {
47
+ dragIndexRef.current = null
48
+ return
49
+ }
50
+ const commands = form.getFieldValue('commands') || []
51
+ const item = commands[dragIndex]
52
+ const newCommands = [...commands]
53
+ newCommands.splice(dragIndex, 1)
54
+ newCommands.splice(index, 0, item)
55
+ form.setFieldValue('commands', newCommands)
56
+ dragIndexRef.current = null
57
+ }
58
+
59
+ function handleDragEnd (e) {
60
+ const el = e.target.closest('.ant-space-compact')
61
+ el?.classList.remove('qm-field-dragging')
62
+ el?.classList.remove('qm-field-dragover')
63
+ dragIndexRef.current = null
64
+ }
65
+
66
+ function renderItem (field, i, add, remove, form) {
20
67
  return (
21
68
  <Space.Compact
22
69
  align='center'
23
70
  className='width-100 mg2b'
24
71
  key={field.key}
72
+ draggable
73
+ onDragStart={(e) => handleDragStart(e, i)}
74
+ onDragOver={(e) => handleDragOver(e, i)}
75
+ onDragLeave={handleDragLeave}
76
+ onDrop={(e) => handleDrop(e, i, form)}
77
+ onDragEnd={handleDragEnd}
25
78
  >
79
+ <HolderOutlined className='mg1r qm-drag-handle' />
80
+
26
81
  <Space.Addon>{e('delay')}</Space.Addon>
27
82
  <FormItem
28
83
  label=''
@@ -119,7 +174,7 @@ export default function renderQm () {
119
174
  <>
120
175
  {
121
176
  fields.map((field, i) => {
122
- return renderItem(field, i, add, remove)
177
+ return renderItem(field, i, add, remove, form)
123
178
  })
124
179
  }
125
180
  <FormItem>
@@ -3,12 +3,14 @@
3
3
  */
4
4
 
5
5
  import List from '../setting-panel/list'
6
- import { PlusOutlined } from '@ant-design/icons'
6
+ import { PlusOutlined, CopyOutlined } from '@ant-design/icons'
7
7
  import { Select } from 'antd'
8
8
  import classnames from 'classnames'
9
9
  import highlight from '../common/highlight'
10
10
  import QmTransport from './quick-command-transport'
11
11
  import onDrop from './on-drop'
12
+ import copy from 'json-deep-copy'
13
+ import uid from '../../common/uid'
12
14
 
13
15
  const { Option } = Select
14
16
  const e = window.translate
@@ -45,17 +47,44 @@ export default class QuickCommandsList extends List {
45
47
  }
46
48
 
47
49
  handleDragEnter = e => {
48
- e.target.closest('.item-list-unit').classList.add('dragover')
50
+ e.target.closest('.item-list-unit').classList.add('qm-field-dragover')
49
51
  }
50
52
 
51
53
  handleDragLeave = e => {
52
- e.target.closest('.item-list-unit').classList.remove('dragover')
54
+ e.target.closest('.item-list-unit').classList.remove('qm-field-dragover')
53
55
  }
54
56
 
55
57
  handleDrop = e => {
56
58
  onDrop(e, '.item-list-unit')
57
59
  }
58
60
 
61
+ duplicateItem = (e, item) => {
62
+ e.stopPropagation()
63
+ const { store } = window
64
+ const newCommand = copy(item)
65
+ newCommand.id = uid()
66
+ const baseName = item.name.replace(/\(\d+\)$/, '')
67
+ const sameNameCount = store.currentQuickCommands.filter(
68
+ cmd => cmd.name && cmd.name.replace(/\(\d+\)$/, '').includes(baseName)
69
+ ).length
70
+ const duplicateIndex = sameNameCount > 0 ? sameNameCount : 1
71
+ newCommand.name = baseName + '(' + duplicateIndex + ')'
72
+ store.addQuickCommand(newCommand)
73
+ }
74
+
75
+ renderDuplicateBtn = (item) => {
76
+ if (!item.id) {
77
+ return null
78
+ }
79
+ return (
80
+ <CopyOutlined
81
+ title={e('duplicate')}
82
+ className='pointer list-item-duplicate'
83
+ onClick={(e) => this.duplicateItem(e, item)}
84
+ />
85
+ )
86
+ }
87
+
59
88
  renderItem = (item, i) => {
60
89
  if (!item) {
61
90
  return null
@@ -94,6 +123,7 @@ export default class QuickCommandsList extends List {
94
123
  }
95
124
  {title}
96
125
  </div>
126
+ {this.renderDuplicateBtn(item)}
97
127
  {this.renderDelBtn(item)}
98
128
  </div>
99
129
  )
@@ -7,6 +7,7 @@
7
7
  .list-item-apply
8
8
  .list-item-remove
9
9
  .list-item-bookmark
10
+ .list-item-duplicate
10
11
  display none
11
12
  width 24px
12
13
  line-height 35px
@@ -35,6 +36,7 @@
35
36
  .list-item-edit
36
37
  .list-item-remove
37
38
  .list-item-bookmark
39
+ .list-item-duplicate
38
40
  display block
39
41
  .theme-item:hover
40
42
  .list-item-remove
@@ -49,4 +51,6 @@
49
51
  // right 20px
50
52
  .item-list-unit
51
53
  .list-item-bookmark
52
- right 18px
54
+ right 18px
55
+ .list-item-duplicate
56
+ right 24px
@@ -1,3 +1,4 @@
1
+ import { syncTypes } from '../../common/constants'
1
2
  import { useState } from 'react'
2
3
  import { LoadingOutlined, ReloadOutlined } from '@ant-design/icons'
3
4
  import dayjs from 'dayjs'
@@ -10,7 +11,7 @@ export default function ServerDataStatus (props) {
10
11
  const [loading, setLoading] = useState(false)
11
12
  const token = store.getSyncToken(type)
12
13
  const gistId = store.getSyncGistId(type)
13
- const canSync = token && (gistId || type === 'custom' || type === 'cloud')
14
+ const canSync = token && (gistId || type === 'custom' || type === 'cloud' || type === syncTypes.webdav)
14
15
 
15
16
  async function handleReload () {
16
17
  setLoading(true)