@electerm/electerm-react 1.100.6 → 1.100.18
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.
- package/client/components/quick-commands/quick-commands-form-elem.jsx +79 -64
- package/client/components/quick-commands/templates.js +5 -0
- package/client/components/sftp/address-bookmark-item.jsx +8 -1
- package/client/components/sftp/address-bookmark.jsx +35 -8
- package/client/components/sftp/file-item.jsx +21 -25
- package/client/components/sftp/sftp-entry.jsx +70 -32
- package/client/components/terminal/terminal.jsx +49 -27
- package/client/store/quick-command.js +34 -1
- package/package.json +1 -1
|
@@ -16,6 +16,8 @@ import deepCopy from 'json-deep-copy'
|
|
|
16
16
|
import {
|
|
17
17
|
isMacJs as isMac
|
|
18
18
|
} from '../../common/constants.js'
|
|
19
|
+
import templates from './templates'
|
|
20
|
+
import HelpIcon from '../common/help-icon'
|
|
19
21
|
|
|
20
22
|
const FormItem = Form.Item
|
|
21
23
|
const { Option } = Select
|
|
@@ -130,72 +132,85 @@ export default function QuickCommandForm (props) {
|
|
|
130
132
|
handleClear,
|
|
131
133
|
renderClear: true
|
|
132
134
|
}
|
|
135
|
+
const templatesStr = templates.map(t => {
|
|
136
|
+
return `{{${t}}}`
|
|
137
|
+
}).join(', ')
|
|
138
|
+
const wiki = 'https://github.com/electerm/electerm/wiki/quick-command-templates'
|
|
133
139
|
return (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
hasFeedback
|
|
149
|
-
name='name'
|
|
140
|
+
<>
|
|
141
|
+
<p>
|
|
142
|
+
<b className='mg1r'>{e('templates')}:</b>
|
|
143
|
+
<span className='mg1r'>{templatesStr}</span>
|
|
144
|
+
<HelpIcon
|
|
145
|
+
link={wiki}
|
|
146
|
+
/>
|
|
147
|
+
</p>
|
|
148
|
+
<Form
|
|
149
|
+
form={form}
|
|
150
|
+
onFinish={handleSubmit}
|
|
151
|
+
className='form-wrap pd2l'
|
|
152
|
+
layout='vertical'
|
|
153
|
+
initialValues={initialValues}
|
|
150
154
|
>
|
|
151
|
-
<
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
155
|
+
<FormItem
|
|
156
|
+
label={e('quickCommandName')}
|
|
157
|
+
rules={[{
|
|
158
|
+
max: 60, message: '60 chars max'
|
|
159
|
+
}, {
|
|
160
|
+
required: true, message: 'Name required'
|
|
161
|
+
}]}
|
|
162
|
+
hasFeedback
|
|
163
|
+
name='name'
|
|
160
164
|
>
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
</
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
165
|
+
<InputAutoFocus />
|
|
166
|
+
</FormItem>
|
|
167
|
+
{renderQm()}
|
|
168
|
+
<FormItem
|
|
169
|
+
name='labels'
|
|
170
|
+
label={e('label')}
|
|
171
|
+
>
|
|
172
|
+
<Select
|
|
173
|
+
mode='tags'
|
|
174
|
+
>
|
|
175
|
+
{
|
|
176
|
+
quickCommandTags.map(q => {
|
|
177
|
+
return (
|
|
178
|
+
<Option value={q} key={'qmt-' + q}>
|
|
179
|
+
{q}
|
|
180
|
+
</Option>
|
|
181
|
+
)
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
</Select>
|
|
185
|
+
</FormItem>
|
|
186
|
+
<FormItem
|
|
187
|
+
label={e('settingShortcuts')}
|
|
188
|
+
name='shortcut'
|
|
189
|
+
>
|
|
190
|
+
<div>
|
|
191
|
+
<Input className='hide' />
|
|
192
|
+
<ShortcutEdit
|
|
193
|
+
{...editorProps}
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
</FormItem>
|
|
197
|
+
<FormItem
|
|
198
|
+
label={e('inputOnly')}
|
|
199
|
+
name='inputOnly'
|
|
200
|
+
valuePropName='checked'
|
|
201
|
+
>
|
|
202
|
+
<Switch />
|
|
203
|
+
</FormItem>
|
|
204
|
+
<FormItem>
|
|
205
|
+
<p>
|
|
206
|
+
<Button
|
|
207
|
+
type='primary'
|
|
208
|
+
htmlType='submit'
|
|
209
|
+
>{e('save')}
|
|
210
|
+
</Button>
|
|
211
|
+
</p>
|
|
212
|
+
</FormItem>
|
|
213
|
+
</Form>
|
|
214
|
+
</>
|
|
200
215
|
)
|
|
201
216
|
}
|
|
@@ -2,6 +2,9 @@ import { Component } from 'react'
|
|
|
2
2
|
import {
|
|
3
3
|
CloseCircleOutlined
|
|
4
4
|
} from '@ant-design/icons'
|
|
5
|
+
import {
|
|
6
|
+
Tag
|
|
7
|
+
} from 'antd'
|
|
5
8
|
|
|
6
9
|
export default class AddrBookmarkItem extends Component {
|
|
7
10
|
handleClick = () => {
|
|
@@ -50,6 +53,9 @@ export default class AddrBookmarkItem extends Component {
|
|
|
50
53
|
item
|
|
51
54
|
} = this.props
|
|
52
55
|
const id = `${item.host}#${item.id}`
|
|
56
|
+
const globTag = item.isGlobal
|
|
57
|
+
? <Tag color='green'>G</Tag>
|
|
58
|
+
: null
|
|
53
59
|
return (
|
|
54
60
|
<div
|
|
55
61
|
key={item.id}
|
|
@@ -61,7 +67,8 @@ export default class AddrBookmarkItem extends Component {
|
|
|
61
67
|
onDragStart={this.handleDragStart}
|
|
62
68
|
onDrop={this.handleDrop}
|
|
63
69
|
>
|
|
64
|
-
{
|
|
70
|
+
{globTag}
|
|
71
|
+
<b>{item.addr}</b>
|
|
65
72
|
<CloseCircleOutlined
|
|
66
73
|
className='del-addr-bookmark'
|
|
67
74
|
onClick={this.handleDel}
|
|
@@ -4,7 +4,8 @@ import {
|
|
|
4
4
|
PlusSquareOutlined
|
|
5
5
|
} from '@ant-design/icons'
|
|
6
6
|
import {
|
|
7
|
-
Popover
|
|
7
|
+
Popover,
|
|
8
|
+
Button
|
|
8
9
|
} from 'antd'
|
|
9
10
|
import AddrBookmarkItem from './address-bookmark-item'
|
|
10
11
|
import { typeMap } from '../../common/constants'
|
|
@@ -16,7 +17,7 @@ export default auto(function AddrBookmark (props) {
|
|
|
16
17
|
window.store.delAddressBookmark(item)
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
function
|
|
20
|
+
function handleAddAddrAct (isGlobal = false) {
|
|
20
21
|
const {
|
|
21
22
|
host, realPath, type
|
|
22
23
|
} = props
|
|
@@ -24,11 +25,20 @@ export default auto(function AddrBookmark (props) {
|
|
|
24
25
|
{
|
|
25
26
|
addr: realPath,
|
|
26
27
|
host: type === typeMap.local ? '' : host,
|
|
28
|
+
isGlobal,
|
|
27
29
|
id: uid()
|
|
28
30
|
}
|
|
29
31
|
)
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
function handleAddAddr () {
|
|
35
|
+
handleAddAddrAct(false)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function handleAddAddrGlob () {
|
|
39
|
+
handleAddAddrAct(true)
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
const { type, onClickHistory, host } = props
|
|
33
43
|
const { store } = window
|
|
34
44
|
// const cls = classnames(
|
|
@@ -36,10 +46,13 @@ export default auto(function AddrBookmark (props) {
|
|
|
36
46
|
// 'animated',
|
|
37
47
|
// `sftp-history-${type}`
|
|
38
48
|
// )
|
|
39
|
-
const
|
|
49
|
+
const isLocal = type === typeMap.local
|
|
50
|
+
const addrs = isLocal
|
|
40
51
|
? store.addressBookmarksLocal
|
|
41
52
|
: store.addressBookmarks.filter(
|
|
42
|
-
g =>
|
|
53
|
+
g => {
|
|
54
|
+
return g.isGlobal || g.host === host
|
|
55
|
+
}
|
|
43
56
|
)
|
|
44
57
|
const inner = addrs.length
|
|
45
58
|
? addrs.map(o => {
|
|
@@ -61,11 +74,25 @@ export default auto(function AddrBookmark (props) {
|
|
|
61
74
|
{inner}
|
|
62
75
|
</div>
|
|
63
76
|
)
|
|
77
|
+
const globButton = isLocal
|
|
78
|
+
? null
|
|
79
|
+
: (
|
|
80
|
+
<Button
|
|
81
|
+
onClick={handleAddAddrGlob}
|
|
82
|
+
icon={<PlusSquareOutlined />}
|
|
83
|
+
>
|
|
84
|
+
{window.translate('global')}
|
|
85
|
+
</Button>
|
|
86
|
+
)
|
|
64
87
|
const title = (
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
88
|
+
<div>
|
|
89
|
+
<Button
|
|
90
|
+
className='add-addr-bookmark mg1r'
|
|
91
|
+
onClick={handleAddAddr}
|
|
92
|
+
icon={<PlusSquareOutlined />}
|
|
93
|
+
/>
|
|
94
|
+
{globButton}
|
|
95
|
+
</div>
|
|
69
96
|
)
|
|
70
97
|
return (
|
|
71
98
|
<Popover
|
|
@@ -52,7 +52,7 @@ export default class FileSection extends React.Component {
|
|
|
52
52
|
constructor (props) {
|
|
53
53
|
super(props)
|
|
54
54
|
this.state = {
|
|
55
|
-
file:
|
|
55
|
+
file: props.file,
|
|
56
56
|
overwriteStrategy: '',
|
|
57
57
|
dropdownOpen: false
|
|
58
58
|
}
|
|
@@ -122,7 +122,7 @@ export default class FileSection extends React.Component {
|
|
|
122
122
|
const files = targetFiles ||
|
|
123
123
|
(
|
|
124
124
|
selected
|
|
125
|
-
? this.props.
|
|
125
|
+
? this.props.getSelectedFiles()
|
|
126
126
|
: [file]
|
|
127
127
|
)
|
|
128
128
|
const prefix = file.type === typeMap.remote
|
|
@@ -141,7 +141,7 @@ export default class FileSection extends React.Component {
|
|
|
141
141
|
const files = targetFiles ||
|
|
142
142
|
(
|
|
143
143
|
selected
|
|
144
|
-
? this.props.
|
|
144
|
+
? this.props.getSelectedFiles()
|
|
145
145
|
: [file]
|
|
146
146
|
)
|
|
147
147
|
const textToCopy = files.map(f => {
|
|
@@ -220,7 +220,7 @@ export default class FileSection extends React.Component {
|
|
|
220
220
|
this.props.modifier({
|
|
221
221
|
onDrag: true
|
|
222
222
|
})
|
|
223
|
-
const cls = this.props.selectedFiles.
|
|
223
|
+
const cls = this.props.selectedFiles.size > 1
|
|
224
224
|
? onDragCls + ' ' + onMultiDragCls
|
|
225
225
|
: onDragCls
|
|
226
226
|
addClass(this.domRef.current, cls)
|
|
@@ -357,7 +357,7 @@ export default class FileSection extends React.Component {
|
|
|
357
357
|
|
|
358
358
|
transferDrop = (fromFiles, toFile, operation) => {
|
|
359
359
|
const files = this.isSelected(fromFiles[0])
|
|
360
|
-
? this.props.
|
|
360
|
+
? this.props.getSelectedFiles()
|
|
361
361
|
: fromFiles
|
|
362
362
|
return this.doTransferSelected(
|
|
363
363
|
null,
|
|
@@ -369,10 +369,7 @@ export default class FileSection extends React.Component {
|
|
|
369
369
|
}
|
|
370
370
|
|
|
371
371
|
isSelected = file => {
|
|
372
|
-
return
|
|
373
|
-
this.props.selectedFiles,
|
|
374
|
-
f => f.id === file.id
|
|
375
|
-
)
|
|
372
|
+
return this.props.selectedFiles.has(file.id)
|
|
376
373
|
}
|
|
377
374
|
|
|
378
375
|
doRename = () => {
|
|
@@ -453,7 +450,7 @@ export default class FileSection extends React.Component {
|
|
|
453
450
|
}
|
|
454
451
|
|
|
455
452
|
getShiftSelected (file, type) {
|
|
456
|
-
const indexs = this.props.
|
|
453
|
+
const indexs = this.props.getSelectedFiles().map(
|
|
457
454
|
this.props.getIndex
|
|
458
455
|
)
|
|
459
456
|
const i = this.props.getIndex(file)
|
|
@@ -486,12 +483,10 @@ export default class FileSection extends React.Component {
|
|
|
486
483
|
this.onDragEnd(e)
|
|
487
484
|
if (!id) {
|
|
488
485
|
return this.props.modifier({
|
|
489
|
-
selectedFiles:
|
|
486
|
+
selectedFiles: new Set()
|
|
490
487
|
})
|
|
491
488
|
}
|
|
492
|
-
const selectedFilesOld =
|
|
493
|
-
this.props.selectedFiles
|
|
494
|
-
)
|
|
489
|
+
const selectedFilesOld = this.props.getSelectedFiles()
|
|
495
490
|
const isSameSide = selectedFilesOld.length &&
|
|
496
491
|
type === selectedFilesOld[0].type
|
|
497
492
|
let selectedFiles = [file]
|
|
@@ -515,7 +510,8 @@ export default class FileSection extends React.Component {
|
|
|
515
510
|
}
|
|
516
511
|
}
|
|
517
512
|
this.props.modifier({
|
|
518
|
-
selectedFiles,
|
|
513
|
+
selectedFiles: new Set(selectedFiles.map(f => f.id)),
|
|
514
|
+
selectedType: type,
|
|
519
515
|
lastClickedFile: file
|
|
520
516
|
})
|
|
521
517
|
}
|
|
@@ -779,7 +775,7 @@ export default class FileSection extends React.Component {
|
|
|
779
775
|
|
|
780
776
|
doTransferSelected = async (
|
|
781
777
|
e,
|
|
782
|
-
selectedFiles = this.props.
|
|
778
|
+
selectedFiles = this.props.getSelectedFiles(),
|
|
783
779
|
toPathBase,
|
|
784
780
|
typeTo,
|
|
785
781
|
operation
|
|
@@ -820,16 +816,16 @@ export default class FileSection extends React.Component {
|
|
|
820
816
|
selectedFiles
|
|
821
817
|
} = this.props
|
|
822
818
|
return id &&
|
|
823
|
-
selectedFiles.
|
|
824
|
-
|
|
819
|
+
selectedFiles.size > 1 &&
|
|
820
|
+
selectedFiles.has(id)
|
|
825
821
|
}
|
|
826
822
|
|
|
827
823
|
del = async () => {
|
|
828
824
|
const delSelected = this.shouldShowSelectedMenu()
|
|
829
|
-
const { file
|
|
825
|
+
const { file } = this.props
|
|
830
826
|
const { type } = file
|
|
831
827
|
const files = delSelected
|
|
832
|
-
?
|
|
828
|
+
? this.props.getSelectedFiles()
|
|
833
829
|
: [file]
|
|
834
830
|
await this.props.delFiles(type, files)
|
|
835
831
|
}
|
|
@@ -875,9 +871,9 @@ export default class FileSection extends React.Component {
|
|
|
875
871
|
}
|
|
876
872
|
|
|
877
873
|
renderDelConfirmTitle (shouldShowSelectedMenu) {
|
|
878
|
-
const { file
|
|
874
|
+
const { file } = this.props
|
|
879
875
|
const files = shouldShowSelectedMenu
|
|
880
|
-
?
|
|
876
|
+
? this.props.getSelectedFiles()
|
|
881
877
|
: [file]
|
|
882
878
|
return this.props.renderDelConfirmTitle(files, true)
|
|
883
879
|
}
|
|
@@ -966,10 +962,10 @@ export default class FileSection extends React.Component {
|
|
|
966
962
|
const iconType = isLocal
|
|
967
963
|
? 'CloudUploadOutlined'
|
|
968
964
|
: 'CloudDownloadOutlined'
|
|
969
|
-
const len = selectedFiles.
|
|
965
|
+
const len = selectedFiles.size
|
|
970
966
|
const shouldShowSelectedMenu = id &&
|
|
971
967
|
len > 1 &&
|
|
972
|
-
|
|
968
|
+
selectedFiles.has(id)
|
|
973
969
|
const delTxt = shouldShowSelectedMenu ? `${e('deleteAll')}(${len})` : e('del')
|
|
974
970
|
const canPaste = hasFileInClipboardText()
|
|
975
971
|
const showEdit = !isDirectory && id &&
|
|
@@ -1219,7 +1215,7 @@ export default class FileSection extends React.Component {
|
|
|
1219
1215
|
if (isEditing) {
|
|
1220
1216
|
return this.renderEditing(file)
|
|
1221
1217
|
}
|
|
1222
|
-
const selected =
|
|
1218
|
+
const selected = selectedFiles.has(id)
|
|
1223
1219
|
const className = classnames('sftp-item', cls, type, {
|
|
1224
1220
|
directory: isDirectory,
|
|
1225
1221
|
selected
|
|
@@ -41,7 +41,8 @@ export default class Sftp extends Component {
|
|
|
41
41
|
super(props)
|
|
42
42
|
this.state = {
|
|
43
43
|
id: props.id || generate(),
|
|
44
|
-
selectedFiles:
|
|
44
|
+
selectedFiles: new Set(),
|
|
45
|
+
selectedType: '',
|
|
45
46
|
lastClickedFile: null,
|
|
46
47
|
onEditFile: false,
|
|
47
48
|
...this.defaultState(),
|
|
@@ -71,17 +72,17 @@ export default class Sftp extends Component {
|
|
|
71
72
|
}
|
|
72
73
|
if (
|
|
73
74
|
prevState.remotePath !== this.state.remotePath &&
|
|
74
|
-
this.state.
|
|
75
|
+
this.state.selectedType === typeMap.remote
|
|
75
76
|
) {
|
|
76
77
|
this.setState({
|
|
77
|
-
selectedFiles:
|
|
78
|
+
selectedFiles: new Set()
|
|
78
79
|
})
|
|
79
80
|
} else if (
|
|
80
81
|
prevState.localPath !== this.state.localPath &&
|
|
81
|
-
this.state.
|
|
82
|
+
this.state.selectedType === typeMap.local
|
|
82
83
|
) {
|
|
83
84
|
this.setState({
|
|
84
|
-
selectedFiles:
|
|
85
|
+
selectedFiles: new Set()
|
|
85
86
|
})
|
|
86
87
|
}
|
|
87
88
|
if (
|
|
@@ -136,6 +137,14 @@ export default class Sftp extends Component {
|
|
|
136
137
|
return this.directions[i]
|
|
137
138
|
}
|
|
138
139
|
|
|
140
|
+
getFileItemById = (id, type) => {
|
|
141
|
+
if (type) {
|
|
142
|
+
return this.state[`${type}FileTree`].get(id)
|
|
143
|
+
}
|
|
144
|
+
return this.getFileItemById(id, typeMap.local) ||
|
|
145
|
+
this.getFileItemById(id, typeMap.remote)
|
|
146
|
+
}
|
|
147
|
+
|
|
139
148
|
defaultState = () => {
|
|
140
149
|
const def = this.props.config.showHiddenFilesOnSftpStart
|
|
141
150
|
return Object.keys(typeMap).reduce((prev, k, i) => {
|
|
@@ -288,49 +297,61 @@ export default class Sftp extends Component {
|
|
|
288
297
|
selectAll = (type, e) => {
|
|
289
298
|
e && e.preventDefault && e.preventDefault()
|
|
290
299
|
this.setState({
|
|
291
|
-
selectedFiles: this.getFileList(type)
|
|
300
|
+
selectedFiles: new Set(this.getFileList(type).map(f => f.id))
|
|
292
301
|
})
|
|
293
302
|
}
|
|
294
303
|
|
|
295
304
|
selectNext = type => {
|
|
296
305
|
const { selectedFiles } = this.state
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
const lastOne = last(sorted)
|
|
300
|
-
const list = this.getFileList(type)
|
|
301
|
-
if (!list.length) {
|
|
306
|
+
const fileList = this.getFileList(type)
|
|
307
|
+
if (!fileList.length) {
|
|
302
308
|
return
|
|
303
309
|
}
|
|
310
|
+
|
|
311
|
+
// Convert Set of IDs to array of indices
|
|
312
|
+
const fileIndices = Array.from(selectedFiles)
|
|
313
|
+
.map(id => fileList.findIndex(f => f.id === id))
|
|
314
|
+
.filter(index => index !== -1)
|
|
315
|
+
.sort(sorterIndex)
|
|
316
|
+
|
|
317
|
+
const lastOne = last(fileIndices)
|
|
304
318
|
let next = 0
|
|
305
319
|
if (isNumber(lastOne)) {
|
|
306
|
-
next = (lastOne + 1) %
|
|
320
|
+
next = (lastOne + 1) % fileList.length
|
|
307
321
|
}
|
|
308
|
-
|
|
322
|
+
|
|
323
|
+
const nextFile = fileList[next]
|
|
309
324
|
if (nextFile) {
|
|
310
325
|
this.setState({
|
|
311
|
-
selectedFiles: [nextFile]
|
|
326
|
+
selectedFiles: new Set([nextFile.id])
|
|
312
327
|
})
|
|
313
328
|
}
|
|
314
329
|
}
|
|
315
330
|
|
|
316
331
|
selectPrev = type => {
|
|
317
332
|
const { selectedFiles } = this.state
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
const lastOne = sorted[0]
|
|
321
|
-
const list = this.getFileList(type)
|
|
322
|
-
if (!list.length) {
|
|
333
|
+
const fileList = this.getFileList(type)
|
|
334
|
+
if (!fileList.length) {
|
|
323
335
|
return
|
|
324
336
|
}
|
|
337
|
+
|
|
338
|
+
// Convert Set of IDs to array of indices
|
|
339
|
+
const fileIndices = Array.from(selectedFiles)
|
|
340
|
+
.map(id => fileList.findIndex(f => f.id === id))
|
|
341
|
+
.filter(index => index !== -1)
|
|
342
|
+
.sort(sorterIndex)
|
|
343
|
+
|
|
344
|
+
const firstOne = fileIndices[0]
|
|
325
345
|
let next = 0
|
|
326
|
-
const len =
|
|
327
|
-
if (isNumber(
|
|
328
|
-
next = (
|
|
346
|
+
const len = fileList.length
|
|
347
|
+
if (isNumber(firstOne)) {
|
|
348
|
+
next = (firstOne - 1 + len) % len
|
|
329
349
|
}
|
|
330
|
-
|
|
350
|
+
|
|
351
|
+
const nextFile = fileList[next]
|
|
331
352
|
if (nextFile) {
|
|
332
353
|
this.setState({
|
|
333
|
-
selectedFiles: [nextFile]
|
|
354
|
+
selectedFiles: new Set([nextFile.id])
|
|
334
355
|
})
|
|
335
356
|
}
|
|
336
357
|
}
|
|
@@ -366,14 +387,23 @@ export default class Sftp extends Component {
|
|
|
366
387
|
})
|
|
367
388
|
}
|
|
368
389
|
|
|
369
|
-
|
|
390
|
+
getSelectedFiles = (selectedFiles = this.state.selectedFiles) => {
|
|
391
|
+
// Convert Set of IDs to array of file objects
|
|
392
|
+
return Array.isArray(selectedFiles)
|
|
393
|
+
? selectedFiles
|
|
394
|
+
: Array.from(selectedFiles)
|
|
395
|
+
.map(id => this.getFileItemById(id))
|
|
396
|
+
.filter(Boolean) // Filter out any undefined items
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
delFiles = async (_type, files = this.getSelectedFiles()) => {
|
|
370
400
|
this.onDelete = true
|
|
371
401
|
const confirm = await this.confirmDelete(files)
|
|
372
402
|
this.onDelete = false
|
|
373
403
|
if (!confirm) {
|
|
374
404
|
return
|
|
375
405
|
}
|
|
376
|
-
const type = files[0]
|
|
406
|
+
const type = files[0]?.type || _type
|
|
377
407
|
const func = this[type + 'Del']
|
|
378
408
|
for (const f of files) {
|
|
379
409
|
await func(f)
|
|
@@ -384,7 +414,7 @@ export default class Sftp extends Component {
|
|
|
384
414
|
this[type + 'List']()
|
|
385
415
|
}
|
|
386
416
|
|
|
387
|
-
renderDelConfirmTitle (files = this.
|
|
417
|
+
renderDelConfirmTitle (files = this.getSelectedFiles(), pureText) {
|
|
388
418
|
const hasDirectory = some(files, f => f.isDirectory)
|
|
389
419
|
const names = hasDirectory ? e('filesAndFolders') : e('files')
|
|
390
420
|
if (pureText) {
|
|
@@ -409,10 +439,14 @@ export default class Sftp extends Component {
|
|
|
409
439
|
|
|
410
440
|
enter = (type, e) => {
|
|
411
441
|
const { selectedFiles, onEditFile } = this.state
|
|
412
|
-
if (onEditFile || selectedFiles.
|
|
442
|
+
if (onEditFile || selectedFiles.size !== 1) {
|
|
443
|
+
return
|
|
444
|
+
}
|
|
445
|
+
const fileId = Array.from(selectedFiles)[0]
|
|
446
|
+
const file = this.getFileItemById(fileId)
|
|
447
|
+
if (!file) {
|
|
413
448
|
return
|
|
414
449
|
}
|
|
415
|
-
const file = selectedFiles[0]
|
|
416
450
|
const { isDirectory } = file
|
|
417
451
|
if (isDirectory) {
|
|
418
452
|
this[type + 'Dom'].enterDirectory(e, file)
|
|
@@ -440,11 +474,13 @@ export default class Sftp extends Component {
|
|
|
440
474
|
}
|
|
441
475
|
|
|
442
476
|
doCopy = (type, e) => {
|
|
443
|
-
|
|
477
|
+
const selectedFiles = this.getSelectedFiles()
|
|
478
|
+
this[type + 'Dom'].onCopy(selectedFiles)
|
|
444
479
|
}
|
|
445
480
|
|
|
446
481
|
doCut = (type, e) => {
|
|
447
|
-
|
|
482
|
+
const selectedFiles = this.getSelectedFiles()
|
|
483
|
+
this[type + 'Dom'].onCut(selectedFiles)
|
|
448
484
|
}
|
|
449
485
|
|
|
450
486
|
doPaste = (type) => {
|
|
@@ -937,7 +973,9 @@ export default class Sftp extends Component {
|
|
|
937
973
|
'getFileList',
|
|
938
974
|
'onGoto',
|
|
939
975
|
'addTransferList',
|
|
940
|
-
'renderDelConfirmTitle'
|
|
976
|
+
'renderDelConfirmTitle',
|
|
977
|
+
'getSelectedFiles',
|
|
978
|
+
'getFileItemById'
|
|
941
979
|
]),
|
|
942
980
|
...pick(this.state, [
|
|
943
981
|
'id',
|
|
@@ -58,6 +58,19 @@ import SearchResultBar from './terminal-search-bar'
|
|
|
58
58
|
|
|
59
59
|
const e = window.translate
|
|
60
60
|
|
|
61
|
+
const PS1_SETUP_CMD = `\recho $0|grep csh >/dev/null && set prompt_bak="$prompt" && set prompt="$prompt${cwdId}%/${cwdId}"\r
|
|
62
|
+
echo $0|grep zsh >/dev/null && PS1_bak=$PS1&&PS1=$PS1'${cwdId}%d${cwdId}'\r
|
|
63
|
+
echo $0|grep ash >/dev/null && PS1_bak=$PS1&&PS1=$PS1'\`echo ${cwdId}$PWD${cwdId}\`'\r
|
|
64
|
+
echo $0|grep ksh >/dev/null && PS1_bak=$PS1&&PS1=$PS1'\`echo ${cwdId}$PWD${cwdId}\`'\r
|
|
65
|
+
echo $0|grep '^sh' >/dev/null && PS1_bak=$PS1&&PS1=$PS1'\`echo ${cwdId}$PWD${cwdId}\`'\r
|
|
66
|
+
clear\r`
|
|
67
|
+
|
|
68
|
+
const PS1_RESTORE_CMD = `\recho $0|grep csh >/dev/null && set prompt="$prompt_bak"\r
|
|
69
|
+
echo $0|grep zsh >/dev/null && PS1="$PS1_bak"\r
|
|
70
|
+
echo $0|grep ash >/dev/null && PS1="$PS1_bak"\r
|
|
71
|
+
echo $0|grep ksh >/dev/null && PS1="$PS1_bak"\r
|
|
72
|
+
echo $0|grep '^sh' >/dev/null && PS1="$PS1_bak"\r
|
|
73
|
+
clear\r`
|
|
61
74
|
class Term extends Component {
|
|
62
75
|
constructor (props) {
|
|
63
76
|
super(props)
|
|
@@ -133,25 +146,20 @@ class Term extends Component {
|
|
|
133
146
|
)
|
|
134
147
|
|
|
135
148
|
if (sftpPathFollowSshChanged) {
|
|
136
|
-
const ps1Cmd = `\recho $0|grep csh >/dev/null && set prompt_bak="$prompt" && set prompt="$prompt${cwdId}%/${cwdId}"\r
|
|
137
|
-
echo $0|grep zsh >/dev/null && PS1_bak=$PS1&&PS1=$PS1'${cwdId}%d${cwdId}'\r
|
|
138
|
-
echo $0|grep ash >/dev/null && PS1_bak=$PS1&&PS1=$PS1'\`echo ${cwdId}$PWD${cwdId}\`'\r
|
|
139
|
-
echo $0|grep ksh >/dev/null && PS1_bak=$PS1&&PS1=$PS1'\`echo ${cwdId}$PWD${cwdId}\`'\r
|
|
140
|
-
echo $0|grep '^sh' >/dev/null && PS1_bak=$PS1&&PS1=$PS1'\`echo ${cwdId}$PWD${cwdId}\`'\r
|
|
141
|
-
clear\r`
|
|
142
|
-
const ps1RestoreCmd = `\recho $0|grep csh >/dev/null && set prompt="$prompt_bak"\r
|
|
143
|
-
echo $0|grep zsh >/dev/null && PS1="$PS1_bak"\r
|
|
144
|
-
echo $0|grep ash >/dev/null && PS1="$PS1_bak"\r
|
|
145
|
-
echo $0|grep ksh >/dev/null && PS1="$PS1_bak"\r
|
|
146
|
-
echo $0|grep '^sh' >/dev/null && PS1="$PS1_bak"\r
|
|
147
|
-
clear\r`
|
|
148
|
-
|
|
149
149
|
if (this.props.sftpPathFollowSsh) {
|
|
150
|
-
this.
|
|
151
|
-
|
|
150
|
+
if (this.attachAddon && this.term) {
|
|
151
|
+
this.attachAddon._sendData(PS1_SETUP_CMD)
|
|
152
|
+
this.term.cwdId = cwdId
|
|
153
|
+
} else {
|
|
154
|
+
log.warn('Term or attachAddon not ready for PS1_SETUP_CMD in componentDidUpdate')
|
|
155
|
+
}
|
|
152
156
|
} else {
|
|
153
|
-
this.
|
|
154
|
-
|
|
157
|
+
if (this.attachAddon) {
|
|
158
|
+
this.attachAddon._sendData(PS1_RESTORE_CMD)
|
|
159
|
+
}
|
|
160
|
+
if (this.term) {
|
|
161
|
+
delete this.term.cwdId
|
|
162
|
+
}
|
|
155
163
|
}
|
|
156
164
|
}
|
|
157
165
|
}
|
|
@@ -878,17 +886,17 @@ clear\r`
|
|
|
878
886
|
getCwd = () => {
|
|
879
887
|
if (
|
|
880
888
|
this.props.sftpPathFollowSsh &&
|
|
889
|
+
this.term &&
|
|
881
890
|
this.term.buffer.active.type !== 'alternate' && !this.term.cwdId
|
|
882
891
|
) {
|
|
892
|
+
// This block should ideally not be hit for initial setup if runInitScript works.
|
|
893
|
+
// It acts as a fallback.
|
|
883
894
|
this.term.cwdId = cwdId
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
clear\r`
|
|
890
|
-
|
|
891
|
-
this.socket.send(ps1Cmd)
|
|
895
|
+
if (this.attachAddon) {
|
|
896
|
+
this.attachAddon._sendData(PS1_SETUP_CMD)
|
|
897
|
+
} else {
|
|
898
|
+
log.warn('attachAddon not ready for PS1_SETUP_CMD in getCwd fallback')
|
|
899
|
+
}
|
|
892
900
|
}
|
|
893
901
|
}
|
|
894
902
|
|
|
@@ -1053,9 +1061,23 @@ clear\r`
|
|
|
1053
1061
|
} = this.props.tab
|
|
1054
1062
|
const startFolder = startDirectory || window.initFolder
|
|
1055
1063
|
if (startFolder) {
|
|
1056
|
-
|
|
1057
|
-
|
|
1064
|
+
if (this.attachAddon) {
|
|
1065
|
+
const cmd = `cd "${startFolder}"\r`
|
|
1066
|
+
this.attachAddon._sendData(cmd)
|
|
1067
|
+
} else {
|
|
1068
|
+
log.warn('attachAddon not ready for cd command in runInitScript')
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
if (this.props.sftpPathFollowSsh) {
|
|
1073
|
+
if (this.term && this.attachAddon) {
|
|
1074
|
+
this.attachAddon._sendData(PS1_SETUP_CMD)
|
|
1075
|
+
this.term.cwdId = cwdId
|
|
1076
|
+
} else {
|
|
1077
|
+
log.warn('Term or attachAddon not ready for PS1_SETUP_CMD in runInitScript')
|
|
1078
|
+
}
|
|
1058
1079
|
}
|
|
1080
|
+
|
|
1059
1081
|
if (runScripts && runScripts.length) {
|
|
1060
1082
|
this.delayedScripts = deepCopy(runScripts)
|
|
1061
1083
|
this.timers.timerDelay = setTimeout(this.runDelayedScripts, this.delayedScripts[0].delay || 0)
|
|
@@ -12,6 +12,35 @@ import generate from '../common/uid'
|
|
|
12
12
|
import * as ls from '../common/safe-local-storage'
|
|
13
13
|
import { debounce } from 'lodash-es'
|
|
14
14
|
import { refs } from '../components/common/ref'
|
|
15
|
+
import templates from '../components/quick-commands/templates'
|
|
16
|
+
import { readClipboardAsync } from '../common/clipboard'
|
|
17
|
+
|
|
18
|
+
// Function to parse templates in command string
|
|
19
|
+
async function parseTemplates (cmd) {
|
|
20
|
+
if (!cmd.includes('{{')) return cmd
|
|
21
|
+
|
|
22
|
+
// Process each template from templates.js
|
|
23
|
+
for (const template of templates) {
|
|
24
|
+
const placeholder = `{{${template}}}`
|
|
25
|
+
if (cmd.includes(placeholder)) {
|
|
26
|
+
let replacement = ''
|
|
27
|
+
|
|
28
|
+
// Handle each supported template using if-else
|
|
29
|
+
if (template === 'clipboard') {
|
|
30
|
+
replacement = await readClipboardAsync()
|
|
31
|
+
} else if (template === 'time') {
|
|
32
|
+
replacement = Date.now()
|
|
33
|
+
} else if (template === 'date') {
|
|
34
|
+
replacement = new Date().toLocaleDateString()
|
|
35
|
+
}
|
|
36
|
+
// Add more conditions for any new templates as needed
|
|
37
|
+
|
|
38
|
+
cmd = cmd.replaceAll(placeholder, replacement)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return cmd
|
|
43
|
+
}
|
|
15
44
|
|
|
16
45
|
export default Store => {
|
|
17
46
|
Store.prototype.addQuickCommand = function (
|
|
@@ -54,9 +83,13 @@ export default Store => {
|
|
|
54
83
|
: []
|
|
55
84
|
)
|
|
56
85
|
for (const q of qms) {
|
|
57
|
-
|
|
86
|
+
let realCmd = isWin
|
|
58
87
|
? q.command.replace(/\n/g, '\n\r')
|
|
59
88
|
: q.command
|
|
89
|
+
|
|
90
|
+
// Parse templates
|
|
91
|
+
realCmd = await parseTemplates(realCmd)
|
|
92
|
+
|
|
60
93
|
await delay(q.delay || 100)
|
|
61
94
|
runQuickCommand(realCmd, qm.inputOnly)
|
|
62
95
|
store.editQuickCommand(qm.id, {
|