@electerm/electerm-react 3.6.16 → 3.7.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.
- package/client/common/parse-quick-connect.js +6 -1
- package/client/components/bookmark-form/ai-bookmark-form.jsx +98 -23
- package/client/components/bookmark-form/bookmark-schema.js +2 -2
- package/client/components/bookmark-form/common/ai-category-select.jsx +3 -1
- package/client/components/bookmark-form/common/proxy.jsx +9 -15
- package/client/components/bookmark-form/fix-bookmark-default.js +4 -2
- package/client/components/bookmark-form/index.jsx +5 -1
- package/client/components/bookmark-form/tree-select.jsx +3 -1
- package/client/components/file-transfer/transfer.jsx +11 -9
- package/client/components/file-transfer/transports-action-store.jsx +1 -14
- package/client/components/main/main.jsx +0 -2
- package/client/components/sftp/list-table-ui.jsx +2 -2
- package/client/components/sftp/paged-list.jsx +1 -1
- package/client/components/terminal/shell.js +8 -9
- package/client/components/terminal/terminal.jsx +1 -0
- package/client/components/tree-list/tree-expander.jsx +23 -1
- package/client/components/tree-list/tree-list-item.jsx +36 -6
- package/package.json +1 -1
- package/client/components/common/auto-check-update.jsx +0 -31
|
@@ -370,7 +370,12 @@ function parseQuickConnect (str) {
|
|
|
370
370
|
opts.port = parseInt(port, 10)
|
|
371
371
|
}
|
|
372
372
|
if (username !== undefined && username !== '') {
|
|
373
|
-
|
|
373
|
+
// FTP form uses 'user' instead of 'username'
|
|
374
|
+
if (finalProtocol === 'ftp') {
|
|
375
|
+
opts.user = username
|
|
376
|
+
} else {
|
|
377
|
+
opts.username = username
|
|
378
|
+
}
|
|
374
379
|
}
|
|
375
380
|
if (password !== undefined && password !== '') {
|
|
376
381
|
opts.password = password
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* AI-powered bookmark generation form
|
|
3
3
|
*/
|
|
4
4
|
import { useState, useEffect } from 'react'
|
|
5
|
-
import { Button, Input, Space, Alert } from 'antd'
|
|
5
|
+
import { Button, Input, Space, Alert, Progress } from 'antd'
|
|
6
6
|
import message from '../common/message'
|
|
7
7
|
import {
|
|
8
8
|
RobotOutlined,
|
|
@@ -33,6 +33,16 @@ const EVENT_NAME_HISTORY = 'ai-bookmark-history-update'
|
|
|
33
33
|
const { TextArea } = Input
|
|
34
34
|
const e = window.translate
|
|
35
35
|
|
|
36
|
+
function yieldToUI () {
|
|
37
|
+
return new Promise(resolve => {
|
|
38
|
+
if (typeof requestAnimationFrame === 'function') {
|
|
39
|
+
requestAnimationFrame(() => resolve())
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
setTimeout(resolve, 0)
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
36
46
|
export default function AIBookmarkForm (props) {
|
|
37
47
|
const { onCancel } = props
|
|
38
48
|
const [description, setDescription] = useState(() => getItem(STORAGE_KEY_DESC) || '')
|
|
@@ -41,6 +51,7 @@ export default function AIBookmarkForm (props) {
|
|
|
41
51
|
const [editMode, setEditMode] = useState(false)
|
|
42
52
|
const [editorText, setEditorText] = useState('')
|
|
43
53
|
const [selectedCategory, setSelectedCategory] = useState('default')
|
|
54
|
+
const [confirmProgress, setConfirmProgress] = useState(null)
|
|
44
55
|
|
|
45
56
|
useEffect(() => {
|
|
46
57
|
setItem(STORAGE_KEY_DESC, description)
|
|
@@ -80,8 +91,18 @@ export default function AIBookmarkForm (props) {
|
|
|
80
91
|
|
|
81
92
|
let bookmarkData
|
|
82
93
|
if (aiResponse && aiResponse.response) {
|
|
94
|
+
// Normalize response payload to string before JSON extraction.
|
|
95
|
+
const rawResponse = aiResponse.response
|
|
96
|
+
let jsonStr = ''
|
|
97
|
+
if (typeof rawResponse === 'string') {
|
|
98
|
+
jsonStr = rawResponse
|
|
99
|
+
} else if (typeof rawResponse === 'object') {
|
|
100
|
+
jsonStr = JSON.stringify(rawResponse)
|
|
101
|
+
} else {
|
|
102
|
+
jsonStr = String(rawResponse)
|
|
103
|
+
}
|
|
83
104
|
// Parse the JSON response
|
|
84
|
-
|
|
105
|
+
jsonStr = jsonStr.trim()
|
|
85
106
|
// Remove markdown code blocks if present
|
|
86
107
|
if (jsonStr.startsWith('```json')) {
|
|
87
108
|
jsonStr = jsonStr.slice(7)
|
|
@@ -93,7 +114,7 @@ export default function AIBookmarkForm (props) {
|
|
|
93
114
|
}
|
|
94
115
|
jsonStr = jsonStr.trim()
|
|
95
116
|
|
|
96
|
-
bookmarkData =
|
|
117
|
+
bookmarkData = getGeneratedData(jsonStr)
|
|
97
118
|
const pretty = JSON.stringify(bookmarkData, null, 2)
|
|
98
119
|
setEditorText(pretty)
|
|
99
120
|
// set default category when preview opens
|
|
@@ -103,25 +124,26 @@ export default function AIBookmarkForm (props) {
|
|
|
103
124
|
}
|
|
104
125
|
} catch (error) {
|
|
105
126
|
console.error('AI bookmark generation error:', error)
|
|
106
|
-
message.error(
|
|
127
|
+
message.error('Can not generate bookmarks from AI response: ' + error.message)
|
|
107
128
|
} finally {
|
|
108
129
|
setLoading(false)
|
|
109
130
|
}
|
|
110
131
|
}
|
|
111
132
|
|
|
112
|
-
function getGeneratedData () {
|
|
113
|
-
if (!
|
|
133
|
+
function getGeneratedData (txt = editorText) {
|
|
134
|
+
if (!txt) return []
|
|
114
135
|
let parsed = null
|
|
115
136
|
try {
|
|
116
|
-
parsed =
|
|
137
|
+
parsed = JSON.parse(txt)
|
|
117
138
|
} catch (err) {
|
|
118
139
|
return []
|
|
119
140
|
}
|
|
120
141
|
if (!parsed) return []
|
|
121
|
-
|
|
142
|
+
const arr = Array.isArray(parsed) ? parsed : [parsed]
|
|
143
|
+
return arr.map(d => fixBookmarkData(d))
|
|
122
144
|
}
|
|
123
145
|
|
|
124
|
-
const createBookmark =
|
|
146
|
+
const createBookmark = (bm) => {
|
|
125
147
|
const { store } = window
|
|
126
148
|
const { addItem } = store
|
|
127
149
|
const fixedBm = fixBookmarkData(bm)
|
|
@@ -152,19 +174,35 @@ export default function AIBookmarkForm (props) {
|
|
|
152
174
|
|
|
153
175
|
const handleConfirm = async () => {
|
|
154
176
|
const parsed = getGeneratedData()
|
|
155
|
-
if (!parsed.length) {
|
|
177
|
+
if (!parsed.length || confirmProgress) {
|
|
156
178
|
return
|
|
157
179
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
180
|
+
|
|
181
|
+
setConfirmProgress({ current: 0, total: parsed.length })
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
185
|
+
// Yield between synchronous store mutations so large imports stay responsive.
|
|
186
|
+
await yieldToUI()
|
|
187
|
+
createBookmark(parsed[i])
|
|
188
|
+
setConfirmProgress({ current: i + 1, total: parsed.length })
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
setShowConfirm(false)
|
|
192
|
+
setDescription('') // Clear description only on successful creation
|
|
193
|
+
message.success(e('Done'))
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.error('AI bookmark creation error:', error)
|
|
196
|
+
message.error('Can not create bookmarks from AI response: ' + error.message)
|
|
197
|
+
} finally {
|
|
198
|
+
setConfirmProgress(null)
|
|
161
199
|
}
|
|
162
|
-
setShowConfirm(false)
|
|
163
|
-
setDescription('') // Clear description only on successful creation
|
|
164
|
-
message.success(e('Done'))
|
|
165
200
|
}
|
|
166
201
|
|
|
167
202
|
const handleCancelConfirm = () => {
|
|
203
|
+
if (confirmProgress) {
|
|
204
|
+
return
|
|
205
|
+
}
|
|
168
206
|
setShowConfirm(false)
|
|
169
207
|
}
|
|
170
208
|
|
|
@@ -198,6 +236,9 @@ export default function AIBookmarkForm (props) {
|
|
|
198
236
|
}
|
|
199
237
|
|
|
200
238
|
const handleToggleEdit = () => {
|
|
239
|
+
if (confirmProgress) {
|
|
240
|
+
return
|
|
241
|
+
}
|
|
201
242
|
setEditMode(!editMode)
|
|
202
243
|
}
|
|
203
244
|
|
|
@@ -208,10 +249,16 @@ export default function AIBookmarkForm (props) {
|
|
|
208
249
|
}
|
|
209
250
|
|
|
210
251
|
const handleCopy = () => {
|
|
252
|
+
if (confirmProgress) {
|
|
253
|
+
return
|
|
254
|
+
}
|
|
211
255
|
copy(editorText)
|
|
212
256
|
}
|
|
213
257
|
|
|
214
258
|
const handleSaveToFile = async () => {
|
|
259
|
+
if (confirmProgress) {
|
|
260
|
+
return
|
|
261
|
+
}
|
|
215
262
|
const parsed = getGeneratedData()
|
|
216
263
|
if (!parsed.length) {
|
|
217
264
|
return
|
|
@@ -228,6 +275,7 @@ export default function AIBookmarkForm (props) {
|
|
|
228
275
|
}
|
|
229
276
|
return (
|
|
230
277
|
<SimpleEditor
|
|
278
|
+
key='editor'
|
|
231
279
|
{...editorProps}
|
|
232
280
|
/>
|
|
233
281
|
)
|
|
@@ -238,7 +286,7 @@ export default function AIBookmarkForm (props) {
|
|
|
238
286
|
return renderEditor()
|
|
239
287
|
}
|
|
240
288
|
return (
|
|
241
|
-
<pre className='ai-bookmark-json-preview'>
|
|
289
|
+
<pre key='preview' className='ai-bookmark-json-preview'>
|
|
242
290
|
{editorText}
|
|
243
291
|
</pre>
|
|
244
292
|
)
|
|
@@ -249,11 +297,29 @@ export default function AIBookmarkForm (props) {
|
|
|
249
297
|
<AICategorySelect
|
|
250
298
|
bookmarkGroups={window.store.bookmarkGroups}
|
|
251
299
|
value={selectedCategory}
|
|
300
|
+
disabled={!!confirmProgress}
|
|
252
301
|
onChange={setSelectedCategory}
|
|
253
302
|
/>
|
|
254
303
|
)
|
|
255
304
|
}
|
|
256
305
|
|
|
306
|
+
const renderConfirmProgress = () => {
|
|
307
|
+
if (!confirmProgress) {
|
|
308
|
+
return null
|
|
309
|
+
}
|
|
310
|
+
const { current, total } = confirmProgress
|
|
311
|
+
const percent = Math.floor(current * 100 / (total || 1))
|
|
312
|
+
return (
|
|
313
|
+
<div className='pd1y'>
|
|
314
|
+
<Progress
|
|
315
|
+
percent={percent}
|
|
316
|
+
status='active'
|
|
317
|
+
format={() => `${current}/${total}`}
|
|
318
|
+
/>
|
|
319
|
+
</div>
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
|
|
257
323
|
const textAreaProps = {
|
|
258
324
|
value: description,
|
|
259
325
|
onChange: e => setDescription(e.target.value),
|
|
@@ -272,7 +338,7 @@ export default function AIBookmarkForm (props) {
|
|
|
272
338
|
|
|
273
339
|
function renderQuickConnectBtn () {
|
|
274
340
|
const parsed = getGeneratedData()
|
|
275
|
-
if (!parsed.length || !parsed[0] || parsed.length > 1) {
|
|
341
|
+
if (!parsed.length || !parsed[0] || parsed.length > 1 || confirmProgress) {
|
|
276
342
|
return null
|
|
277
343
|
}
|
|
278
344
|
return (
|
|
@@ -283,7 +349,7 @@ export default function AIBookmarkForm (props) {
|
|
|
283
349
|
}
|
|
284
350
|
|
|
285
351
|
const modalProps = {
|
|
286
|
-
title: e('
|
|
352
|
+
title: e('bookmarks') + ' ' + e('preview'),
|
|
287
353
|
open: showConfirm,
|
|
288
354
|
onCancel: handleCancelConfirm,
|
|
289
355
|
footer: (
|
|
@@ -292,7 +358,12 @@ export default function AIBookmarkForm (props) {
|
|
|
292
358
|
<CloseOutlined /> {e('cancel')}
|
|
293
359
|
</Button>
|
|
294
360
|
{renderQuickConnectBtn()}
|
|
295
|
-
<Button
|
|
361
|
+
<Button
|
|
362
|
+
type='primary'
|
|
363
|
+
onClick={handleConfirm}
|
|
364
|
+
loading={!!confirmProgress}
|
|
365
|
+
disabled={!!confirmProgress}
|
|
366
|
+
>
|
|
296
367
|
<CheckOutlined /> {e('confirm')}
|
|
297
368
|
</Button>
|
|
298
369
|
</div>
|
|
@@ -303,19 +374,22 @@ export default function AIBookmarkForm (props) {
|
|
|
303
374
|
const editBtnProps = {
|
|
304
375
|
icon: editMode ? <EyeOutlined /> : <EditOutlined />,
|
|
305
376
|
title: editMode ? e('preview') : e('edit'),
|
|
306
|
-
onClick: handleToggleEdit
|
|
377
|
+
onClick: handleToggleEdit,
|
|
378
|
+
disabled: !!confirmProgress
|
|
307
379
|
}
|
|
308
380
|
|
|
309
381
|
const copyBtnProps = {
|
|
310
382
|
icon: <CopyOutlined />,
|
|
311
383
|
title: e('copy'),
|
|
312
|
-
onClick: handleCopy
|
|
384
|
+
onClick: handleCopy,
|
|
385
|
+
disabled: !!confirmProgress
|
|
313
386
|
}
|
|
314
387
|
|
|
315
388
|
const downloadBtnProps = {
|
|
316
389
|
icon: <DownloadOutlined />,
|
|
317
390
|
title: e('download'),
|
|
318
|
-
onClick: handleSaveToFile
|
|
391
|
+
onClick: handleSaveToFile,
|
|
392
|
+
disabled: !!confirmProgress
|
|
319
393
|
}
|
|
320
394
|
|
|
321
395
|
const cancelProps = {
|
|
@@ -366,6 +440,7 @@ export default function AIBookmarkForm (props) {
|
|
|
366
440
|
<Button {...downloadBtnProps} />
|
|
367
441
|
</Space.Compact>
|
|
368
442
|
</div>
|
|
443
|
+
{renderConfirmProgress()}
|
|
369
444
|
<div className='pd1y'>
|
|
370
445
|
{renderCategorySelect()}
|
|
371
446
|
</div>
|
|
@@ -6,9 +6,9 @@ const bookmarkSchema = {
|
|
|
6
6
|
username: 'string (required) - SSH username',
|
|
7
7
|
password: 'string - password for authentication',
|
|
8
8
|
privateKey: 'string - private key content or path for key-based auth',
|
|
9
|
-
passphrase: 'string - passphrase for private key/
|
|
9
|
+
passphrase: 'string - passphrase for private key/certificate',
|
|
10
10
|
certificate: 'string - certificate content',
|
|
11
|
-
authType: 'string - auth type (password|privateKey|profiles)',
|
|
11
|
+
authType: 'string - auth type (password|privateKey|profiles), when have profile, should be profiles',
|
|
12
12
|
profile: 'string - profile id to reuse saved auth',
|
|
13
13
|
title: 'string - bookmark title',
|
|
14
14
|
description: 'string - bookmark description',
|
|
@@ -6,7 +6,8 @@ const e = window.translate
|
|
|
6
6
|
export default function AICategorySelect ({
|
|
7
7
|
bookmarkGroups = [],
|
|
8
8
|
value,
|
|
9
|
-
onChange
|
|
9
|
+
onChange,
|
|
10
|
+
disabled = false
|
|
10
11
|
}) {
|
|
11
12
|
const tree = formatBookmarkGroups(bookmarkGroups)
|
|
12
13
|
|
|
@@ -24,6 +25,7 @@ export default function AICategorySelect ({
|
|
|
24
25
|
treeData={tree}
|
|
25
26
|
treeDefaultExpandAll
|
|
26
27
|
showSearch
|
|
28
|
+
disabled={disabled}
|
|
27
29
|
onChange={handleChange}
|
|
28
30
|
style={{ minWidth: 200 }}
|
|
29
31
|
/>
|
|
@@ -17,17 +17,13 @@ export default function renderProxy (props) {
|
|
|
17
17
|
}
|
|
18
18
|
return prev
|
|
19
19
|
}, {})
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}),
|
|
28
|
-
placeholder: 'socks5://127.0.0.1:1080',
|
|
29
|
-
allowClear: true
|
|
30
|
-
}
|
|
20
|
+
const options = Object.keys(proxyTree)
|
|
21
|
+
.map(d => {
|
|
22
|
+
return {
|
|
23
|
+
label: d,
|
|
24
|
+
value: d
|
|
25
|
+
}
|
|
26
|
+
})
|
|
31
27
|
return (
|
|
32
28
|
<FormItem
|
|
33
29
|
{...formItemLayout}
|
|
@@ -38,10 +34,8 @@ export default function renderProxy (props) {
|
|
|
38
34
|
max: 1024, message: '1024 chars max'
|
|
39
35
|
}]}
|
|
40
36
|
>
|
|
41
|
-
<AutoComplete
|
|
42
|
-
|
|
43
|
-
>
|
|
44
|
-
<Input />
|
|
37
|
+
<AutoComplete options={options}>
|
|
38
|
+
<Input allowClear placeholder='socks5://127.0.0.1:1080' />
|
|
45
39
|
</AutoComplete>
|
|
46
40
|
</FormItem>
|
|
47
41
|
)
|
|
@@ -9,8 +9,10 @@ const defaultValues = {
|
|
|
9
9
|
x11: false,
|
|
10
10
|
term: 'xterm-256color',
|
|
11
11
|
displayRaw: false,
|
|
12
|
+
authType: 'password',
|
|
12
13
|
encode: 'utf8',
|
|
13
|
-
envLang: 'en_US.UTF-8'
|
|
14
|
+
envLang: 'en_US.UTF-8',
|
|
15
|
+
username: 'root'
|
|
14
16
|
},
|
|
15
17
|
telnet: {
|
|
16
18
|
port: 23
|
|
@@ -44,7 +46,7 @@ const defaultValues = {
|
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
const requiredFields = {
|
|
47
|
-
ssh: ['host'
|
|
49
|
+
ssh: ['host'],
|
|
48
50
|
telnet: ['host'],
|
|
49
51
|
serial: ['path'],
|
|
50
52
|
vnc: ['host'],
|
|
@@ -57,6 +57,7 @@ export default class BookmarkIndex2 extends PureComponent {
|
|
|
57
57
|
|
|
58
58
|
componentWillUnmount () {
|
|
59
59
|
clearTimeout(this.timer)
|
|
60
|
+
clearTimeout(this.timer1)
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
getInitAiModeState () {
|
|
@@ -64,7 +65,10 @@ export default class BookmarkIndex2 extends PureComponent {
|
|
|
64
65
|
if (v !== true) {
|
|
65
66
|
return false
|
|
66
67
|
}
|
|
67
|
-
|
|
68
|
+
this.timer1 = setTimeout(() => {
|
|
69
|
+
delete window.et.openBookmarkWithAIMode
|
|
70
|
+
}, 1000)
|
|
71
|
+
|
|
68
72
|
return true
|
|
69
73
|
}
|
|
70
74
|
|
|
@@ -113,6 +113,7 @@ export default function BookmarkTreeSelect (props) {
|
|
|
113
113
|
const [expandedKeys, setExpandedKeys] = useState(() => deepCopy(propExpandedKeys || []))
|
|
114
114
|
const [checkedKeys, setCheckedKeys] = useState(() => deepCopy(propCheckedKeys || []))
|
|
115
115
|
const [searchText, setSearchText] = useState('')
|
|
116
|
+
const [refreshKey, setRefreshKey] = useState(0)
|
|
116
117
|
|
|
117
118
|
const onCheck = setCheckedKeys
|
|
118
119
|
|
|
@@ -122,6 +123,7 @@ export default function BookmarkTreeSelect (props) {
|
|
|
122
123
|
if (type === 'delete') {
|
|
123
124
|
store.delItems(arr, settingMap.bookmarks)
|
|
124
125
|
store.delItems(arr, settingMap.bookmarkGroups)
|
|
126
|
+
setRefreshKey(k => k + 1)
|
|
125
127
|
} else {
|
|
126
128
|
store.openBookmarks(arr)
|
|
127
129
|
if (props.onClose) {
|
|
@@ -141,7 +143,7 @@ export default function BookmarkTreeSelect (props) {
|
|
|
141
143
|
setCheckedKeys([])
|
|
142
144
|
}
|
|
143
145
|
|
|
144
|
-
const treeData = useMemo(() => buildData(bookmarks, bookmarkGroups, searchText), [bookmarks, bookmarkGroups, searchText])
|
|
146
|
+
const treeData = useMemo(() => buildData(bookmarks, bookmarkGroups, searchText), [bookmarks, bookmarkGroups, searchText, refreshKey])
|
|
145
147
|
|
|
146
148
|
// Auto expand parent nodes when searching
|
|
147
149
|
const handleExpand = (keys) => {
|
|
@@ -71,9 +71,6 @@ export default class TransportAction extends Component {
|
|
|
71
71
|
this.transport = null
|
|
72
72
|
this.fromFile = null
|
|
73
73
|
refsTransfers.remove(this.id)
|
|
74
|
-
if (this.isFtp) {
|
|
75
|
-
window.initingFtpTabIds?.delete(this.tabId)
|
|
76
|
-
}
|
|
77
74
|
}
|
|
78
75
|
|
|
79
76
|
localCheckExist = (path) => {
|
|
@@ -241,10 +238,11 @@ export default class TransportAction extends Component {
|
|
|
241
238
|
operation // 'mv' or 'cp'
|
|
242
239
|
} = transfer
|
|
243
240
|
|
|
244
|
-
|
|
241
|
+
// Use this.newPath when set (e.g. user chose rename from conflict modal)
|
|
242
|
+
let finalToPath = this.newPath || toPath
|
|
245
243
|
|
|
246
|
-
// Check if it's a copy operation to the same path
|
|
247
|
-
if (fromPath === toPath && operation === fileOperationsMap.cp) {
|
|
244
|
+
// Check if it's a copy operation to the same path (no rename decision pending)
|
|
245
|
+
if (!this.newPath && fromPath === toPath && operation === fileOperationsMap.cp) {
|
|
248
246
|
finalToPath = this.handleRename(toPath, typeFrom === typeMap.remote).newPath
|
|
249
247
|
transfer.toPath = finalToPath
|
|
250
248
|
this.update({
|
|
@@ -410,6 +408,10 @@ export default class TransportAction extends Component {
|
|
|
410
408
|
this.newName = newName
|
|
411
409
|
}
|
|
412
410
|
|
|
411
|
+
const { typeFrom, typeTo } = this.props.transfer
|
|
412
|
+
if (typeFrom === typeTo) {
|
|
413
|
+
return this.mvOrCp()
|
|
414
|
+
}
|
|
413
415
|
this.startTransfer()
|
|
414
416
|
}
|
|
415
417
|
|
|
@@ -758,9 +760,9 @@ export default class TransportAction extends Component {
|
|
|
758
760
|
|
|
759
761
|
const list = await this.list(typeFrom, fromPath, tabId)
|
|
760
762
|
const bigFileSize = 1024 * 1024
|
|
761
|
-
const smallFilesBatch =
|
|
762
|
-
const BigFilesBatch =
|
|
763
|
-
const foldersBatch =
|
|
763
|
+
const smallFilesBatch = 30
|
|
764
|
+
const BigFilesBatch = 3
|
|
765
|
+
const foldersBatch = 50
|
|
764
766
|
|
|
765
767
|
const {
|
|
766
768
|
folders,
|
|
@@ -9,8 +9,6 @@ import { maxTransport } from '../../common/constants'
|
|
|
9
9
|
import { refsStatic } from '../common/ref'
|
|
10
10
|
// import { action } from 'manate'
|
|
11
11
|
|
|
12
|
-
window.initingFtpTabIds = new Set()
|
|
13
|
-
|
|
14
12
|
export default class TransportsActionStore extends Component {
|
|
15
13
|
constructor (props) {
|
|
16
14
|
super(props)
|
|
@@ -84,9 +82,7 @@ export default class TransportsActionStore extends Component {
|
|
|
84
82
|
typeTo,
|
|
85
83
|
typeFrom,
|
|
86
84
|
inited,
|
|
87
|
-
id
|
|
88
|
-
tabType,
|
|
89
|
-
tabId
|
|
85
|
+
id
|
|
90
86
|
} = tr
|
|
91
87
|
|
|
92
88
|
const isTransfer = typeTo !== typeFrom
|
|
@@ -95,15 +91,6 @@ export default class TransportsActionStore extends Component {
|
|
|
95
91
|
continue
|
|
96
92
|
}
|
|
97
93
|
|
|
98
|
-
// For ftp transfers, ensure only one per tabId is inited
|
|
99
|
-
if (tabType === 'ftp') {
|
|
100
|
-
const hasInited = fileTransfers.some(t => t.tabId === tabId && t.inited && t.id !== id)
|
|
101
|
-
if (hasInited || window.initingFtpTabIds.has(tabId)) {
|
|
102
|
-
continue
|
|
103
|
-
}
|
|
104
|
-
window.initingFtpTabIds.add(tabId)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
94
|
if (count < maxTransport) {
|
|
108
95
|
count++
|
|
109
96
|
this.pendingInitIds.add(id)
|
|
@@ -34,7 +34,6 @@ import InputContextMenu from '../common/input-context-menu'
|
|
|
34
34
|
import WorkspaceSaveModal from '../tabs/workspace-save-modal'
|
|
35
35
|
import BookmarkFromHistoryModal from '../bookmark-form/bookmark-from-history-modal'
|
|
36
36
|
import AutoSync from '../setting-sync/auto-sync'
|
|
37
|
-
import AutoCheckUpdate from '../common/auto-check-update'
|
|
38
37
|
import BatchOpRunner from '../batch-op/batch-op-runner'
|
|
39
38
|
import { pick } from 'lodash-es'
|
|
40
39
|
import deepCopy from 'json-deep-copy'
|
|
@@ -292,7 +291,6 @@ export default auto(function Index (props) {
|
|
|
292
291
|
<TerminalCmdSuggestions {...cmdSuggestionsProps} />
|
|
293
292
|
<TransferQueue />
|
|
294
293
|
<AutoSync config={config} />
|
|
295
|
-
<AutoCheckUpdate config={config} />
|
|
296
294
|
<WorkspaceSaveModal store={store} />
|
|
297
295
|
<BookmarkFromHistoryModal />
|
|
298
296
|
<NotificationContainer />
|
|
@@ -214,10 +214,10 @@ export default class FileListTable extends Component {
|
|
|
214
214
|
this.currentFileId = id
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
renderItem = (item) => {
|
|
217
|
+
renderItem = (item, index) => {
|
|
218
218
|
const { type } = this.props
|
|
219
219
|
const cls = item.isParent ? 'parent-file-item' : 'real-file-item'
|
|
220
|
-
const key = item.id
|
|
220
|
+
const key = item.id ?? index + 'file-item'
|
|
221
221
|
const fileProps = {
|
|
222
222
|
...this.props.getFileProps(item, type),
|
|
223
223
|
cls,
|
|
@@ -143,11 +143,9 @@ export function getShellIntegrationCommand (shellType = 'bash') {
|
|
|
143
143
|
return wrapSilent(cmd, shellType)
|
|
144
144
|
}
|
|
145
145
|
export async function detectRemoteShell (pid) {
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
// This syntax is safe for Bash, Zsh, and Fish.
|
|
150
|
-
const cmd = 'fish --version 2>/dev/null | grep -q fish && echo fish || { env | grep -q ZSH_VERSION && echo zsh || { env | grep -q BASH_VERSION && echo bash || { ps -p $$ -o comm= 2>/dev/null || echo sh; }; }; }'
|
|
146
|
+
// SSH exec runs under the account shell, so prefer the configured shell path
|
|
147
|
+
// instead of probing for any shell binary installed on the host.
|
|
148
|
+
const cmd = 'printf "%s\n" "$SHELL"'
|
|
151
149
|
|
|
152
150
|
const r = await runCmd(pid, cmd)
|
|
153
151
|
.catch((err) => {
|
|
@@ -157,8 +155,9 @@ export async function detectRemoteShell (pid) {
|
|
|
157
155
|
|
|
158
156
|
const shell = r.trim().toLowerCase()
|
|
159
157
|
|
|
160
|
-
if (shell
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
158
|
+
if (!shell) {
|
|
159
|
+
return 'sh'
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return detectShellType(shell)
|
|
164
163
|
}
|
|
@@ -1,9 +1,29 @@
|
|
|
1
|
+
import { memo } from 'react'
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
CaretDownOutlined,
|
|
3
5
|
CaretRightOutlined
|
|
4
6
|
} from '@ant-design/icons'
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
function hasChildren (group) {
|
|
9
|
+
return Boolean(
|
|
10
|
+
group?.bookmarkIds?.length ||
|
|
11
|
+
group?.bookmarkGroupIds?.length
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isOpen (props) {
|
|
16
|
+
return Boolean(props.keyword) || props.expandedKeys.includes(props.group.id)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function areEqual (prevProps, nextProps) {
|
|
20
|
+
return prevProps.group?.id === nextProps.group?.id &&
|
|
21
|
+
hasChildren(prevProps.group) === hasChildren(nextProps.group) &&
|
|
22
|
+
Boolean(prevProps.keyword) === Boolean(nextProps.keyword) &&
|
|
23
|
+
isOpen(prevProps) === isOpen(nextProps)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function TreeExpander (props) {
|
|
7
27
|
function onExpand (e) {
|
|
8
28
|
e.stopPropagation()
|
|
9
29
|
props.onExpand(group)
|
|
@@ -35,3 +55,5 @@ export default function TreeExpander (props) {
|
|
|
35
55
|
</div>
|
|
36
56
|
)
|
|
37
57
|
}
|
|
58
|
+
|
|
59
|
+
export default memo(TreeExpander, areEqual)
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* tree list for bookmarks
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { memo, useState } from 'react'
|
|
6
|
+
|
|
5
7
|
import {
|
|
6
8
|
CloseOutlined,
|
|
7
9
|
CopyOutlined,
|
|
@@ -24,7 +26,31 @@ import uid from '../../common/uid'
|
|
|
24
26
|
|
|
25
27
|
const e = window.translate
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
function getItemLabel (item, isGroup) {
|
|
30
|
+
return isGroup
|
|
31
|
+
? item?.title || ''
|
|
32
|
+
: createName(item)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function areEqual (prevProps, nextProps) {
|
|
36
|
+
const prevSelected = prevProps.selectedItemId === prevProps.item.id
|
|
37
|
+
const nextSelected = nextProps.selectedItemId === nextProps.item.id
|
|
38
|
+
|
|
39
|
+
return prevProps.isGroup === nextProps.isGroup &&
|
|
40
|
+
prevProps.parentId === nextProps.parentId &&
|
|
41
|
+
prevProps.staticList === nextProps.staticList &&
|
|
42
|
+
prevProps.keyword === nextProps.keyword &&
|
|
43
|
+
prevSelected === nextSelected &&
|
|
44
|
+
prevProps.item.id === nextProps.item.id &&
|
|
45
|
+
prevProps.item.level === nextProps.item.level &&
|
|
46
|
+
prevProps.item.color === nextProps.item.color &&
|
|
47
|
+
prevProps.item.description === nextProps.item.description &&
|
|
48
|
+
getItemLabel(prevProps.item, prevProps.isGroup) === getItemLabel(nextProps.item, nextProps.isGroup)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function TreeListItem (props) {
|
|
52
|
+
const [hovered, setHovered] = useState(false)
|
|
53
|
+
|
|
28
54
|
const handleDel = (e) => {
|
|
29
55
|
props.del(props.item, e)
|
|
30
56
|
}
|
|
@@ -225,6 +251,8 @@ export default function TreeListItem (props) {
|
|
|
225
251
|
'data-item-id': item.id,
|
|
226
252
|
'data-parent-id': props.parentId,
|
|
227
253
|
'data-is-group': isGroup ? 'true' : 'false',
|
|
254
|
+
onMouseEnter: () => setHovered(true),
|
|
255
|
+
onMouseLeave: () => setHovered(false),
|
|
228
256
|
onDragOver,
|
|
229
257
|
onDragStart,
|
|
230
258
|
onDragEnter,
|
|
@@ -250,18 +278,20 @@ export default function TreeListItem (props) {
|
|
|
250
278
|
{colorTag}{tag}{titleHighlight}
|
|
251
279
|
</div>
|
|
252
280
|
{
|
|
253
|
-
isGroup
|
|
281
|
+
hovered && isGroup
|
|
254
282
|
? renderGroupBtns()
|
|
255
283
|
: null
|
|
256
284
|
}
|
|
257
285
|
{
|
|
258
|
-
!isGroup
|
|
286
|
+
hovered && !isGroup
|
|
259
287
|
? renderDuplicateBtn()
|
|
260
288
|
: null
|
|
261
289
|
}
|
|
262
|
-
{renderOperationBtn()}
|
|
263
|
-
{renderDelBtn()}
|
|
264
|
-
{renderEditBtn()}
|
|
290
|
+
{hovered ? renderOperationBtn() : null}
|
|
291
|
+
{hovered ? renderDelBtn() : null}
|
|
292
|
+
{hovered ? renderEditBtn() : null}
|
|
265
293
|
</div>
|
|
266
294
|
)
|
|
267
295
|
}
|
|
296
|
+
|
|
297
|
+
export default memo(TreeListItem, areEqual)
|
package/package.json
CHANGED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef } from 'react'
|
|
2
|
-
|
|
3
|
-
export default function AutoCheckUpdate ({ config }) {
|
|
4
|
-
const lastCheckTimeRef = useRef(0)
|
|
5
|
-
const intervalIdRef = useRef(null)
|
|
6
|
-
|
|
7
|
-
useEffect(() => {
|
|
8
|
-
if (!config.checkUpdateOnStart) {
|
|
9
|
-
clearInterval(intervalIdRef.current)
|
|
10
|
-
return
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const checkForUpdate = () => {
|
|
14
|
-
const { store } = window
|
|
15
|
-
if (store.config.checkUpdateOnStart) {
|
|
16
|
-
store.onCheckUpdate(false)
|
|
17
|
-
}
|
|
18
|
-
lastCheckTimeRef.current = Date.now()
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
intervalIdRef.current = setInterval(checkForUpdate, 60 * 60 * 1000)
|
|
22
|
-
|
|
23
|
-
return () => {
|
|
24
|
-
if (intervalIdRef.current) {
|
|
25
|
-
clearInterval(intervalIdRef.current)
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}, [config.checkUpdateOnStart])
|
|
29
|
-
|
|
30
|
-
return null
|
|
31
|
-
}
|