@electerm/electerm-react 5.5.35 → 5.5.56
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/bookmark-schemas.js +1 -0
- package/client/common/constants.js +1 -0
- package/client/common/parse-quick-connect.js +1 -0
- package/client/common/parse-widget-log.js +57 -0
- package/client/components/ai/agent-tool-call-card.jsx +7 -3
- package/client/components/ai/agent.js +176 -33
- package/client/components/ai/ai-auto-compress.js +119 -0
- package/client/components/ai/ai-chat-history-item.jsx +125 -40
- package/client/components/ai/ai-chat.jsx +67 -19
- package/client/components/ai/ai-compress.js +95 -0
- package/client/components/ai/ai-config.jsx +29 -16
- package/client/components/ai/ai-config.styl +20 -0
- package/client/components/ai/ai-context.js +1 -1
- package/client/components/ai/ai-output.jsx +1 -6
- package/client/components/ai/ai-presets.js +0 -15
- package/client/components/batch-op/batch-op-runner.jsx +1 -0
- package/client/components/bookmark-form/bookmark-schema.js +1 -0
- package/client/components/bookmark-form/common/ssh-agent.jsx +31 -21
- package/client/components/bookmark-form/config/ssh.js +1 -0
- package/client/components/bookmark-form/fix-bookmark-default.js +1 -0
- package/client/components/file-transfer/transfer.jsx +3 -119
- package/client/components/main/main.jsx +1 -0
- package/client/components/session/session-control.jsx +35 -2
- package/client/components/session/session-control.styl +5 -0
- package/client/components/session/session.jsx +13 -0
- package/client/components/setting-panel/tab-widgets.jsx +27 -7
- package/client/components/sftp/file-item.jsx +0 -13
- package/client/components/terminal/mixins/term-touch.js +18 -0
- package/client/components/widgets/widget-control.jsx +4 -2
- package/client/components/widgets/widget-instance-detail.jsx +370 -0
- package/client/components/widgets/widget-instance.jsx +30 -13
- package/client/components/widgets/widget-instances.jsx +3 -1
- package/client/components/widgets/widgets-list.jsx +8 -0
- package/client/components/widgets/widgets.styl +100 -0
- package/client/store/common.js +81 -23
- package/client/store/init-state.js +7 -0
- package/client/store/load-data.js +1 -0
- package/client/store/widgets.js +23 -5
- package/package.json +1 -1
- package/client/components/file-transfer/zip.js +0 -42
|
@@ -9,26 +9,36 @@ const e = window.translate
|
|
|
9
9
|
|
|
10
10
|
export default function SshAgent () {
|
|
11
11
|
return (
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
12
|
+
<>
|
|
13
|
+
<FormItem
|
|
14
|
+
{...formItemLayout}
|
|
15
|
+
label={e('useSshAgent')}
|
|
16
|
+
>
|
|
17
|
+
<Space align='center'>
|
|
18
|
+
<FormItem
|
|
19
|
+
name='useSshAgent'
|
|
20
|
+
valuePropName='checked'
|
|
21
|
+
noStyle
|
|
22
|
+
>
|
|
23
|
+
<SwitchLabel />
|
|
24
|
+
</FormItem>
|
|
25
|
+
<FormItem
|
|
26
|
+
name='sshAgent'
|
|
27
|
+
noStyle
|
|
28
|
+
>
|
|
29
|
+
<Input placeholder={e('SSH Agent Path')} />
|
|
30
|
+
</FormItem>
|
|
31
|
+
<HelpIcon link='https://github.com/electerm/electerm/wiki/ssh-agent' />
|
|
32
|
+
</Space>
|
|
33
|
+
</FormItem>
|
|
34
|
+
<FormItem
|
|
35
|
+
{...formItemLayout}
|
|
36
|
+
label={e('agentForward')}
|
|
37
|
+
name='agentForward'
|
|
38
|
+
valuePropName='checked'
|
|
39
|
+
>
|
|
40
|
+
<SwitchLabel />
|
|
41
|
+
</FormItem>
|
|
42
|
+
</>
|
|
33
43
|
)
|
|
34
44
|
}
|
|
@@ -12,13 +12,6 @@ import {
|
|
|
12
12
|
import resolve from '../../common/resolve'
|
|
13
13
|
import sanitizeFilename from '../../common/sanitize-filename'
|
|
14
14
|
import { refsTransfers, refsStatic, refs } from '../common/ref'
|
|
15
|
-
import {
|
|
16
|
-
zipCmd,
|
|
17
|
-
unzipCmd,
|
|
18
|
-
rmCmd,
|
|
19
|
-
mvCmd,
|
|
20
|
-
mkdirCmd
|
|
21
|
-
} from './zip'
|
|
22
15
|
import './transfer.styl'
|
|
23
16
|
|
|
24
17
|
const { assign } = Object
|
|
@@ -279,9 +272,7 @@ export default class TransportAction extends Component {
|
|
|
279
272
|
typeFrom,
|
|
280
273
|
toFile = {}
|
|
281
274
|
} = transfer
|
|
282
|
-
const toPath = transfer.
|
|
283
|
-
? transfer.toPath
|
|
284
|
-
: this.newPath || transfer.toPath
|
|
275
|
+
const toPath = this.newPath || transfer.toPath
|
|
285
276
|
const fromFile = transfer.fromFile || this.fromFile
|
|
286
277
|
const fromMode = fromFile.mode
|
|
287
278
|
const transferType = typeFrom === typeMap.local ? transferTypeMap.upload : transferTypeMap.download
|
|
@@ -408,12 +399,11 @@ export default class TransportAction extends Component {
|
|
|
408
399
|
toPath
|
|
409
400
|
} = this.props.transfer
|
|
410
401
|
this.oldPath = toPath
|
|
411
|
-
const { newPath
|
|
402
|
+
const { newPath } = this.handleRename(toPath, typeTo === typeMap.remote)
|
|
412
403
|
this.update({
|
|
413
404
|
toPath: newPath
|
|
414
405
|
})
|
|
415
406
|
this.newPath = newPath
|
|
416
|
-
this.newName = newName
|
|
417
407
|
}
|
|
418
408
|
|
|
419
409
|
const { typeFrom, typeTo } = this.props.transfer
|
|
@@ -423,120 +413,14 @@ export default class TransportAction extends Component {
|
|
|
423
413
|
this.startTransfer()
|
|
424
414
|
}
|
|
425
415
|
|
|
426
|
-
zipTransferFolder = async () => {
|
|
427
|
-
const {
|
|
428
|
-
transfer
|
|
429
|
-
} = this.props
|
|
430
|
-
const {
|
|
431
|
-
fromPath,
|
|
432
|
-
typeFrom
|
|
433
|
-
} = transfer
|
|
434
|
-
const toPath = this.oldPath || transfer.toPath
|
|
435
|
-
let p
|
|
436
|
-
let isFromRemote
|
|
437
|
-
if (typeFrom === typeMap.local) {
|
|
438
|
-
isFromRemote = false
|
|
439
|
-
p = await window.fs.zipFolder(fromPath)
|
|
440
|
-
} else {
|
|
441
|
-
isFromRemote = true
|
|
442
|
-
const terminalId = refs.get('sftp-' + this.tabId)?.terminalId
|
|
443
|
-
p = await zipCmd(terminalId, fromPath)
|
|
444
|
-
}
|
|
445
|
-
this.zipSrc = p
|
|
446
|
-
const { name } = getFolderFromFilePath(p, isFromRemote)
|
|
447
|
-
const { path } = getFolderFromFilePath(toPath, !isFromRemote)
|
|
448
|
-
const nTo = resolve(path, name)
|
|
449
|
-
this.zipPath = nTo
|
|
450
|
-
const newTrans1 = {
|
|
451
|
-
...copy(transfer),
|
|
452
|
-
toPath: nTo,
|
|
453
|
-
fromPath: p
|
|
454
|
-
}
|
|
455
|
-
this.transferFile(newTrans1, this.unzipFile)
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
unzipFile = async () => {
|
|
459
|
-
const { transfer } = this.props
|
|
460
|
-
const {
|
|
461
|
-
typeTo
|
|
462
|
-
} = transfer
|
|
463
|
-
const toPath = this.zipPath
|
|
464
|
-
const fromPath = this.zipSrc
|
|
465
|
-
const isToRemote = typeTo === typeMap.remote
|
|
466
|
-
const {
|
|
467
|
-
path,
|
|
468
|
-
name,
|
|
469
|
-
targetPath
|
|
470
|
-
} = this.buildUnzipPath(transfer)
|
|
471
|
-
const {
|
|
472
|
-
newName,
|
|
473
|
-
terminalId
|
|
474
|
-
} = this
|
|
475
|
-
if (isToRemote) {
|
|
476
|
-
if (newName) {
|
|
477
|
-
await mkdirCmd(terminalId, path)
|
|
478
|
-
}
|
|
479
|
-
await unzipCmd(terminalId, toPath, path)
|
|
480
|
-
if (newName) {
|
|
481
|
-
const mvFrom = resolve(path, name)
|
|
482
|
-
const mvTo = resolve(targetPath, newName)
|
|
483
|
-
await mvCmd(terminalId, mvFrom, mvTo)
|
|
484
|
-
}
|
|
485
|
-
} else {
|
|
486
|
-
if (newName) {
|
|
487
|
-
await window.fs.mkdir(path)
|
|
488
|
-
}
|
|
489
|
-
await window.fs.unzipFile(toPath, path)
|
|
490
|
-
if (newName) {
|
|
491
|
-
const mvFrom = resolve(path, name)
|
|
492
|
-
const mvTo = resolve(targetPath, newName)
|
|
493
|
-
await window.fs.mv(mvFrom, mvTo)
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
await rmCmd(terminalId, !isToRemote ? fromPath : toPath)
|
|
497
|
-
await window.fs.rmrf(!isToRemote ? toPath : fromPath)
|
|
498
|
-
if (newName) {
|
|
499
|
-
if (isToRemote) {
|
|
500
|
-
await rmCmd(terminalId, path)
|
|
501
|
-
} else {
|
|
502
|
-
await window.fs.rmrf(path)
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
this.onEnd()
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
buildUnzipPath = (transfer) => {
|
|
509
|
-
const {
|
|
510
|
-
typeTo
|
|
511
|
-
} = transfer
|
|
512
|
-
const isToRemote = typeTo === typeMap.remote
|
|
513
|
-
const toPath = this.oldPath || transfer.toPath
|
|
514
|
-
const {
|
|
515
|
-
newName
|
|
516
|
-
} = this
|
|
517
|
-
const { path } = getFolderFromFilePath(toPath, isToRemote)
|
|
518
|
-
const oldName = getFolderFromFilePath(toPath, isToRemote).name
|
|
519
|
-
const np = newName
|
|
520
|
-
? resolve(path, 'temp-' + newName)
|
|
521
|
-
: path
|
|
522
|
-
return {
|
|
523
|
-
targetPath: path,
|
|
524
|
-
path: np,
|
|
525
|
-
name: oldName
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
416
|
startTransfer = async () => {
|
|
530
|
-
const { fromFile = this.fromFile
|
|
417
|
+
const { fromFile = this.fromFile } = this.props.transfer
|
|
531
418
|
if (!fromFile) {
|
|
532
419
|
return
|
|
533
420
|
}
|
|
534
421
|
if (!fromFile.isDirectory) {
|
|
535
422
|
return this.transferFile()
|
|
536
423
|
}
|
|
537
|
-
if (zip) {
|
|
538
|
-
return this.zipTransferFolder()
|
|
539
|
-
}
|
|
540
424
|
if (!this.isFtp) {
|
|
541
425
|
return this.transferFile()
|
|
542
426
|
} else {
|
|
@@ -289,6 +289,7 @@ export default auto(function Index (props) {
|
|
|
289
289
|
rightPanelTab,
|
|
290
290
|
agentRunning: store.agentRunning,
|
|
291
291
|
aiContextInfo: store.aiContextInfo,
|
|
292
|
+
aiAutoCompress: store.aiAutoCompress,
|
|
292
293
|
currentChatSessionId: store.currentChatSessionId,
|
|
293
294
|
showChatSessions: store.showChatSessions
|
|
294
295
|
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
PaperClipOutlined,
|
|
9
9
|
ApartmentOutlined,
|
|
10
10
|
MoreOutlined,
|
|
11
|
+
MenuOutlined,
|
|
11
12
|
ColumnWidthOutlined,
|
|
12
13
|
LogoutOutlined,
|
|
13
14
|
PlusOutlined,
|
|
@@ -39,6 +40,7 @@ export default function SessionControl (props) {
|
|
|
39
40
|
keepaliveEnabled,
|
|
40
41
|
broadcastInput,
|
|
41
42
|
wrapDisabled,
|
|
43
|
+
pasteWhenContextMenu,
|
|
42
44
|
onChangePane,
|
|
43
45
|
toggleCheckSftpPathFollowSsh,
|
|
44
46
|
onSshSftpSplitView,
|
|
@@ -47,6 +49,7 @@ export default function SessionControl (props) {
|
|
|
47
49
|
toggleWrap,
|
|
48
50
|
onFullscreen,
|
|
49
51
|
onOpenSearch,
|
|
52
|
+
onOpenContextMenu,
|
|
50
53
|
onExitGracefully,
|
|
51
54
|
onZoomFontSize
|
|
52
55
|
} = props
|
|
@@ -279,7 +282,35 @@ export default function SessionControl (props) {
|
|
|
279
282
|
)
|
|
280
283
|
}
|
|
281
284
|
|
|
282
|
-
|
|
285
|
+
/**
|
|
286
|
+
* Touch devices have no right click, so the terminal's context menu needs a
|
|
287
|
+
* visible way in: this icon, next to the fullscreen icon on the desktop
|
|
288
|
+
* toolbar and next to the three-dot toggle in the mobile one (see below).
|
|
289
|
+
* Hidden when there is no menu to open — `pasteWhenContextMenu` turns both
|
|
290
|
+
* right click and the long press into a paste (see terminal.jsx), and it
|
|
291
|
+
* only ever acts on the terminal pane.
|
|
292
|
+
*/
|
|
293
|
+
function renderContextMenuIcon (inMobileBar) {
|
|
294
|
+
if (
|
|
295
|
+
!isTouchDevice ||
|
|
296
|
+
pasteWhenContextMenu ||
|
|
297
|
+
tab.pane !== paneMap.terminal
|
|
298
|
+
) {
|
|
299
|
+
return null
|
|
300
|
+
}
|
|
301
|
+
const cls = classnames(
|
|
302
|
+
'iblock pointer spliter context-menu-control-icon',
|
|
303
|
+
inMobileBar ? 'mobile-context-menu-toggle' : 'mg1r icon-info'
|
|
304
|
+
)
|
|
305
|
+
return (
|
|
306
|
+
<MenuOutlined
|
|
307
|
+
className={cls}
|
|
308
|
+
onClick={onOpenContextMenu}
|
|
309
|
+
/>
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function renderTermControls (skipContextMenuIcon) {
|
|
283
314
|
const { pane } = tab
|
|
284
315
|
if (pane !== paneMap.terminal) {
|
|
285
316
|
return null
|
|
@@ -287,6 +318,7 @@ export default function SessionControl (props) {
|
|
|
287
318
|
return (
|
|
288
319
|
<div className='fright term-controls'>
|
|
289
320
|
{renderFontSizeIcons()}
|
|
321
|
+
{skipContextMenuIcon ? null : renderContextMenuIcon()}
|
|
290
322
|
{renderFullscreenIcon()}
|
|
291
323
|
{renderSearchIcon()}
|
|
292
324
|
</div>
|
|
@@ -303,12 +335,13 @@ export default function SessionControl (props) {
|
|
|
303
335
|
{renderBroadcastIcon()}
|
|
304
336
|
{renderWrapIcon()}
|
|
305
337
|
{renderExitGracefullyIcon()}
|
|
306
|
-
{renderTermControls()}
|
|
338
|
+
{renderTermControls(true)}
|
|
307
339
|
</div>
|
|
308
340
|
)
|
|
309
341
|
return (
|
|
310
342
|
<div className='terminal-control mobile-session-control'>
|
|
311
343
|
{renderPaneControl()}
|
|
344
|
+
{renderContextMenuIcon(true)}
|
|
312
345
|
<Popover
|
|
313
346
|
content={extraIcons}
|
|
314
347
|
trigger='click'
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
flex 1
|
|
15
15
|
min-width 0
|
|
16
16
|
.mobile-control-toggle
|
|
17
|
+
.mobile-context-menu-toggle
|
|
17
18
|
font-size 16px
|
|
18
19
|
line-height 32px
|
|
19
20
|
color var(--text)
|
|
@@ -21,6 +22,10 @@
|
|
|
21
22
|
&:hover
|
|
22
23
|
color var(--text-light)
|
|
23
24
|
|
|
25
|
+
// touch-only terminal context menu icon, in the bar next to the toggle above
|
|
26
|
+
.mobile-context-menu-toggle
|
|
27
|
+
margin-right 10px
|
|
28
|
+
|
|
24
29
|
// Popover content — icons laid out in a row, wrapping as needed
|
|
25
30
|
.mobile-control-icons
|
|
26
31
|
display flex
|
|
@@ -480,6 +480,17 @@ export default class SessionWrapper extends Component {
|
|
|
480
480
|
refs.get('term-' + this.props.tab.id)?.toggleSearch()
|
|
481
481
|
}
|
|
482
482
|
|
|
483
|
+
// touch-only: the toolbar menu icon opens the same context menu a right
|
|
484
|
+
// click (or a long press) does, anchored just below the icon itself
|
|
485
|
+
handleOpenContextMenu = (evt) => {
|
|
486
|
+
const term = refs.get('term-' + this.props.tab.id)
|
|
487
|
+
if (!term) {
|
|
488
|
+
return
|
|
489
|
+
}
|
|
490
|
+
const rect = evt.currentTarget.getBoundingClientRect()
|
|
491
|
+
term.openContextMenuAtPoint(rect.left, rect.bottom)
|
|
492
|
+
}
|
|
493
|
+
|
|
483
494
|
handleZoomFontSize = (v) => {
|
|
484
495
|
const term = refs.get('term-' + this.props.tab.id)
|
|
485
496
|
if (!term?.term) {
|
|
@@ -625,6 +636,7 @@ export default class SessionWrapper extends Component {
|
|
|
625
636
|
keepaliveEnabled={this.state.keepaliveEnabled}
|
|
626
637
|
broadcastInput={this.state.broadcastInput}
|
|
627
638
|
wrapDisabled={this.state.wrapDisabled}
|
|
639
|
+
pasteWhenContextMenu={!!this.props.config.pasteWhenContextMenu}
|
|
628
640
|
onChangePane={(pane) => this.onChangePane(pane)}
|
|
629
641
|
toggleCheckSftpPathFollowSsh={this.toggleCheckSftpPathFollowSsh}
|
|
630
642
|
onSshSftpSplitView={this.handleSshSftpSplitView}
|
|
@@ -634,6 +646,7 @@ export default class SessionWrapper extends Component {
|
|
|
634
646
|
onFullscreen={this.handleFullscreen}
|
|
635
647
|
onZoomFontSize={this.handleZoomFontSize}
|
|
636
648
|
onOpenSearch={this.handleOpenSearch}
|
|
649
|
+
onOpenContextMenu={this.handleOpenContextMenu}
|
|
637
650
|
onExitGracefully={this.handleExitGracefully}
|
|
638
651
|
/>
|
|
639
652
|
<Suspense fallback={null}>
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import SettingCol from './col'
|
|
2
2
|
import WidgetControl from '../widgets/widget-control'
|
|
3
|
+
import WidgetInstanceDetail from '../widgets/widget-instance-detail'
|
|
3
4
|
import WidgetList from '../widgets/widgets-list'
|
|
5
|
+
import { auto } from 'manate/react'
|
|
4
6
|
import {
|
|
5
7
|
settingMap
|
|
6
8
|
} from '../../common/constants'
|
|
7
9
|
|
|
8
|
-
export default function TabWidgets (props) {
|
|
10
|
+
export default auto(function TabWidgets (props) {
|
|
9
11
|
const {
|
|
10
12
|
settingTab
|
|
11
13
|
} = props
|
|
@@ -15,8 +17,14 @@ export default function TabWidgets (props) {
|
|
|
15
17
|
const {
|
|
16
18
|
settingItem,
|
|
17
19
|
listProps,
|
|
18
|
-
formProps
|
|
20
|
+
formProps,
|
|
21
|
+
store
|
|
19
22
|
} = props
|
|
23
|
+
// The right column shows whatever the list selected: a widget definition (run
|
|
24
|
+
// form) or a running instance (its detail + log). A definition has `info`, an
|
|
25
|
+
// instance has `widgetId` — and an instance that has been stopped stays the
|
|
26
|
+
// selected item on purpose, so its log is still readable after it dies.
|
|
27
|
+
const isInstance = !!(settingItem && settingItem.widgetId)
|
|
20
28
|
return (
|
|
21
29
|
<div
|
|
22
30
|
className='setting-tabs-profile'
|
|
@@ -25,11 +33,23 @@ export default function TabWidgets (props) {
|
|
|
25
33
|
<WidgetList
|
|
26
34
|
{...listProps}
|
|
27
35
|
/>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
{
|
|
37
|
+
isInstance
|
|
38
|
+
? (
|
|
39
|
+
<WidgetInstanceDetail
|
|
40
|
+
key={settingItem.id}
|
|
41
|
+
instance={settingItem}
|
|
42
|
+
store={store}
|
|
43
|
+
/>
|
|
44
|
+
)
|
|
45
|
+
: (
|
|
46
|
+
<WidgetControl
|
|
47
|
+
{...formProps}
|
|
48
|
+
key={settingItem.id}
|
|
49
|
+
/>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
32
52
|
</SettingCol>
|
|
33
53
|
</div>
|
|
34
54
|
)
|
|
35
|
-
}
|
|
55
|
+
})
|
|
@@ -902,12 +902,6 @@ export default class FileSection extends React.Component {
|
|
|
902
902
|
this.transfer()
|
|
903
903
|
}
|
|
904
904
|
|
|
905
|
-
zipAndTransfer = async () => {
|
|
906
|
-
this.transfer(transfer => {
|
|
907
|
-
transfer.zip = true
|
|
908
|
-
})
|
|
909
|
-
}
|
|
910
|
-
|
|
911
905
|
newFile = () => {
|
|
912
906
|
return this.newItem(false)
|
|
913
907
|
}
|
|
@@ -1098,13 +1092,6 @@ export default class FileSection extends React.Component {
|
|
|
1098
1092
|
icon: iconType,
|
|
1099
1093
|
text: transferText
|
|
1100
1094
|
})
|
|
1101
|
-
// if (isDirectory && !this.props.isFtp) {
|
|
1102
|
-
// res.push({
|
|
1103
|
-
// func: 'zipAndTransfer',
|
|
1104
|
-
// icon: 'FileZipOutlined',
|
|
1105
|
-
// text: e('compressAndTransfer')
|
|
1106
|
-
// })
|
|
1107
|
-
// }
|
|
1108
1095
|
}
|
|
1109
1096
|
if (!isDirectory && isRealFile && isLocal) {
|
|
1110
1097
|
res.push({
|
|
@@ -41,6 +41,10 @@
|
|
|
41
41
|
* `stopPropagation()` is enough to veto xterm's scroll for the duration of
|
|
42
42
|
* the drag. `preventDefault()` is not an option: React listens to
|
|
43
43
|
* `touchmove` passively, so it is ignored (with a console warning).
|
|
44
|
+
*
|
|
45
|
+
* A long press is not the only way in: the toolbar shows a touch-only menu
|
|
46
|
+
* icon next to the three-dot toggle (see session-control.jsx), which opens the
|
|
47
|
+
* same menu through openContextMenuAtPoint() below.
|
|
44
48
|
*/
|
|
45
49
|
|
|
46
50
|
const DEFAULT_WORD_SEPARATOR = './\\()"\'-:,.;<>~!@#$%^&*|+=[]{}`~ ?'
|
|
@@ -505,6 +509,20 @@ export const touchMixin = {
|
|
|
505
509
|
target.dispatchEvent(event)
|
|
506
510
|
},
|
|
507
511
|
|
|
512
|
+
/**
|
|
513
|
+
* Opens the menu from the toolbar icon, which lives outside the antd
|
|
514
|
+
* dropdown's trigger element — so the event is dispatched on the terminal's
|
|
515
|
+
* own node and bubbles up to it. The icon's corner is passed in as the point
|
|
516
|
+
* to drop the menu from, rather than the finger that tapped it.
|
|
517
|
+
*/
|
|
518
|
+
openContextMenuAtPoint (clientX, clientY) {
|
|
519
|
+
this.openContextMenuAt(
|
|
520
|
+
clientX,
|
|
521
|
+
clientY,
|
|
522
|
+
this.domRef?.current || this.term?.element
|
|
523
|
+
)
|
|
524
|
+
},
|
|
525
|
+
|
|
508
526
|
cancelLongPress () {
|
|
509
527
|
clearTimeout(this.longPressTimer)
|
|
510
528
|
this.longPressTimer = null
|
|
@@ -46,9 +46,11 @@ export default function WidgetControl ({ formData, widgetInstancesLength }) {
|
|
|
46
46
|
title: `${widget.info.name} (${result.instanceId})`,
|
|
47
47
|
widgetId: result.widgetId,
|
|
48
48
|
serverInfo: result.serverInfo,
|
|
49
|
-
config
|
|
49
|
+
config,
|
|
50
|
+
startedAt: Date.now()
|
|
50
51
|
}
|
|
51
|
-
|
|
52
|
+
// assigned, not pushed: see deleteWidgetInstance in store/widgets.js
|
|
53
|
+
window.store.widgetInstances = [...window.store.widgetInstances, instance]
|
|
52
54
|
if (config.autoRun) {
|
|
53
55
|
window.store.toggleAutoRunWidget(instance)
|
|
54
56
|
}
|