@electerm/electerm-react 3.15.110 → 3.15.121

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.
@@ -2,12 +2,12 @@
2
2
  * check event if control/meta key pressed
3
3
  */
4
4
 
5
- import { isMac } from './constants'
5
+ import { isMacJs } from './constants'
6
6
  export default e => {
7
7
  if (e.metaKey && e.ctrlKey) {
8
8
  return false
9
9
  }
10
- return isMac
10
+ return isMacJs
11
11
  ? e.metaKey
12
12
  : e.ctrlKey
13
13
  }
@@ -10,11 +10,14 @@ export default function normalizeRemotePath (path) {
10
10
  if (!path) return path
11
11
  // Fix mixed separators: /c:\windows → /c:/windows
12
12
  if (/^\/[a-zA-Z]:\\/.test(path)) {
13
- return path.replace(/\\/g, '/')
13
+ path = path.replace(/\\/g, '/')
14
+ } else if (/^[a-zA-Z]:/.test(path)) {
15
+ // Add leading / to bare drive letters: c: → /c:, c:\windows → /c:/windows
16
+ path = '/' + path.replace(/\\/g, '/')
14
17
  }
15
- // Add leading / to bare drive letters: c: → /c:, c:\windows → /c:/windows
16
- if (/^[a-zA-Z]:/.test(path)) {
17
- return '/' + path.replace(/\\/g, '/')
18
+ // Strip trailing slashes except for root path "/"
19
+ if (path.length > 1 && path.endsWith('/')) {
20
+ path = path.replace(/\/+$/, '')
18
21
  }
19
22
  return path
20
23
  }
@@ -36,3 +36,20 @@
36
36
  overflow auto
37
37
  font-size 12px
38
38
  font-family monospace
39
+
40
+ // Flash the add/save button when the form has unsaved edits,
41
+ // so users remember to click it instead of losing their input.
42
+ .btn-flash
43
+ animation add-btn-flash 1.1s ease-in-out infinite
44
+
45
+ @keyframes add-btn-flash
46
+ 0%, 100%
47
+ box-shadow 0 0 0 0 var(--primary)
48
+ transform scale(1)
49
+ 50%
50
+ box-shadow 0 0 8px 2px var(--primary)
51
+ transform scale(1.04)
52
+
53
+ @media (prefers-reduced-motion: reduce)
54
+ .btn-flash
55
+ animation none
@@ -36,6 +36,7 @@ export default function ConnectionHoppingForm (props) {
36
36
  isEdit
37
37
  } = props
38
38
  const [authType, setAuthType] = useState(initialValues.authType || authTypeMap.password)
39
+ const [dirty, setDirty] = useState(false)
39
40
 
40
41
  function onChangeAuthType (e) {
41
42
  setAuthType(e.target.value)
@@ -45,6 +46,17 @@ export default function ConnectionHoppingForm (props) {
45
46
  formChild.submit()
46
47
  }
47
48
 
49
+ // mark the form as edited so the add/save button can flash as a reminder
50
+ function handleValuesChange () {
51
+ setDirty(true)
52
+ }
53
+
54
+ // called on a successful (validated) submit; clears the flash
55
+ function onFinishLocal (data) {
56
+ setDirty(false)
57
+ onFinish(data)
58
+ }
59
+
48
60
  const authTypes = authTypesProp || Object.keys(authTypeMap).map(k => k)
49
61
 
50
62
  const treeProps = {
@@ -52,13 +64,14 @@ export default function ConnectionHoppingForm (props) {
52
64
  return d.host && d.port && d.username
53
65
  }),
54
66
  bookmarkGroups: store.bookmarkGroups,
55
- onSelect: onFinish
67
+ onSelect: onFinishLocal
56
68
  }
57
69
 
58
70
  return (
59
71
  <Form
60
72
  form={formChild}
61
- onFinish={onFinish}
73
+ onFinish={onFinishLocal}
74
+ onValuesChange={handleValuesChange}
62
75
  initialValues={initialValues}
63
76
  component='div'
64
77
  >
@@ -144,6 +157,7 @@ export default function ConnectionHoppingForm (props) {
144
157
  htmlType='button'
145
158
  icon={isEdit ? <SaveOutlined /> : <PlusOutlined />}
146
159
  onClick={onSubmit}
160
+ className={dirty ? 'btn-flash' : ''}
147
161
  >
148
162
  {isEdit ? e('save') : e('connectionHopping')}
149
163
  </Button>
@@ -34,6 +34,7 @@ export default function SshTunnelForm (props) {
34
34
  const [isDynamic, setIsDynamic] = useState(
35
35
  (initialValues?.sshTunnel || 'forwardRemoteToLocal') === 'dynamicForward'
36
36
  )
37
+ const [dirty, setDirty] = useState(false)
37
38
 
38
39
  function onChange (ev) {
39
40
  setIsDynamic(ev.target.value === 'dynamicForward')
@@ -43,6 +44,17 @@ export default function SshTunnelForm (props) {
43
44
  formChild.submit()
44
45
  }
45
46
 
47
+ // mark the form as edited so the add/save button can flash as a reminder
48
+ function handleValuesChange () {
49
+ setDirty(true)
50
+ }
51
+
52
+ // called on a successful (validated) submit; clears the flash
53
+ function onFinishLocal (data) {
54
+ setDirty(false)
55
+ onFinish(data)
56
+ }
57
+
46
58
  function renderSshTunnelFlow (direction) {
47
59
  const localToRemote = direction === 'localToRemote'
48
60
  const middle = localToRemote ? e('local') : e('remote')
@@ -101,7 +113,8 @@ export default function SshTunnelForm (props) {
101
113
  return (
102
114
  <Form
103
115
  form={formChild}
104
- onFinish={onFinish}
116
+ onFinish={onFinishLocal}
117
+ onValuesChange={handleValuesChange}
105
118
  initialValues={initialValues}
106
119
  component='div'
107
120
  >
@@ -174,6 +187,7 @@ export default function SshTunnelForm (props) {
174
187
  htmlType='button'
175
188
  icon={isEdit ? <SaveOutlined /> : <PlusOutlined />}
176
189
  onClick={onSubmit}
190
+ className={dirty ? 'btn-flash' : ''}
177
191
  >
178
192
  {isEdit ? e('save') : e('sshTunnel')}
179
193
  </Button>
@@ -10,6 +10,7 @@ import {
10
10
  getFolderFromFilePath
11
11
  } from '../sftp/file-read'
12
12
  import resolve from '../../common/resolve'
13
+ import sanitizeFilename from '../../common/sanitize-filename'
13
14
  import { refsTransfers, refsStatic, refs } from '../common/ref'
14
15
  import {
15
16
  zipCmd,
@@ -672,7 +673,7 @@ export default class TransportAction extends Component {
672
673
  }
673
674
 
674
675
  const fromItemPath = resolve(fromPath, file.name)
675
- const toItemPath = resolve(toPath, file.name)
676
+ const toItemPath = resolve(toPath, sanitizeFilename(file.name))
676
677
 
677
678
  const itemTransfer = {
678
679
  ...transfer,
@@ -711,7 +712,7 @@ export default class TransportAction extends Component {
711
712
 
712
713
  const batchFolders = folders.slice(i, i + batch)
713
714
  const createFolderPromises = batchFolders.map(folder => {
714
- const toItemPath = resolve(toPath, folder.name)
715
+ const toItemPath = resolve(toPath, sanitizeFilename(folder.name))
715
716
 
716
717
  // Create folder itself (don't process contents)
717
718
  const createTransfer = {
@@ -734,7 +735,7 @@ export default class TransportAction extends Component {
734
735
  }
735
736
 
736
737
  const fromItemPath = resolve(fromPath, folder.name)
737
- const toItemPath = resolve(toPath, folder.name)
738
+ const toItemPath = resolve(toPath, sanitizeFilename(folder.name))
738
739
 
739
740
  const itemTransfer = {
740
741
  ...transfer,
@@ -2,6 +2,7 @@ import { auto } from 'manate/react'
2
2
  import { useEffect } from 'react'
3
3
  import Layout from '../layout/layout'
4
4
  import FileInfoModal from '../sftp/file-info-modal'
5
+ import FileCompareModal from '../sftp/file-compare-modal'
5
6
  import UpdateCheck from './upgrade'
6
7
  import SettingModal from '../setting-panel/setting-modal'
7
8
  import TextEditor from '../text-editor/text-editor-entry'
@@ -263,6 +264,7 @@ export default auto(function Index (props) {
263
264
  installSrc={installSrc}
264
265
  />
265
266
  <FileInfoModal />
267
+ <FileCompareModal />
266
268
  <SettingModal store={store} />
267
269
  <MoveItemModal store={store} />
268
270
  <div
@@ -6,6 +6,7 @@
6
6
  import { getLocalFileInfo } from '../sftp/file-read'
7
7
  import { osResolve } from '../../common/resolve'
8
8
  import { filesize } from 'filesize'
9
+ import sanitizeFilename from '../../common/sanitize-filename'
9
10
 
10
11
  const LOG_PREFIX = '[RDP-FILE-TRANSFER]'
11
12
 
@@ -245,7 +246,8 @@ export class FileTransferManager {
245
246
  return
246
247
  }
247
248
 
248
- const fullPath = osResolve(savePath[0], fileInfo.name)
249
+ const safeName = sanitizeFilename(fileInfo.name)
250
+ const fullPath = osResolve(savePath[0], safeName)
249
251
 
250
252
  const fd = await new Promise((resolve, reject) => {
251
253
  window.fs.open(fullPath, O_WRONLY | O_CREAT | O_TRUNC, (err, fd) => {
@@ -272,12 +274,12 @@ export class FileTransferManager {
272
274
  })
273
275
  })
274
276
 
275
- this.log(`Downloaded ${fileInfo.name} (${filesize(fileInfo._totalSize)}) to ${fullPath}`, 'success')
277
+ this.log(`Downloaded ${safeName} (${filesize(fileInfo._totalSize)}) to ${fullPath}`, 'success')
276
278
  this.hasRemoteFiles = false
277
279
  this.pendingDownloads.clear()
278
280
  this.notifyStateChange()
279
281
  if (this.onDownloadComplete) {
280
- this.onDownloadComplete(fullPath, fileInfo.name, fileInfo._totalSize)
282
+ this.onDownloadComplete(fullPath, safeName, fileInfo._totalSize)
281
283
  }
282
284
  } catch (e) {
283
285
  this.log(`Failed to save file: ${e.message}`, 'error')
@@ -56,7 +56,9 @@ export default class SessionWrapper extends Component {
56
56
  broadcastInput: false,
57
57
  keepaliveEnabled: false
58
58
  }
59
- props.tab.sshSftpSplitView = !!props.config.sshSftpSplitView
59
+ if (props.tab.sshSftpSplitView === undefined) {
60
+ props.tab.sshSftpSplitView = !!props.config.sshSftpSplitView
61
+ }
60
62
  }
61
63
 
62
64
  minWithForSplit = 640
@@ -547,6 +547,7 @@ export default class SettingCommon extends Component {
547
547
 
548
548
  <div className='pd2b'>
549
549
  <span className='inline-title mg1r'>{e('customCss')}</span>
550
+ <HelpIcon link='https://github.com/electerm/electerm/wiki/Custom-CSS-examples' />
550
551
  <TextareaConfirm
551
552
  onChange={this.handleCustomCss}
552
553
  value={customCss}
@@ -0,0 +1,145 @@
1
+ /**
2
+ * file compare modal - shows side by side comparison of two files' attributes
3
+ */
4
+
5
+ import React from 'react'
6
+ import Modal from '../common/modal'
7
+ import resolve from '../../common/resolve'
8
+ import time from '../../common/time'
9
+ import { mode2permission, permission2mode } from '../../common/mode2permission'
10
+ import FileIcon from './file-icon'
11
+ import { filesize } from 'filesize'
12
+ import { refsStatic } from '../common/ref'
13
+ import './file-compare-modal.styl'
14
+
15
+ const e = window.translate
16
+ const formatTime = time
17
+
18
+ export default class FileCompareModal extends React.PureComponent {
19
+ state = {
20
+ visible: false,
21
+ file1: {},
22
+ file2: {},
23
+ tab: null,
24
+ uidTree: {},
25
+ gidTree: {}
26
+ }
27
+
28
+ componentDidMount () {
29
+ refsStatic.add('file-compare-modal', this)
30
+ }
31
+
32
+ showFileCompareModal (data) {
33
+ this.setState({
34
+ ...data,
35
+ visible: true
36
+ })
37
+ }
38
+
39
+ onClose = () => {
40
+ this.setState({
41
+ file1: {},
42
+ file2: {},
43
+ visible: false
44
+ })
45
+ }
46
+
47
+ render () {
48
+ const {
49
+ visible,
50
+ file1,
51
+ file2,
52
+ tab = {},
53
+ uidTree = {},
54
+ gidTree = {}
55
+ } = this.state
56
+ if (!visible) {
57
+ return null
58
+ }
59
+ const {
60
+ host,
61
+ port,
62
+ username
63
+ } = tab
64
+ const title = e('compare')
65
+ const ps = {
66
+ open: visible,
67
+ width: 800,
68
+ title,
69
+ footer: null,
70
+ onCancel: this.onClose
71
+ }
72
+
73
+ const fp1 = resolve(file1.path, file1.name)
74
+ const fp2 = resolve(file2.path, file2.name)
75
+ const ffp1 = file1.type === 'local'
76
+ ? fp1
77
+ : `${username}@${host}:${port}:${fp1}`
78
+ const ffp2 = file2.type === 'local'
79
+ ? fp2
80
+ : `${username}@${host}:${port}:${fp2}`
81
+
82
+ const perms1 = mode2permission(file1.mode)
83
+ const perms2 = mode2permission(file2.mode)
84
+ const permission1 = permission2mode(perms1)
85
+ const permission2 = permission2mode(perms2)
86
+
87
+ const rows = [
88
+ [
89
+ e('name'),
90
+ (
91
+ <React.Fragment key='name1'>
92
+ <FileIcon file={file1} height={20} />
93
+ {' '}
94
+ {file1.isSymbolicLink ? <sup className='color-blue symbolic-link-icon'>*</sup> : null}
95
+ {file1.name}
96
+ </React.Fragment>
97
+ ),
98
+ (
99
+ <React.Fragment key='name2'>
100
+ <FileIcon file={file2} height={20} />
101
+ {' '}
102
+ {file2.isSymbolicLink ? <sup className='color-blue symbolic-link-icon'>*</sup> : null}
103
+ {file2.name}
104
+ </React.Fragment>
105
+ )
106
+ ],
107
+ [
108
+ e('type'),
109
+ file1.isDirectory ? e('folder') : e('file'),
110
+ file2.isDirectory ? e('folder') : e('file')
111
+ ],
112
+ [
113
+ e('size'),
114
+ file1.isDirectory ? '-' : filesize(file1.size || 0),
115
+ file2.isDirectory ? '-' : filesize(file2.size || 0)
116
+ ],
117
+ [e('mode'), permission1, permission2],
118
+ [e('fullPath'), ffp1, ffp2],
119
+ [e('owner'), uidTree['' + file1.owner] || file1.owner, uidTree['' + file2.owner] || file2.owner],
120
+ [e('group'), gidTree['' + file1.group] || file1.group, gidTree['' + file2.group] || file2.group],
121
+ [e('accessTime'), formatTime(file1.accessTime), formatTime(file2.accessTime)],
122
+ [e('modifyTime'), formatTime(file1.modifyTime), formatTime(file2.modifyTime)]
123
+ ]
124
+
125
+ return (
126
+ <Modal {...ps}>
127
+ <table className='file-compare-table'>
128
+ <tbody>
129
+ {rows.map(([label, v1, v2], i) => {
130
+ const same = String(v1 ?? '') === String(v2 ?? '')
131
+ const cls = same ? '' : 'file-compare-diff'
132
+ return (
133
+ <tr key={i}>
134
+ <td className='bold file-compare-label'>{label}</td>
135
+ <td className={cls}>{v1}</td>
136
+ <td className={cls}>{v2}</td>
137
+ </tr>
138
+ )
139
+ })}
140
+ </tbody>
141
+ </table>
142
+ </Modal>
143
+ )
144
+ }
145
+ }
@@ -0,0 +1,13 @@
1
+ .file-compare-table
2
+ border-collapse collapse
3
+ width 100%
4
+ text-align left
5
+ td
6
+ border 1px solid var(--main-darker)
7
+ padding 4px 8px
8
+
9
+ .file-compare-label
10
+ width 120px
11
+
12
+ .file-compare-diff
13
+ color var(--warn)
@@ -123,6 +123,13 @@ export default class FileMode extends React.PureComponent {
123
123
  return String(value).replace(/'/g, "''")
124
124
  }
125
125
 
126
+ escapePosixShellArg = (value) => {
127
+ // Within single quotes, the only special character is the single quote itself.
128
+ // Escape it by closing the quote, inserting an escaped quote, and reopening:
129
+ // ' -> '\''
130
+ return String(value).replace(/'/g, "'\\''")
131
+ }
132
+
126
133
  normalizeRemoteWindowsPath = (value) => {
127
134
  return String(value).replace(/^\/([a-zA-Z]:)/, '$1')
128
135
  }
@@ -161,9 +168,12 @@ export default class FileMode extends React.PureComponent {
161
168
  }
162
169
 
163
170
  calcLocal = async (folder) => {
171
+ const safeFolder = isWin
172
+ ? this.escapePowerShellPath(folder)
173
+ : this.escapePosixShellArg(folder)
164
174
  const cmd = isWin
165
- ? `Get-ChildItem -Recurse '${folder}' | Measure-Object -Property Length -Sum`
166
- : `du -sh '${folder}'`
175
+ ? `Get-ChildItem -Recurse '${safeFolder}' | Measure-Object -Property Length -Sum`
176
+ : `du -sh '${safeFolder}'`
167
177
  const func = isWin ? 'runWinCmd' : 'run'
168
178
  const res = await window.fs[func](cmd).catch(window.store.onError)
169
179
  return this.getSize(res)
@@ -174,7 +184,8 @@ export default class FileMode extends React.PureComponent {
174
184
  if (platform === 'windows') {
175
185
  return this.calcRemoteWin(folder)
176
186
  }
177
- const cmd = `du -sh '${folder}'`
187
+ const safeFolder = this.escapePosixShellArg(folder)
188
+ const cmd = `du -sh '${safeFolder}'`
178
189
  const r = await runCmd(
179
190
  this.state.pid,
180
191
  cmd
@@ -25,7 +25,7 @@ import {
25
25
  fileOperationsMap,
26
26
  isWin, transferTypeMap, typeMap,
27
27
  paneMap,
28
- isMac, maxEditFileSize, ctrlOrCmd
28
+ isMacJs, maxEditFileSize, ctrlOrCmd
29
29
  } from '../../common/constants'
30
30
  import sorter from '../../common/index-sorter'
31
31
  import { getFolderFromFilePath, getLocalFileInfo } from './file-read'
@@ -38,6 +38,7 @@ import generate from '../../common/uid'
38
38
  import sanitizeFilename from '../../common/sanitize-filename'
39
39
  import { refsStatic, refs, filesRef } from '../common/ref'
40
40
  import iconsMap from '../sys-menu/icons-map'
41
+ import message from '../common/message'
41
42
 
42
43
  const e = window.translate
43
44
 
@@ -377,6 +378,46 @@ export default class FileSection extends React.Component {
377
378
  })
378
379
  }
379
380
 
381
+ getExt = (name = '') => {
382
+ const parts = String(name).split('.')
383
+ if (parts.length < 2) {
384
+ return ''
385
+ }
386
+ return parts[parts.length - 1].toLowerCase()
387
+ }
388
+
389
+ showCompare = () => {
390
+ const { type } = this.props
391
+ const selected = this.props.getSelectedFiles().filter(f => !f.isDirectory)
392
+ if (selected.length !== 2) {
393
+ return
394
+ }
395
+ refsStatic.get('file-compare-modal')?.showFileCompareModal({
396
+ file1: selected[0],
397
+ file2: selected[1],
398
+ tab: this.props.tab,
399
+ uidTree: this.props[`${type}UidTree`],
400
+ gidTree: this.props[`${type}GidTree`]
401
+ })
402
+ }
403
+
404
+ canCompare = () => {
405
+ const { selectedFiles } = this.props
406
+ if (!selectedFiles || selectedFiles.size !== 2) {
407
+ return false
408
+ }
409
+ const selected = this.props.getSelectedFiles()
410
+ if (selected.length !== 2) {
411
+ return false
412
+ }
413
+ if (selected.some(f => f.isDirectory || !f.id)) {
414
+ return false
415
+ }
416
+ const ext1 = this.getExt(selected[0].name)
417
+ const ext2 = this.getExt(selected[1].name)
418
+ return ext1 === ext2
419
+ }
420
+
380
421
  cancelNew = (type) => {
381
422
  let list = this.props[type]
382
423
  list = list.filter(p => p.id)
@@ -467,8 +508,8 @@ export default class FileSection extends React.Component {
467
508
  let selectedFiles = [file]
468
509
  if (isSameSide) {
469
510
  if (
470
- (e.ctrlKey && !isMac) ||
471
- (e.metaKey && isMac)
511
+ (e.ctrlKey && !isMacJs) ||
512
+ (e.metaKey && isMacJs)
472
513
  ) {
473
514
  const isSelected = some(
474
515
  selectedFilesOld,
@@ -617,9 +658,15 @@ export default class FileSection extends React.Component {
617
658
  tempPath = window.pre.resolve(path, name)
618
659
  } else {
619
660
  const id = generate()
661
+ const safeName = sanitizeFilename(name)
620
662
  tempPath = window.pre.resolve(
621
- window.pre.tempDir, `electerm-temp-${id}-${name}`
663
+ window.pre.tempDir, `electerm-temp-${id}-${safeName}`
622
664
  )
665
+ // Defense-in-depth: verify the resolved path stays within tempDir
666
+ if (!tempPath.startsWith(window.pre.tempDir + window.pre.sep)) {
667
+ message.error(e('invalidTempFilePath'))
668
+ return
669
+ }
623
670
  await window.fs.writeFile(tempPath, text)
624
671
  }
625
672
  this.watchingFile = tempPath
@@ -637,9 +684,15 @@ export default class FileSection extends React.Component {
637
684
  tempPath = window.pre.resolve(path, name)
638
685
  } else {
639
686
  const id = generate()
687
+ const safeName = sanitizeFilename(name)
640
688
  tempPath = window.pre.resolve(
641
- window.pre.tempDir, `electerm-temp-${id}-${name}`
689
+ window.pre.tempDir, `electerm-temp-${id}-${safeName}`
642
690
  )
691
+ // Defense-in-depth: verify the resolved path stays within tempDir
692
+ if (!tempPath.startsWith(window.pre.tempDir + window.pre.sep)) {
693
+ message.error(e('invalidTempFilePath'))
694
+ return
695
+ }
643
696
  await window.fs.writeFile(tempPath, text)
644
697
  }
645
698
  this.watchingFile = tempPath
@@ -1020,7 +1073,7 @@ export default class FileSection extends React.Component {
1020
1073
  res.push({
1021
1074
  func: 'doTransferSelected',
1022
1075
  icon: iconType,
1023
- text: `${e('selected')}(${len})`
1076
+ text: `${transferText}:${e('selected')}(${len})`
1024
1077
  })
1025
1078
  }
1026
1079
  if (
@@ -1158,6 +1211,13 @@ export default class FileSection extends React.Component {
1158
1211
  text: e('info')
1159
1212
  })
1160
1213
  }
1214
+ if (this.canCompare()) {
1215
+ res.push({
1216
+ func: 'showCompare',
1217
+ icon: 'SwapOutlined',
1218
+ text: e('compare')
1219
+ })
1220
+ }
1161
1221
  return res
1162
1222
  }
1163
1223
 
@@ -1027,6 +1027,11 @@ export default class Sftp extends Component {
1027
1027
  let np = await this.parsePath(type, this.state[nt])
1028
1028
  if (type === typeMap.remote) {
1029
1029
  np = normalizeRemotePath(np)
1030
+ } else if (np.length > 1 && np.endsWith('/')) {
1031
+ // Strip trailing slash except for root path and Windows drive roots
1032
+ if (!/^[a-zA-Z]:\/$/.test(np)) {
1033
+ np = np.replace(/\/+$/, '')
1034
+ }
1030
1035
  }
1031
1036
  if (!isValidPath(np)) {
1032
1037
  return notification.warning({
@@ -28,7 +28,8 @@ import {
28
28
  SaveOutlined,
29
29
  PlayCircleFilled,
30
30
  StopOutlined,
31
- DownloadOutlined
31
+ DownloadOutlined,
32
+ SwapOutlined
32
33
  } from '@ant-design/icons'
33
34
  import IconHolder from './icon-holder'
34
35
 
@@ -60,5 +61,6 @@ export default {
60
61
  SaveOutlined,
61
62
  PlayCircleFilled,
62
63
  StopOutlined,
63
- DownloadOutlined
64
+ DownloadOutlined,
65
+ SwapOutlined
64
66
  }
@@ -25,7 +25,8 @@ export default Store => {
25
25
  // Store srcId (bookmark id) if available, otherwise store basic connection info
26
26
  if (tab.srcId) {
27
27
  tabsByBatch[batch].push({
28
- srcId: tab.srcId
28
+ srcId: tab.srcId,
29
+ sshSftpSplitView: tab.sshSftpSplitView
29
30
  })
30
31
  }
31
32
  }
@@ -82,6 +83,12 @@ export default Store => {
82
83
  // Open from bookmark
83
84
  window.openTabBatch = batch
84
85
  store.onSelectBookmark(tabInfo.srcId)
86
+ // Apply saved sshSftpSplitView state to the newly created tab
87
+ if (tabInfo.sshSftpSplitView !== undefined) {
88
+ store.updateTab(store.activeTabId, {
89
+ sshSftpSplitView: tabInfo.sshSftpSplitView
90
+ })
91
+ }
85
92
  }
86
93
  }
87
94
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "3.15.110",
3
+ "version": "3.15.121",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",