@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
@@ -41,6 +41,10 @@ export default function SyncForm (props) {
41
41
  if (syncType === syncTypes.cloud) {
42
42
  return !props.formData.token
43
43
  }
44
+ if (syncType === syncTypes.webdav) {
45
+ const { serverUrl, username, password } = props.formData
46
+ return !serverUrl || !username || !password
47
+ }
44
48
  const {
45
49
  token,
46
50
  gistId
@@ -70,6 +74,14 @@ export default function SyncForm (props) {
70
74
  } else {
71
75
  up[syncType + 'Proxy'] = ''
72
76
  }
77
+
78
+ // Handle WebDAV specific fields
79
+ if (syncType === syncTypes.webdav) {
80
+ up[syncType + 'ServerUrl'] = res.serverUrl || ''
81
+ up[syncType + 'Username'] = res.username || ''
82
+ up[syncType + 'Password'] = res.password || ''
83
+ }
84
+
73
85
  window.store.updateSyncSetting(up)
74
86
  const test = await window.store.testSyncToken(syncType, res.gistId)
75
87
  if (isError(test)) {
@@ -180,6 +192,9 @@ export default function SyncForm (props) {
180
192
  </p>
181
193
  )
182
194
  }
195
+ if (syncType === syncTypes.webdav) {
196
+ return createWebdavItems()
197
+ }
183
198
  if (syncType !== syncTypes.custom) {
184
199
  return null
185
200
  }
@@ -199,18 +214,73 @@ export default function SyncForm (props) {
199
214
  </FormItem>
200
215
  )
201
216
  }
217
+ function createWebdavItems () {
218
+ return (
219
+ <div>
220
+ <FormItem
221
+ label={createLabel('URL')}
222
+ name='serverUrl'
223
+ normalize={trim}
224
+ rules={[{
225
+ max: 500, message: '500 chars max'
226
+ }, {
227
+ required: true, message: 'Server URL is required'
228
+ }]}
229
+ >
230
+ <Input
231
+ placeholder='https://your-webdav-server.com/remote.php/dav/files/username'
232
+ id='sync-input-webdav-server-url'
233
+ />
234
+ </FormItem>
235
+ <FormItem
236
+ label={createLabel(e('username'))}
237
+ name='username'
238
+ normalize={trim}
239
+ rules={[{
240
+ max: 200, message: '200 chars max'
241
+ }, {
242
+ required: true, message: 'Username is required'
243
+ }]}
244
+ >
245
+ <Input
246
+ placeholder='WebDAV username'
247
+ id='sync-input-webdav-username'
248
+ />
249
+ </FormItem>
250
+ <FormItem
251
+ label={createLabel(e('password'))}
252
+ name='password'
253
+ normalize={trim}
254
+ rules={[{
255
+ max: 200, message: '200 chars max'
256
+ }, {
257
+ required: true, message: 'Password is required'
258
+ }]}
259
+ >
260
+ <Password
261
+ placeholder='WebDAV password'
262
+ id='sync-input-webdav-password'
263
+ />
264
+ </FormItem>
265
+ </div>
266
+ )
267
+ }
202
268
  const desc = syncType === syncTypes.custom
203
269
  ? 'jwt secret'
204
- : 'personal access token'
270
+ : syncType === syncTypes.webdav
271
+ ? 'WebDAV credentials'
272
+ : 'personal access token'
205
273
  const idDesc = syncType === syncTypes.custom
206
274
  ? 'user id'
207
- : 'gist ID'
275
+ : syncType === syncTypes.webdav
276
+ ? 'WebDAV server'
277
+ : 'gist ID'
208
278
  const tokenLabel = createLabel('token', desc)
209
279
  const gistLabel = createLabel('gist', idDesc)
210
280
  const syncPasswordName = e('encrypt') + ' ' + e('password')
211
281
  const syncPasswordLabel = createLabel(syncPasswordName, '')
212
282
  function createIdItem () {
213
- if (syncType === syncTypes.cloud) {
283
+ if (syncType === syncTypes.cloud || syncType === syncTypes.webdav) {
214
284
  return null
215
285
  }
216
286
  return (
@@ -231,7 +301,7 @@ export default function SyncForm (props) {
231
301
  )
232
302
  }
233
303
  function createPasswordItem () {
234
- if (syncType === syncTypes.cloud) {
304
+ if (syncType === syncTypes.cloud || syncType === syncTypes.webdav) {
235
305
  return null
236
306
  }
237
307
  return (
@@ -271,17 +341,11 @@ export default function SyncForm (props) {
271
341
  type: syncType,
272
342
  status: props.serverStatus
273
343
  }
274
- return (
275
- <Form
276
- onFinish={save}
277
- form={form}
278
- className='form-wrap pd1x'
279
- name={'setting-sync-form' + syncType}
280
- layout='vertical'
281
- initialValues={props.formData}
282
- >
283
- {renderWarning()}
284
- {createUrlItem()}
344
+ function createTokenItem () {
345
+ if (syncType === syncTypes.webdav) {
346
+ return null
347
+ }
348
+ return (
285
349
  <FormItem
286
350
  label={tokenLabel}
287
351
  hasFeedback
@@ -298,6 +362,20 @@ export default function SyncForm (props) {
298
362
  id={createId('token')}
299
363
  />
300
364
  </FormItem>
365
+ )
366
+ }
367
+ return (
368
+ <Form
369
+ onFinish={save}
370
+ form={form}
371
+ className='form-wrap pd1x'
372
+ name={'setting-sync-form' + syncType}
373
+ layout='vertical'
374
+ initialValues={props.formData}
375
+ >
376
+ {renderWarning()}
377
+ {createUrlItem()}
378
+ {createTokenItem()}
301
379
  {
302
380
  createIdItem()
303
381
  }
@@ -44,7 +44,11 @@ export default auto(function SyncSettingEntry (props) {
44
44
  apiUrl: syncSetting[type + 'ApiUrl'],
45
45
  lastSyncTime: syncSetting[type + 'LastSyncTime'],
46
46
  syncPassword: syncSetting[type + 'SyncPassword'],
47
- proxy: syncSetting[type + 'Proxy']
47
+ proxy: syncSetting[type + 'Proxy'],
48
+ // WebDAV specific fields
49
+ serverUrl: syncSetting[type + 'ServerUrl'],
50
+ username: syncSetting[type + 'Username'],
51
+ password: syncSetting[type + 'Password']
48
52
  }
49
53
  return (
50
54
  <SyncForm
@@ -98,8 +98,8 @@ export default memo(function TransferHistoryModal (props) {
98
98
  pageSize,
99
99
  showSizeChanger: true,
100
100
  pageSizeOptions: [5, 10, 20, 50, 100],
101
- onChange: handlePageSizeChange,
102
- position: ['topRight']
101
+ placement: 'topEnd',
102
+ onChange: handlePageSizeChange
103
103
  },
104
104
  size: 'small',
105
105
  rowKey: 'id'
@@ -168,6 +168,7 @@
168
168
  border-radius 0 0 3px 3px
169
169
  padding 0
170
170
  background var(--main)
171
+ -webkit-app-region no-drag
171
172
 
172
173
  .window-control-box
173
174
  display inline-block
@@ -23,9 +23,11 @@ export default auto(function WindowControl (props) {
23
23
  }
24
24
  const maximize = () => {
25
25
  window.pre.runGlobalAsync('maximize')
26
+ window.store.isMaximized = true
26
27
  }
27
28
  const unmaximize = () => {
28
29
  window.pre.runGlobalAsync('unmaximize')
30
+ window.store.isMaximized = false
29
31
  }
30
32
  const closeApp = () => {
31
33
  window.store.exit()
@@ -164,7 +164,7 @@ export default class TerminalCmdSuggestions extends Component {
164
164
  }
165
165
 
166
166
  handleDelete = (item) => {
167
- window.store.terminalCommandHistory.delete(item.command)
167
+ window.store.deleteCmdHistory(item.command)
168
168
  }
169
169
 
170
170
  handleSelect = (item) => {
@@ -484,9 +484,22 @@ class Term extends Component {
484
484
  }
485
485
 
486
486
  onClear = () => {
487
+ const shouldClear = this.searchAddon &&
488
+ window.store.termSearchOpen &&
489
+ window.store.termSearch
490
+ if (
491
+ shouldClear
492
+ ) {
493
+ this.searchAddon.clearDecorations()
494
+ }
487
495
  this.term.clear()
488
496
  this.term.focus()
489
- // this.notifyOnData('')
497
+ if (shouldClear) {
498
+ this.searchAddon._linesCache = undefined
499
+ this.timers.clearSearchTimer = setTimeout(() => {
500
+ refsStatic.get('term-search')?.next()
501
+ }, 100)
502
+ }
490
503
  }
491
504
 
492
505
  isRemote = () => {
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  // import { transferTypeMap } from '../../common/constants.js'
7
- import { safeGetItem, safeSetItem } from '../../common/safe-local-storage.js'
7
+ import { getItem, setItem } from '../../common/safe-local-storage.js'
8
8
  import { getLocalFileInfo } from '../sftp/file-read.js'
9
9
 
10
10
  /**
@@ -176,6 +176,8 @@ export class TransferClientBase {
176
176
 
177
177
  /**
178
178
  * Open file select dialog
179
+ * Supports window._apiControlSelectFile for e2e testing
180
+ * Set window._apiControlSelectFile = ['/path/to/file1', '/path/to/file2'] to bypass native dialog
179
181
  * @param {Object} options - Options for file selection
180
182
  * @returns {Promise<Array>} - Selected files
181
183
  */
@@ -186,20 +188,28 @@ export class TransferClientBase {
186
188
  message = 'Choose some files to send'
187
189
  } = options
188
190
 
189
- const properties = [
190
- directory ? 'openDirectory' : 'openFile',
191
- 'multiSelections',
192
- 'showHiddenFiles',
193
- 'noResolveAliases',
194
- 'treatPackageAsDirectory',
195
- 'dontAddToRecent'
196
- ]
197
-
198
- const files = await window.api.openDialog({
199
- title,
200
- message,
201
- properties
202
- }).catch(() => false)
191
+ let files
192
+ if (window._apiControlSelectFile) {
193
+ files = Array.isArray(window._apiControlSelectFile)
194
+ ? window._apiControlSelectFile
195
+ : [window._apiControlSelectFile]
196
+ delete window._apiControlSelectFile
197
+ } else {
198
+ const properties = [
199
+ directory ? 'openDirectory' : 'openFile',
200
+ 'multiSelections',
201
+ 'showHiddenFiles',
202
+ 'noResolveAliases',
203
+ 'treatPackageAsDirectory',
204
+ 'dontAddToRecent'
205
+ ]
206
+
207
+ files = await window.api.openDialog({
208
+ title,
209
+ message,
210
+ properties
211
+ }).catch(() => false)
212
+ }
203
213
 
204
214
  if (!files || !files.length) {
205
215
  return null
@@ -215,11 +225,22 @@ export class TransferClientBase {
215
225
 
216
226
  /**
217
227
  * Open save folder select dialog
228
+ * Supports window._apiControlSelectFolder for e2e testing
229
+ * Set window._apiControlSelectFolder = '/path/to/folder' to bypass native dialog
218
230
  * @returns {Promise<string>} - Selected folder path
219
231
  */
220
232
  openSaveFolderSelect = async () => {
233
+ if (window._apiControlSelectFolder) {
234
+ const folder = window._apiControlSelectFolder
235
+ delete window._apiControlSelectFolder
236
+ if (this.storageKey) {
237
+ setItem(this.storageKey, folder)
238
+ }
239
+ return folder
240
+ }
241
+
221
242
  // Try to use last saved path
222
- const lastPath = this.storageKey ? safeGetItem(this.storageKey) : null
243
+ const lastPath = this.storageKey ? getItem(this.storageKey) : null
223
244
 
224
245
  const savePaths = await window.api.openDialog({
225
246
  title: 'Choose a folder to save file(s)',
@@ -240,7 +261,7 @@ export class TransferClientBase {
240
261
  }
241
262
 
242
263
  if (this.storageKey) {
243
- safeSetItem(this.storageKey, savePaths[0])
264
+ setItem(this.storageKey, savePaths[0])
244
265
  }
245
266
  return savePaths[0]
246
267
  }
@@ -92,7 +92,6 @@ export default function TerminalInfoDisk (props) {
92
92
  },
93
93
  render: (v) => {
94
94
  if (k === 'up' || k === 'down') {
95
- console.log('render traffic', k, v)
96
95
  return filesize(v || 0)
97
96
  }
98
97
  return v
@@ -171,6 +171,10 @@ export default function TreeListItem (props) {
171
171
  props.onDragStart(e)
172
172
  }
173
173
 
174
+ const onDragEnter = e => {
175
+ props.onDragEnter(e)
176
+ }
177
+
174
178
  const onDragLeave = e => {
175
179
  props.onDragLeave(e)
176
180
  }
@@ -224,6 +228,7 @@ export default function TreeListItem (props) {
224
228
  'data-is-group': isGroup ? 'true' : 'false',
225
229
  onDragOver,
226
230
  onDragStart,
231
+ onDragEnter,
227
232
  onDragLeave,
228
233
  onDrop
229
234
  }
@@ -381,6 +381,18 @@ export default class ItemListTree extends Component {
381
381
  )
382
382
  }
383
383
 
384
+ onDragEnter = e => {
385
+ e.preventDefault()
386
+ let {
387
+ target
388
+ } = e
389
+ const tar = findParentBySel(target, '.tree-item')
390
+ if (tar) {
391
+ target = tar
392
+ }
393
+ target.classList.add('item-dragover-top')
394
+ }
395
+
384
396
  onDragLeave = e => {
385
397
  e.preventDefault()
386
398
  let {
@@ -390,7 +402,7 @@ export default class ItemListTree extends Component {
390
402
  if (tar) {
391
403
  target = tar
392
404
  }
393
- target.classList.remove('item-dragover')
405
+ target.classList.remove('item-dragover-top')
394
406
  }
395
407
 
396
408
  onDragOver = e => {
@@ -401,14 +413,14 @@ export default class ItemListTree extends Component {
401
413
  if (tar) {
402
414
  target = tar
403
415
  }
404
- target.classList.add('item-dragover')
416
+ target.classList.add('item-dragover-top')
405
417
  }
406
418
 
407
419
  onDrop = action(e => {
408
420
  e.preventDefault()
409
- const elems = document.querySelectorAll('.tree-item.item-dragover')
421
+ const elems = document.querySelectorAll('.tree-item.item-dragover-top')
410
422
  elems.forEach(elem => {
411
- elem.classList.remove('item-dragover')
423
+ elem.classList.remove('item-dragover-top')
412
424
  })
413
425
  let {
414
426
  target
@@ -662,6 +674,7 @@ export default class ItemListTree extends Component {
662
674
  'duplicateItem',
663
675
  'onDragStart',
664
676
  'onDrop',
677
+ 'onDragEnter',
665
678
  'onDragLeave',
666
679
  'onDragOver'
667
680
  ]
@@ -30,7 +30,7 @@
30
30
  vertical-align middle
31
31
  line-height 26px
32
32
  &.item-dragover-top
33
- border-top 1px solid #18d551
33
+ border 2px solid var(--primary)
34
34
  .tree-item-title
35
35
  flex-grow 1
36
36
  line-height 26px
@@ -18,6 +18,7 @@ import {
18
18
  import * as ls from '../common/safe-local-storage'
19
19
  import { refs, refsStatic } from '../components/common/ref'
20
20
  import { action } from 'manate'
21
+ import uid from '../common/uid'
21
22
  import deepCopy from 'json-deep-copy'
22
23
  import { aiConfigsArr } from '../components/ai/ai-config-props'
23
24
  import settingList from '../common/setting-list'
@@ -345,36 +346,35 @@ export default Store => {
345
346
  return
346
347
  }
347
348
  const { terminalCommandHistory } = window.store
348
- const existing = terminalCommandHistory.get(cmd)
349
+ const existing = terminalCommandHistory.find(item => item.cmd === cmd)
349
350
  if (existing) {
350
- // Use set() to trigger reactivity
351
- terminalCommandHistory.set(cmd, {
352
- count: existing.count + 1,
353
- lastUseTime: new Date().toISOString()
354
- })
351
+ existing.count = existing.count + 1
352
+ existing.lastUseTime = new Date().toISOString()
355
353
  } else {
356
- terminalCommandHistory.set(cmd, {
354
+ terminalCommandHistory.push({
355
+ id: uid(),
356
+ cmd,
357
357
  count: 1,
358
358
  lastUseTime: new Date().toISOString()
359
359
  })
360
360
  }
361
- if (terminalCommandHistory.size > 100) {
361
+ if (terminalCommandHistory.length > 200) {
362
362
  // Delete oldest 20 items when history exceeds 100
363
- const entries = Array.from(terminalCommandHistory.entries())
364
- entries.sort((a, b) => new Date(a[1].lastUseTime).getTime() - new Date(b[1].lastUseTime).getTime())
365
- for (let i = 0; i < 20 && i < entries.length; i++) {
366
- terminalCommandHistory.delete(entries[i][0])
367
- }
363
+ terminalCommandHistory.sort((a, b) => new Date(a.lastUseTime).getTime() - new Date(b.lastUseTime).getTime())
364
+ terminalCommandHistory.splice(0, 20)
368
365
  }
369
366
  })
370
367
 
371
368
  Store.prototype.deleteCmdHistory = function (cmd) {
372
369
  const { terminalCommandHistory } = window.store
373
- terminalCommandHistory.delete(cmd)
370
+ const idx = terminalCommandHistory.findIndex(item => item.cmd === cmd)
371
+ if (idx !== -1) {
372
+ terminalCommandHistory.splice(idx, 1)
373
+ }
374
374
  }
375
375
 
376
376
  Store.prototype.clearAllCmdHistory = function () {
377
- window.store.terminalCommandHistory = new Map()
377
+ window.store.terminalCommandHistory = []
378
378
  }
379
379
 
380
380
  Store.prototype.runCmdFromHistory = function (cmd) {
@@ -21,10 +21,8 @@ import {
21
21
  dismissDelKeyTipLsKey,
22
22
  qmSortByFrequencyKey,
23
23
  resolutionsLsKey,
24
- aiChatHistoryKey,
25
24
  syncServerDataKey,
26
- splitMap,
27
- cmdHistoryKey
25
+ splitMap
28
26
  } from '../common/constants'
29
27
  import * as ls from '../common/safe-local-storage'
30
28
  import { exclude } from 'manate'
@@ -54,7 +52,7 @@ export default () => {
54
52
  lastDataUpdateTime: 0,
55
53
  tabs: [],
56
54
  activeTabId: '',
57
- history: ls.safeGetItemJSON('history', []),
55
+ history: [],
58
56
  sshConfigs: [],
59
57
  bookmarks: [],
60
58
  bookmarksMap: new Map(),
@@ -74,32 +72,9 @@ export default () => {
74
72
  addressBookmarksLocal: ls.getItemJSON(localAddrBookmarkLsKey, []),
75
73
  openResolutionEdit: false,
76
74
  resolutions: ls.getItemJSON(resolutionsLsKey, []),
77
- // terminalCommandHistory: Map<cmdString, {count: Number, lastUseTime: DateString}>
78
- // Load from localStorage and migrate from old format (Set of strings) if needed
79
- terminalCommandHistory: (() => {
80
- const savedData = ls.safeGetItemJSON(cmdHistoryKey, [])
81
- const map = new Map()
82
- if (Array.isArray(savedData)) {
83
- // Check if old format (array of strings) or new format (array of objects)
84
- if (savedData.length > 0 && typeof savedData[0] === 'string') {
85
- // Old format: migrate to new format
86
- savedData.forEach(cmd => {
87
- map.set(cmd, { count: 1, lastUseTime: new Date().toISOString() })
88
- })
89
- } else {
90
- // New format: array of {cmd, count, lastUseTime}
91
- savedData.forEach(item => {
92
- if (item.cmd) {
93
- map.set(item.cmd, {
94
- count: item.count || 1,
95
- lastUseTime: item.lastUseTime || new Date().toISOString()
96
- })
97
- }
98
- })
99
- }
100
- }
101
- return map
102
- })(),
75
+ // terminalCommandHistory: [{ id, cmd, count, lastUseTime }]
76
+ // Loaded from DB in initData
77
+ terminalCommandHistory: [],
103
78
 
104
79
  // workspaces
105
80
  workspaces: [],
@@ -111,7 +86,7 @@ export default () => {
111
86
 
112
87
  // batch input selected tab ids
113
88
  _batchInputSelectedTabIds: new Set(),
114
- aiChatHistory: ls.safeGetItemJSON(aiChatHistoryKey, []),
89
+ aiChatHistory: [],
115
90
 
116
91
  // sftp
117
92
  fileOperation: fileOperationsMap.cp, // cp or mv