@electerm/electerm-react 1.90.6 → 1.91.1

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.
@@ -5,23 +5,25 @@
5
5
  * @return {String}
6
6
  */
7
7
 
8
- export default (basePath, nameOrDot) => {
8
+ export default function resolve (basePath, nameOrDot) {
9
9
  const hasWinDrive = (path) => /^[a-zA-Z]:/.test(path)
10
10
  const isWin = basePath.includes('\\') || nameOrDot.includes('\\') || hasWinDrive(basePath) || hasWinDrive(nameOrDot)
11
11
  const sep = isWin ? '\\' : '/'
12
- // Handle Windows drive letters (with or without initial slash)
13
12
  if (/^[a-zA-Z]:/.test(nameOrDot)) {
14
13
  return nameOrDot.replace(/^\//, '').replace(/\//g, sep)
15
14
  }
16
- // Handle absolute paths
17
15
  if (nameOrDot.startsWith('/')) {
18
16
  return nameOrDot.replace(/\\/g, sep)
19
17
  }
20
18
  if (nameOrDot === '..') {
19
+ const baseEndsWithSep = basePath.endsWith(sep)
21
20
  const parts = basePath.split(sep)
22
21
  if (parts.length > 1) {
23
22
  parts.pop()
24
- return isWin && parts.length === 1 ? '/' : parts.join(sep) || '/'
23
+ if (isWin && parts.length === 1) {
24
+ return baseEndsWithSep ? '/' : parts.join(sep)
25
+ }
26
+ return parts.join(sep) || '/'
25
27
  }
26
28
  return '/'
27
29
  }
@@ -12,6 +12,13 @@ import {
12
12
  } from '../sftp/file-read'
13
13
  import resolve from '../../common/resolve'
14
14
  import { refsTransfers, refsStatic, refs } from '../common/ref'
15
+ import {
16
+ zipCmd,
17
+ unzipCmd,
18
+ rmCmd,
19
+ mvCmd,
20
+ mkdirCmd
21
+ } from './zip'
15
22
  import './transfer.styl'
16
23
 
17
24
  const { assign } = Object
@@ -24,13 +31,15 @@ export default class TransportAction extends Component {
24
31
  transferBatch = '',
25
32
  tabId
26
33
  } = props.transfer
34
+ const sftp = refs.get('sftp-' + tabId)
27
35
  this.id = `tr-${transferBatch}-${id}`
28
36
  this.tabId = tabId
29
37
  refsTransfers.add(this.id, this)
30
38
  this.total = 0
31
39
  this.transferred = 0
32
40
  this.currentProgress = 1
33
- this.isFtp = refs.get('sftp-' + tabId)?.type === 'ftp'
41
+ this.isFtp = sftp?.type === 'ftp'
42
+ this.terminalId = sftp?.terminalId
34
43
  }
35
44
 
36
45
  componentDidMount () {
@@ -233,7 +242,7 @@ export default class TransportAction extends Component {
233
242
 
234
243
  // Check if it's a copy operation to the same path
235
244
  if (fromPath === toPath && operation === fileOperationsMap.cp) {
236
- finalToPath = this.handleRename(toPath, typeFrom === typeMap.remote)
245
+ finalToPath = this.handleRename(toPath, typeFrom === typeMap.remote).newPath
237
246
  transfer.toPath = finalToPath
238
247
  this.update({
239
248
  toPath: finalToPath
@@ -256,13 +265,15 @@ export default class TransportAction extends Component {
256
265
  })
257
266
  }
258
267
 
259
- transferFile = async (transfer = this.props.transfer) => {
268
+ transferFile = async (transfer = this.props.transfer, onEnd = this.onEnd) => {
260
269
  const {
261
270
  fromPath,
262
271
  typeFrom,
263
272
  toFile = {}
264
273
  } = transfer
265
- const toPath = this.newPath || transfer.toPath
274
+ const toPath = transfer.zip
275
+ ? transfer.toPath
276
+ : this.newPath || transfer.toPath
266
277
  const fromFile = transfer.fromFile || this.fromFile
267
278
  const fromMode = fromFile.mode
268
279
  const transferType = typeFrom === typeMap.local ? transferTypeMap.upload : transferTypeMap.download
@@ -281,7 +292,7 @@ export default class TransportAction extends Component {
281
292
  options: { mode },
282
293
  onData: this.onData,
283
294
  onError: this.onError,
284
- onEnd: this.onEnd
295
+ onEnd
285
296
  })
286
297
  }
287
298
 
@@ -384,23 +395,132 @@ export default class TransportAction extends Component {
384
395
  typeTo,
385
396
  toPath
386
397
  } = this.props.transfer
387
- const newPath = this.handleRename(toPath, typeTo === typeMap.remote)
398
+ this.oldPath = toPath
399
+ const { newPath, newName } = this.handleRename(toPath, typeTo === typeMap.remote)
388
400
  this.update({
389
401
  toPath: newPath
390
402
  })
391
403
  this.newPath = newPath
404
+ this.newName = newName
392
405
  }
393
406
 
394
407
  this.startTransfer()
395
408
  }
396
409
 
410
+ zipTransferFolder = async () => {
411
+ const {
412
+ transfer
413
+ } = this.props
414
+ const {
415
+ fromPath,
416
+ typeFrom
417
+ } = transfer
418
+ const toPath = this.oldPath || transfer.toPath
419
+ let p
420
+ let isFromRemote
421
+ if (typeFrom === typeMap.local) {
422
+ isFromRemote = false
423
+ p = await fs.zipFolder(fromPath)
424
+ } else {
425
+ isFromRemote = true
426
+ const terminalId = refs.get('sftp-' + this.tabId)?.terminalId
427
+ p = await zipCmd(terminalId, fromPath)
428
+ }
429
+ this.zipSrc = p
430
+ const { name } = getFolderFromFilePath(p, isFromRemote)
431
+ const { path } = getFolderFromFilePath(toPath, !isFromRemote)
432
+ const nTo = resolve(path, name)
433
+ this.zipPath = nTo
434
+ const newTrans1 = {
435
+ ...copy(transfer),
436
+ toPath: nTo,
437
+ fromPath: p
438
+ }
439
+ this.transferFile(newTrans1, this.unzipFile)
440
+ }
441
+
442
+ unzipFile = async () => {
443
+ const { transfer } = this.props
444
+ const {
445
+ typeTo
446
+ } = transfer
447
+ const toPath = this.zipPath
448
+ const fromPath = this.zipSrc
449
+ const isToRemote = typeTo === typeMap.remote
450
+ const {
451
+ path,
452
+ name,
453
+ targetPath
454
+ } = this.buildUnzipPath(transfer)
455
+ const {
456
+ newName,
457
+ terminalId
458
+ } = this
459
+ if (isToRemote) {
460
+ if (newName) {
461
+ await mkdirCmd(terminalId, path)
462
+ }
463
+ await unzipCmd(terminalId, toPath, path)
464
+ if (newName) {
465
+ const mvFrom = resolve(path, name)
466
+ const mvTo = resolve(targetPath, newName)
467
+ await mvCmd(terminalId, mvFrom, mvTo)
468
+ }
469
+ } else {
470
+ if (newName) {
471
+ await fs.mkdir(path)
472
+ }
473
+ await fs.unzipFile(toPath, path)
474
+ if (newName) {
475
+ const mvFrom = resolve(path, name)
476
+ const mvTo = resolve(targetPath, newName)
477
+ await fs.mv(mvFrom, mvTo)
478
+ }
479
+ }
480
+ await rmCmd(terminalId, !isToRemote ? fromPath : toPath)
481
+ await fs.rmrf(!isToRemote ? toPath : fromPath)
482
+ if (newName) {
483
+ if (isToRemote) {
484
+ await rmCmd(terminalId, path)
485
+ } else {
486
+ await fs.rmrf(path)
487
+ }
488
+ }
489
+ this.onEnd()
490
+ }
491
+
492
+ buildUnzipPath = (transfer) => {
493
+ const {
494
+ typeTo
495
+ } = transfer
496
+ const isToRemote = typeTo === typeMap.remote
497
+ const toPath = this.oldPath || transfer.toPath
498
+ const {
499
+ newName
500
+ } = this
501
+ const { path } = getFolderFromFilePath(toPath, isToRemote)
502
+ const oldName = getFolderFromFilePath(toPath, isToRemote).name
503
+ const np = newName
504
+ ? resolve(path, 'temp-' + newName)
505
+ : path
506
+ return {
507
+ targetPath: path,
508
+ path: np,
509
+ name: oldName
510
+ }
511
+ }
512
+
397
513
  startTransfer = async () => {
398
- const { fromFile = this.fromFile } = this.props.transfer
514
+ const { fromFile = this.fromFile, zip } = this.props.transfer
399
515
 
400
516
  if (!fromFile.isDirectory) {
401
517
  return this.transferFile()
402
518
  }
403
- await this.transferFolderRecursive()
519
+ if (zip) {
520
+ return this.zipTransferFolder()
521
+ } else {
522
+ await this.transferFolderRecursive()
523
+ }
404
524
  this.onEnd({
405
525
  transferred: this.transferred,
406
526
  size: this.total
@@ -415,7 +535,10 @@ export default class TransportAction extends Component {
415
535
  handleRename = (fromPath, isRemote) => {
416
536
  const { path, base, ext } = getFolderFromFilePath(fromPath, isRemote)
417
537
  const newName = `${base}(rename-${generate()})${ext ? '.' + ext : ''}`
418
- return resolve(path, newName)
538
+ return {
539
+ newPath: resolve(path, newName),
540
+ newName
541
+ }
419
542
  }
420
543
 
421
544
  onFolderData = (transferred) => {
@@ -0,0 +1,42 @@
1
+ /**
2
+ * zip/unzip remote files
3
+ * should only support linux server
4
+ */
5
+
6
+ import { runCmd } from '../terminal/terminal-apis'
7
+ import { getFolderFromFilePath } from '../sftp/file-read'
8
+ import resolve from '../../common/resolve'
9
+ import generate from '../../common/uid'
10
+
11
+ const isRemote = true
12
+ const temp = '/tmp'
13
+
14
+ export async function zipCmd (pid, filePath) {
15
+ // tar -czf bin.tar bin
16
+ const id = generate()
17
+ const { path, name } = getFolderFromFilePath(filePath, isRemote)
18
+ const np = resolve(temp, `electerm-${id}.tar`)
19
+ const cmd = `tar -C "${path}" -cf "${np}" "${name}"`
20
+ await runCmd(pid, cmd)
21
+ return np
22
+ }
23
+
24
+ export function unzipCmd (pid, from, to) {
25
+ const cmd = `tar -xf "${from}" -C "${to}"`
26
+ return runCmd(pid, cmd)
27
+ }
28
+
29
+ export async function rmCmd (pid, path) {
30
+ const cmd = `rm -rf "${path}"`
31
+ return runCmd(pid, cmd)
32
+ }
33
+
34
+ export async function mvCmd (pid, from, to) {
35
+ const cmd = `mv "${from}" "${to}"`
36
+ return runCmd(pid, cmd)
37
+ }
38
+
39
+ export async function mkdirCmd (pid, p) {
40
+ const cmd = `mkdir "${p}"`
41
+ return runCmd(pid, cmd)
42
+ }
@@ -795,9 +795,12 @@ export default class FileSection extends React.Component {
795
795
  this.props.addTransferList(all)
796
796
  }
797
797
 
798
- transfer = async () => {
798
+ transfer = async (mapper) => {
799
799
  const { file } = this.state
800
800
  const arr = await this.getTransferList(file)
801
+ if (mapper) {
802
+ arr.forEach(mapper)
803
+ }
801
804
  this.props.addTransferList(arr)
802
805
  }
803
806
 
@@ -835,6 +838,12 @@ export default class FileSection extends React.Component {
835
838
  this.transfer()
836
839
  }
837
840
 
841
+ zipAndTransfer = async () => {
842
+ this.transfer(transfer => {
843
+ transfer.zip = true
844
+ })
845
+ }
846
+
838
847
  newFile = () => {
839
848
  return this.newItem(false)
840
849
  }
@@ -1000,6 +1009,13 @@ export default class FileSection extends React.Component {
1000
1009
  icon: iconType,
1001
1010
  text: transferText
1002
1011
  })
1012
+ if (isDirectory && !this.props.isFtp) {
1013
+ res.push({
1014
+ func: 'zipAndTransfer',
1015
+ icon: 'FileZipOutlined',
1016
+ text: e('compressAndTransfer')
1017
+ })
1018
+ }
1003
1019
  }
1004
1020
  if (!isDirectory && isRealFile && isLocal) {
1005
1021
  res.push({
@@ -22,7 +22,9 @@ import {
22
22
  FolderAddOutlined,
23
23
  InfoCircleOutlined,
24
24
  LockOutlined,
25
- ReloadOutlined
25
+ ReloadOutlined,
26
+ FileZipOutlined,
27
+ AppstoreOutlined
26
28
  } from '@ant-design/icons'
27
29
  import IconHolder from './icon-holder'
28
30
 
@@ -48,5 +50,7 @@ export default {
48
50
  FolderAddOutlined,
49
51
  InfoCircleOutlined,
50
52
  LockOutlined,
51
- ReloadOutlined
53
+ ReloadOutlined,
54
+ FileZipOutlined,
55
+ AppstoreOutlined
52
56
  }
@@ -0,0 +1,57 @@
1
+ import {
2
+ SingleIcon,
3
+ TwoColumnsIcon,
4
+ ThreeColumnsIcon,
5
+ TwoRowsIcon,
6
+ ThreeRowsIcon,
7
+ Grid2x2Icon,
8
+ TwoRowsRightIcon,
9
+ TwoColumnsBottomIcon
10
+ } from '../icons/split-icons'
11
+ import {
12
+ splitMapDesc
13
+ } from '../../common/constants'
14
+
15
+ const e = window.translate
16
+
17
+ export default function LayoutChanger (props) {
18
+ const getLayoutIcon = (layout) => {
19
+ const iconMaps = {
20
+ single: SingleIcon,
21
+ twoColumns: TwoColumnsIcon,
22
+ threeColumns: ThreeColumnsIcon,
23
+ twoRows: TwoRowsIcon,
24
+ threeRows: ThreeRowsIcon,
25
+ grid2x2: Grid2x2Icon,
26
+ twoRowsRight: TwoRowsRightIcon,
27
+ twoColumnsBottom: TwoColumnsBottomIcon
28
+ }
29
+ return iconMaps[layout]
30
+ }
31
+
32
+ const handleChangeLayout = ({ key }) => {
33
+ window.store.setLayout(key)
34
+ }
35
+
36
+ const items = Object.keys(splitMapDesc).map((t) => {
37
+ const v = splitMapDesc[t]
38
+ const Icon = getLayoutIcon(v)
39
+ return (
40
+ <div
41
+ key={t}
42
+ className='sub-context-menu-item'
43
+ onClick={() => handleChangeLayout({ key: t })}
44
+ >
45
+ <span>
46
+ <Icon /> {e(v)}
47
+ </span>
48
+ </div>
49
+ )
50
+ })
51
+
52
+ return (
53
+ <div className='sub-context-menu bookmarks-sub-context-menu'>
54
+ {items}
55
+ </div>
56
+ )
57
+ }
@@ -106,6 +106,11 @@ class MenuBtn extends PureComponent {
106
106
  text: e('sessions'),
107
107
  submenu: 'Tabs'
108
108
  },
109
+ {
110
+ icon: 'AppstoreOutlined',
111
+ text: e('layout'),
112
+ submenu: 'Layout'
113
+ },
109
114
  // {
110
115
  // type: 'hr'
111
116
  // },
@@ -10,6 +10,7 @@ import {
10
10
  import { noop } from 'lodash-es'
11
11
  import History from './history'
12
12
  import Bookmark from './boomarks'
13
+ import Layout from './layoout-changer'
13
14
  import Tabs from './tabs'
14
15
  import Zoom from './zoom'
15
16
  import icons from './icons-map'
@@ -22,7 +23,8 @@ export default class ContextMenu extends PureComponent {
22
23
  History,
23
24
  Bookmark,
24
25
  Tabs,
25
- Zoom
26
+ Zoom,
27
+ Layout
26
28
  }
27
29
 
28
30
  onClick = (e, item) => {
@@ -12,7 +12,8 @@ import {
12
12
  Spin,
13
13
  Button,
14
14
  Dropdown,
15
- message
15
+ message,
16
+ Modal
16
17
  } from 'antd'
17
18
  import classnames from 'classnames'
18
19
  import './terminal.styl'
@@ -28,7 +29,7 @@ import {
28
29
  zmodemTransferPackSize
29
30
  } from '../../common/constants.js'
30
31
  import deepCopy from 'json-deep-copy'
31
- import { readClipboardAsync, copy } from '../../common/clipboard.js'
32
+ import { readClipboardAsync, readClipboard, copy } from '../../common/clipboard.js'
32
33
  import { FitAddon } from '@xterm/addon-fit'
33
34
  import AttachAddon from './attach-addon-custom.js'
34
35
  import { SearchAddon } from '@xterm/addon-search'
@@ -289,7 +290,39 @@ clear\r`
289
290
  this.tryInsertSelected()
290
291
  }
291
292
 
293
+ pasteTextTooLong = () => {
294
+ if (window.et.isWebApp) {
295
+ return false
296
+ }
297
+ const text = readClipboard()
298
+ return text.length > 500
299
+ }
300
+
301
+ askUserConfirm = () => {
302
+ Modal.confirm({
303
+ title: e('paste'),
304
+ content: (
305
+ <div>
306
+ <p>{e('paste')}:</p>
307
+ <div className='paste-text'>
308
+ {readClipboard()}
309
+ </div>
310
+ </div>
311
+ ),
312
+ okText: e('ok'),
313
+ cancelText: e('cancel'),
314
+ onOk: () => this.onPaste(true),
315
+ onCancel: Modal.destroyAll
316
+ })
317
+ }
318
+
292
319
  pasteShortcut = (e) => {
320
+ if (this.pasteTextTooLong()) {
321
+ this.askUserConfirm()
322
+ e.preventDefault()
323
+ e.stopPropagation()
324
+ return false
325
+ }
293
326
  if (isMac) {
294
327
  return true
295
328
  }
@@ -706,8 +739,11 @@ clear\r`
706
739
  return this.props.tab?.host
707
740
  }
708
741
 
709
- onPaste = async () => {
742
+ onPaste = async (skipTextLengthCheck) => {
710
743
  let selected = await readClipboardAsync()
744
+ if (!skipTextLengthCheck && selected.length > 500) {
745
+ return this.askUserConfirm()
746
+ }
711
747
  if (isWin && this.isRemote()) {
712
748
  selected = selected.replace(/\r\n/g, '\n')
713
749
  }
@@ -143,3 +143,6 @@
143
143
  bottom 0
144
144
  background transparent
145
145
  width 16px
146
+ .paste-text
147
+ max-height 200px
148
+ overflow-y scroll
@@ -26,11 +26,11 @@ export default function ThemeListItem (props) {
26
26
  const { store } = window
27
27
 
28
28
  function handleClickApply () {
29
+ delete window.originalTheme
29
30
  store.setTheme(item.id)
30
31
  }
31
32
 
32
33
  function handleMouseEnter () {
33
- if (!item.id) return
34
34
  // Store current theme ID before changing
35
35
  const currentTheme = window.store.config.theme
36
36
  window.originalTheme = currentTheme
@@ -3,12 +3,15 @@
3
3
  */
4
4
 
5
5
  import List from '../setting-panel/list'
6
- import { LoadingOutlined } from '@ant-design/icons'
6
+ import { LoadingOutlined, CheckCircleOutlined } from '@ant-design/icons'
7
7
  import { pick } from 'lodash-es'
8
8
  import { Pagination } from 'antd'
9
9
  import ThemeListItem from './theme-list-item'
10
+ import { defaultTheme } from '../../common/constants'
10
11
  import './terminal-theme-list.styl'
11
12
 
13
+ const e = window.translate
14
+
12
15
  export default class ThemeList extends List {
13
16
  handlePager = page => {
14
17
  this.setState({ page })
@@ -37,6 +40,24 @@ export default class ThemeList extends List {
37
40
  )
38
41
  }
39
42
 
43
+ renderCurrentTheme () {
44
+ const { theme, list } = this.props
45
+ const item = list.find(d => d.id === theme)
46
+ if (!item) {
47
+ return null
48
+ }
49
+ const { name, id } = item
50
+ const title = id === defaultTheme.id
51
+ ? e(id)
52
+ : name
53
+ return (
54
+ <div className='pd2'>
55
+ <CheckCircleOutlined className='mg1r' />
56
+ {title}
57
+ </div>
58
+ )
59
+ }
60
+
40
61
  filter = list => {
41
62
  const { keyword, ready } = this.state
42
63
  return keyword
@@ -69,6 +90,7 @@ export default class ThemeList extends List {
69
90
  {this.renderTransport ? this.renderTransport() : null}
70
91
  {this.renderLabels ? this.renderLabels() : null}
71
92
  {this.renderSearch()}
93
+ {this.renderCurrentTheme()}
72
94
  <div className='item-list-wrap' style={listStyle}>
73
95
  {
74
96
  list.map(this.renderItem)
@@ -66,7 +66,7 @@ export default Store => {
66
66
  return deepCopy([
67
67
  ...store.getTerminalThemes(),
68
68
  ...store.itermThemes
69
- ]).sort(window.store.sortTheme)
69
+ ])
70
70
  }
71
71
  return deepCopy(store.getItems(type))
72
72
  }
@@ -37,12 +37,12 @@ export default Store => {
37
37
  return window.pre.runGlobalAsync('toCss', stylus)
38
38
  }
39
39
 
40
- Store.prototype.sortTheme = function (a, b) {
41
- const theme = window.originalTheme || window.store.config.theme
42
- const ax = a.id === theme ? -1 : 1
43
- const bx = b.id === theme ? -1 : 1
44
- return ax - bx
45
- }
40
+ // Store.prototype.sortTheme = function (a, b) {
41
+ // const theme = window.originalTheme || window.store.config.theme
42
+ // const ax = a.id === theme ? -1 : 1
43
+ // const bx = b.id === theme ? -1 : 1
44
+ // return ax > bx ? 1 : -1
45
+ // }
46
46
 
47
47
  Store.prototype.getUiThemeConfig = function () {
48
48
  const { store } = window
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "1.90.6",
3
+ "version": "1.91.1",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",