@electerm/electerm-react 3.15.175 → 3.15.186
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 +2 -0
- package/client/common/drop-position.js +59 -0
- package/client/components/bookmark-form/bookmark-schema.js +2 -0
- package/client/components/bookmark-form/common/connection-hopping.jsx +33 -7
- package/client/components/bookmark-form/common/ssh-tunnel-form.jsx +25 -7
- package/client/components/bookmark-form/common/ssh-tunnels.jsx +1 -1
- package/client/components/bookmark-form/config/serial.js +14 -0
- package/client/components/main/term-fullscreen.styl +5 -0
- package/client/components/quick-commands/on-drop.js +6 -1
- package/client/components/quick-commands/quick-commands-box.jsx +18 -2
- package/client/components/quick-commands/quick-commands-list-form.jsx +15 -4
- package/client/components/quick-commands/quick-commands-list.jsx +18 -2
- package/client/components/session/session-control.jsx +331 -0
- package/client/components/session/session-control.styl +38 -0
- package/client/components/session/session.jsx +41 -246
- package/client/components/session/session.styl +1 -0
- package/client/components/sftp/address-bookmark-item.jsx +15 -2
- package/client/components/sftp/code-compare-utils.js +260 -0
- package/client/components/sftp/code-compare.jsx +183 -0
- package/client/components/sftp/code-compare.styl +13 -0
- package/client/components/sftp/file-compare-modal.jsx +87 -42
- package/client/components/shortcuts/shortcut-handler.js +20 -0
- package/client/components/sidebar/transport-ui.jsx +21 -14
- package/client/components/tabs/index.jsx +18 -3
- package/client/components/tabs/tab.jsx +27 -8
- package/client/components/tabs/tabs.styl +8 -0
- package/client/components/terminal/attach-addon-custom.js +147 -36
- package/client/components/terminal/highlight-addon.js +26 -8
- package/client/components/terminal/term-search.jsx +1 -1
- package/client/components/terminal/terminal.jsx +193 -2
- package/client/components/terminal/terminal.styl +0 -2
- package/client/components/terminal-info/run-cmd.jsx +1 -1
- package/client/components/tree-list/tree-list.jsx +57 -39
- package/client/css/basic.styl +14 -0
- package/client/css/mobile.styl +31 -9
- package/client/store/item.js +10 -6
- package/package.json +1 -1
|
@@ -99,6 +99,8 @@ export const serialBookmarkSchema = {
|
|
|
99
99
|
xany: z.boolean().optional().describe('XANY flow control'),
|
|
100
100
|
txLineEnding: z.enum(['\r', '\n', '\r\n']).optional().describe('TX line ending appended on Enter: "\\r" (CR, default), "\\n" (LF), "\\r\\n" (CR+LF)'),
|
|
101
101
|
rxLineEnding: z.enum(['none', 'lf_to_crlf', 'cr_to_crlf']).optional().describe('RX line ending conversion: "none" (pass-through, default), "lf_to_crlf" (LF→CRLF for LF-only devices), "cr_to_crlf" (CR→CRLF for CR-only devices)'),
|
|
102
|
+
closeSequence: z.string().optional().describe('Key sequence sent to the serial port when the user clicks "exit gracefully" in the terminal controls (e.g. to cleanly exit GNU screen before disconnecting a Bluetooth serial console). Supports \\n \\t \\r \\\\ and \\xHH hex bytes, default "\\x01ky" (Ctrl+A, k, y - GNU screen kill-window confirm)'),
|
|
103
|
+
closeSequenceDelay: z.number().optional().describe('Milliseconds to wait after sending closeSequence before actually closing the port, default 500'),
|
|
102
104
|
description: z.string().optional().describe('Bookmark description')
|
|
103
105
|
}
|
|
104
106
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide whether a dragged item should be inserted
|
|
3
|
+
* AFTER the drop target, based on which half of the
|
|
4
|
+
* target element the pointer is currently in.
|
|
5
|
+
*
|
|
6
|
+
* This is the fix for the "can not drag to the last
|
|
7
|
+
* position" bug: splitting each item into a top/bottom
|
|
8
|
+
* (or left/right) half gives a valid drop zone after the
|
|
9
|
+
* last item, so the insertion index can reach `length`
|
|
10
|
+
* (append) instead of being capped at `length - 1`.
|
|
11
|
+
*
|
|
12
|
+
* @param {Object} e native drag event (needs clientX/clientY)
|
|
13
|
+
* @param {HTMLElement} el resolved drop target element
|
|
14
|
+
* @param {boolean} horizontal true for horizontal lists (e.g. tabs)
|
|
15
|
+
* @returns {boolean} true => insert the dragged item after the target
|
|
16
|
+
*/
|
|
17
|
+
export function isDropAfterHalf (e, el, horizontal = false) {
|
|
18
|
+
if (!el) {
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
const rect = el.getBoundingClientRect()
|
|
22
|
+
if (!rect.height && !rect.width) {
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
if (horizontal) {
|
|
26
|
+
return e.clientX > rect.left + rect.width / 2
|
|
27
|
+
}
|
|
28
|
+
return e.clientY > rect.top + rect.height / 2
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const DROP_BEFORE_CLS = 'dnd-before'
|
|
32
|
+
const DROP_AFTER_CLS = 'dnd-after'
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Show a directional drop indicator on the target element.
|
|
36
|
+
* first half => top edge (insert before)
|
|
37
|
+
* second half => bottom edge (insert after)
|
|
38
|
+
* Used together with the `.dnd-before` / `.dnd-after` CSS rules.
|
|
39
|
+
*
|
|
40
|
+
* @param {HTMLElement} el resolved drop target element
|
|
41
|
+
* @param {boolean} insertAfter result of isDropAfterHalf()
|
|
42
|
+
*/
|
|
43
|
+
export function setDropIndicator (el, insertAfter) {
|
|
44
|
+
if (!el) {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
el.classList.remove(DROP_BEFORE_CLS, DROP_AFTER_CLS)
|
|
48
|
+
el.classList.add(insertAfter ? DROP_AFTER_CLS : DROP_BEFORE_CLS)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Remove any directional drop indicator from the target element.
|
|
53
|
+
*/
|
|
54
|
+
export function clearDropIndicator (el) {
|
|
55
|
+
if (!el) {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
el.classList.remove(DROP_BEFORE_CLS, DROP_AFTER_CLS)
|
|
59
|
+
}
|
|
@@ -84,6 +84,8 @@ const bookmarkSchema = {
|
|
|
84
84
|
xany: 'boolean - enable XANY flow control, default is false',
|
|
85
85
|
txLineEnding: 'string - TX line ending on Enter: "\\r" (CR, default), "\\n" (LF), "\\r\\n" (CR+LF)',
|
|
86
86
|
rxLineEnding: 'string - RX line ending conversion: "none" (default), "lf_to_crlf" (for LF-only devices), "cr_to_crlf" (for CR-only devices)',
|
|
87
|
+
closeSequence: 'string - key sequence sent to the serial port when the user clicks "exit gracefully" in the terminal controls, supports \\n \\t \\r \\\\ and \\xHH hex bytes, default "\\x01ky" (Ctrl+A, k, y - GNU screen kill-window confirm)',
|
|
88
|
+
closeSequenceDelay: 'number - ms to wait after sending closeSequence before closing the port, default 500',
|
|
87
89
|
runScripts: 'array - run scripts after connected ({delay,script})',
|
|
88
90
|
description: 'string - bookmark description'
|
|
89
91
|
},
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
connectionHoppingWarnKey
|
|
18
18
|
} from '../../../common/constants'
|
|
19
19
|
import { useState, useRef, useCallback } from 'react'
|
|
20
|
+
import { isDropAfterHalf } from '../../../common/drop-position'
|
|
20
21
|
import ConnectionHoppingWarningText from '../../common/connection-hopping-warning-text'
|
|
21
22
|
import * as ls from '../../../common/safe-local-storage'
|
|
22
23
|
import Modal from '../../common/modal'
|
|
@@ -49,33 +50,56 @@ export default function renderConnectionHopping (props) {
|
|
|
49
50
|
const [list, setList] = useState(formData.connectionHoppings || [])
|
|
50
51
|
const dragItem = useRef(null)
|
|
51
52
|
const dragOverItem = useRef(null)
|
|
53
|
+
// tracks whether the pointer is in the bottom half of the hovered row,
|
|
54
|
+
// so a drop can land after the target (including after the last row).
|
|
55
|
+
const dragOverAfter = useRef(false)
|
|
56
|
+
// { index, after } of the row currently hovered, or null. Drives the
|
|
57
|
+
// directional drop indicator on the antd table row via a className.
|
|
58
|
+
const [overRow, setOverRow] = useState(null)
|
|
52
59
|
|
|
53
60
|
const handleDragStart = useCallback((index) => {
|
|
54
61
|
dragItem.current = index
|
|
55
62
|
}, [])
|
|
56
63
|
|
|
57
|
-
|
|
64
|
+
// track target index + before/after half on every drag-over pass,
|
|
65
|
+
// because onDragEnter only fires once per row and cannot detect
|
|
66
|
+
// moving between the top and bottom half of the same row.
|
|
67
|
+
const handleDragOver = useCallback((e, index) => {
|
|
68
|
+
e.preventDefault()
|
|
69
|
+
const after = isDropAfterHalf(e, e.currentTarget)
|
|
58
70
|
dragOverItem.current = index
|
|
71
|
+
dragOverAfter.current = after
|
|
72
|
+
setOverRow({ index, after })
|
|
59
73
|
}, [])
|
|
60
74
|
|
|
61
75
|
const handleDragEnd = useCallback(() => {
|
|
76
|
+
setOverRow(null)
|
|
62
77
|
if (dragItem.current === null || dragOverItem.current === null) {
|
|
63
78
|
return
|
|
64
79
|
}
|
|
65
80
|
if (dragItem.current === dragOverItem.current) {
|
|
66
81
|
dragItem.current = null
|
|
67
82
|
dragOverItem.current = null
|
|
83
|
+
dragOverAfter.current = false
|
|
68
84
|
return
|
|
69
85
|
}
|
|
70
86
|
setList(old => {
|
|
71
87
|
const newList = [...old]
|
|
72
|
-
const
|
|
73
|
-
|
|
88
|
+
const fromIndex = dragItem.current
|
|
89
|
+
let insertIndex = dragOverAfter.current
|
|
90
|
+
? dragOverItem.current + 1
|
|
91
|
+
: dragOverItem.current
|
|
92
|
+
const [removed] = newList.splice(fromIndex, 1)
|
|
93
|
+
if (fromIndex < insertIndex) {
|
|
94
|
+
insertIndex = insertIndex - 1
|
|
95
|
+
}
|
|
96
|
+
newList.splice(insertIndex, 0, removed)
|
|
74
97
|
form.setFieldsValue({
|
|
75
98
|
connectionHoppings: newList
|
|
76
99
|
})
|
|
77
100
|
dragItem.current = null
|
|
78
101
|
dragOverItem.current = null
|
|
102
|
+
dragOverAfter.current = false
|
|
79
103
|
return newList
|
|
80
104
|
})
|
|
81
105
|
}, [form])
|
|
@@ -205,15 +229,17 @@ export default function renderConnectionHopping (props) {
|
|
|
205
229
|
<FormItem {...tailFormItemLayout}>
|
|
206
230
|
<Table
|
|
207
231
|
columns={cols}
|
|
208
|
-
className='mg3b'
|
|
232
|
+
className='mg3b connection-hopping-table'
|
|
209
233
|
pagination={false}
|
|
210
234
|
size='small'
|
|
211
235
|
onRow={(record, index) => ({
|
|
212
236
|
draggable: true,
|
|
237
|
+
className: overRow && overRow.index === index
|
|
238
|
+
? (overRow.after ? 'dnd-after' : 'dnd-before')
|
|
239
|
+
: '',
|
|
213
240
|
onDragStart: () => handleDragStart(index),
|
|
214
|
-
|
|
215
|
-
onDragEnd: handleDragEnd
|
|
216
|
-
onDragOver: (e) => e.preventDefault()
|
|
241
|
+
onDragOver: (e) => handleDragOver(e, index),
|
|
242
|
+
onDragEnd: handleDragEnd
|
|
217
243
|
})}
|
|
218
244
|
dataSource={list.map((d, i) => {
|
|
219
245
|
return {
|
|
@@ -32,10 +32,18 @@ export default function SshTunnelForm (props) {
|
|
|
32
32
|
} = props
|
|
33
33
|
|
|
34
34
|
const [isDynamic, setIsDynamic] = useState(
|
|
35
|
-
(initialValues?.sshTunnel || '
|
|
35
|
+
(initialValues?.sshTunnel || 'forwardLocalToRemote') === 'dynamicForward'
|
|
36
36
|
)
|
|
37
37
|
const [dirty, setDirty] = useState(false)
|
|
38
38
|
|
|
39
|
+
// watch host/port fields so the tooltip examples reflect what the user typed
|
|
40
|
+
const localHost = Form.useWatch('sshTunnelLocalHost', formChild) || '127.0.0.1'
|
|
41
|
+
const localPort = Form.useWatch('sshTunnelLocalPort', formChild) || '12200'
|
|
42
|
+
const remoteHost = Form.useWatch('sshTunnelRemoteHost', formChild) || '127.0.0.1'
|
|
43
|
+
const remotePort = Form.useWatch('sshTunnelRemotePort', formChild) || '12300'
|
|
44
|
+
const localAddr = `${localHost}:${localPort}`
|
|
45
|
+
const remoteAddr = `${remoteHost}:${remotePort}`
|
|
46
|
+
|
|
39
47
|
function onChange (ev) {
|
|
40
48
|
setIsDynamic(ev.target.value === 'dynamicForward')
|
|
41
49
|
}
|
|
@@ -63,13 +71,23 @@ export default function SshTunnelForm (props) {
|
|
|
63
71
|
<div>
|
|
64
72
|
<p>{e(direction)}</p>
|
|
65
73
|
<p><UserOutlined /> → {middle} → {last}</p>
|
|
74
|
+
{localToRemote
|
|
75
|
+
? (
|
|
76
|
+
<p>Connect to <b>{localAddr}</b> on your machine to reach <b>{remoteAddr}</b> on the remote server (e.g. open <b>http://{localAddr}</b> in a browser).</p>
|
|
77
|
+
)
|
|
78
|
+
: (
|
|
79
|
+
<p>Connect to <b>{remoteAddr}</b> on the remote server to reach <b>{localAddr}</b> on your machine.</p>
|
|
80
|
+
)}
|
|
66
81
|
</div>
|
|
67
82
|
)
|
|
68
83
|
}
|
|
69
84
|
|
|
70
85
|
function renderDynamicForward () {
|
|
71
86
|
return (
|
|
72
|
-
<
|
|
87
|
+
<div>
|
|
88
|
+
<p><UserOutlined /> → socks proxy → url</p>
|
|
89
|
+
<p>Point your app's SOCKS5 proxy at <b>{localAddr}</b> to reach any host through the remote server.</p>
|
|
90
|
+
</div>
|
|
73
91
|
)
|
|
74
92
|
}
|
|
75
93
|
|
|
@@ -125,16 +143,16 @@ export default function SshTunnelForm (props) {
|
|
|
125
143
|
required
|
|
126
144
|
>
|
|
127
145
|
<RadioGroup onChange={onChange}>
|
|
128
|
-
<RadioButton value='forwardRemoteToLocal'>
|
|
129
|
-
<Tooltip title={renderSshTunnelFlow('remoteToLocal')}>
|
|
130
|
-
<span>R→L <QuestionCircleOutlined /></span>
|
|
131
|
-
</Tooltip>
|
|
132
|
-
</RadioButton>
|
|
133
146
|
<RadioButton value='forwardLocalToRemote'>
|
|
134
147
|
<Tooltip title={renderSshTunnelFlow('localToRemote')}>
|
|
135
148
|
<span>L→R <QuestionCircleOutlined /></span>
|
|
136
149
|
</Tooltip>
|
|
137
150
|
</RadioButton>
|
|
151
|
+
<RadioButton value='forwardRemoteToLocal'>
|
|
152
|
+
<Tooltip title={renderSshTunnelFlow('remoteToLocal')}>
|
|
153
|
+
<span>R→L <QuestionCircleOutlined /></span>
|
|
154
|
+
</Tooltip>
|
|
155
|
+
</RadioButton>
|
|
138
156
|
<RadioButton value='dynamicForward'>
|
|
139
157
|
<Tooltip title={renderDynamicForward()}>
|
|
140
158
|
<span>{e('dynamicForward')}(socks proxy) <QuestionCircleOutlined /></span>
|
|
@@ -14,7 +14,7 @@ const FormItem = Form.Item
|
|
|
14
14
|
const e = window.translate
|
|
15
15
|
|
|
16
16
|
const defaultInitialValues = {
|
|
17
|
-
sshTunnel: '
|
|
17
|
+
sshTunnel: 'forwardLocalToRemote',
|
|
18
18
|
sshTunnelLocalPort: 12200,
|
|
19
19
|
sshTunnelLocalHost: '127.0.0.1',
|
|
20
20
|
sshTunnelRemotePort: 12300,
|
|
@@ -20,6 +20,8 @@ const serialConfig = {
|
|
|
20
20
|
xon: false,
|
|
21
21
|
xoff: false,
|
|
22
22
|
xany: false,
|
|
23
|
+
closeSequence: '\\x01ky',
|
|
24
|
+
closeSequenceDelay: 500,
|
|
23
25
|
term: defaultSettings.terminalType,
|
|
24
26
|
displayRaw: false,
|
|
25
27
|
runScripts: [{}],
|
|
@@ -59,6 +61,18 @@ const serialConfig = {
|
|
|
59
61
|
{ type: 'switch', name: 'xany', label: 'xany', valuePropName: 'checked' },
|
|
60
62
|
{ type: 'select', name: 'txLineEnding', label: 'txLineEnding', options: commonTxLineEndings.map(d => ({ value: d.value, label: d.label })) },
|
|
61
63
|
{ type: 'select', name: 'rxLineEnding', label: 'rxLineEnding', options: commonRxLineEndings.map(d => ({ value: d.value, label: d.label })) },
|
|
64
|
+
{
|
|
65
|
+
type: 'input',
|
|
66
|
+
name: 'closeSequence',
|
|
67
|
+
label: e('closeSequence'),
|
|
68
|
+
props: { placeholder: '\\x01ky' }
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
type: 'number',
|
|
72
|
+
name: 'closeSequenceDelay',
|
|
73
|
+
label: e('closeSequenceDelay'),
|
|
74
|
+
props: { min: 0, max: 10000, step: 100 }
|
|
75
|
+
},
|
|
62
76
|
commonFields.runScripts,
|
|
63
77
|
commonFields.description,
|
|
64
78
|
{ type: 'input', name: 'type', label: 'type', hidden: true }
|
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
.main-footer
|
|
8
8
|
.session-v-info
|
|
9
9
|
display none
|
|
10
|
+
// Mobile tabs carry the extra .mobile-tabs class, and mobile.styl uses
|
|
11
|
+
// .is-mobile .tabs.mobile-tabs (specificity 0,3,0) which overrides the
|
|
12
|
+
// .fullscreen .tabs rule above (0,2,0). This ensures they stay hidden.
|
|
13
|
+
.tabs.mobile-tabs
|
|
14
|
+
display none
|
|
10
15
|
// Hide all sessions first
|
|
11
16
|
.session-wrap
|
|
12
17
|
display none
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import { isDropAfterHalf } from '../../common/drop-position'
|
|
2
|
+
|
|
1
3
|
export default function onDrop (e, cls) {
|
|
2
4
|
e.preventDefault()
|
|
3
5
|
const { store } = window
|
|
4
6
|
const idDragged = e.dataTransfer.getData('idDragged')
|
|
5
7
|
const tar = cls ? e.target.closest(cls) : e.target
|
|
6
8
|
const idDrop = tar.getAttribute('data-id')
|
|
7
|
-
|
|
9
|
+
// drop on the bottom half of the target => insert after it,
|
|
10
|
+
// which makes dropping on the last item append to the end.
|
|
11
|
+
const insertAfter = isDropAfterHalf(e, tar)
|
|
12
|
+
store.adjustOrder('quickCommands', idDragged, idDrop, insertAfter)
|
|
8
13
|
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from '@ant-design/icons'
|
|
16
16
|
import classNames from 'classnames'
|
|
17
17
|
import onDropFunc from './on-drop'
|
|
18
|
+
import { isDropAfterHalf, setDropIndicator, clearDropIndicator } from '../../common/drop-position'
|
|
18
19
|
import './qm.styl'
|
|
19
20
|
|
|
20
21
|
const e = window.translate
|
|
@@ -81,6 +82,10 @@ export default function QuickCommandsFooterBox (props) {
|
|
|
81
82
|
|
|
82
83
|
function onDragOver (e) {
|
|
83
84
|
e.preventDefault()
|
|
85
|
+
const el = e.target.closest('.qm-item')
|
|
86
|
+
if (el) {
|
|
87
|
+
setDropIndicator(el, isDropAfterHalf(e, el))
|
|
88
|
+
}
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
function onDragStart (e) {
|
|
@@ -88,14 +93,25 @@ export default function QuickCommandsFooterBox (props) {
|
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
function onDragEnter (e) {
|
|
91
|
-
e.
|
|
96
|
+
e.preventDefault()
|
|
97
|
+
const el = e.target.closest('.qm-item')
|
|
98
|
+
if (el) {
|
|
99
|
+
setDropIndicator(el, isDropAfterHalf(e, el))
|
|
100
|
+
}
|
|
92
101
|
}
|
|
93
102
|
|
|
94
103
|
function onDragLeave (e) {
|
|
95
|
-
e.target.closest('.qm-item')
|
|
104
|
+
const el = e.target.closest('.qm-item')
|
|
105
|
+
if (el) {
|
|
106
|
+
clearDropIndicator(el)
|
|
107
|
+
}
|
|
96
108
|
}
|
|
97
109
|
|
|
98
110
|
function onDrop (e) {
|
|
111
|
+
const el = e.target.closest('.qm-item')
|
|
112
|
+
if (el) {
|
|
113
|
+
clearDropIndicator(el)
|
|
114
|
+
}
|
|
99
115
|
onDropFunc(e, '.qm-item')
|
|
100
116
|
}
|
|
101
117
|
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import { MinusCircleOutlined, PlusOutlined, HolderOutlined } from '@ant-design/icons'
|
|
9
9
|
import HelpIcon from '../common/help-icon'
|
|
10
10
|
import { copy } from '../../common/clipboard'
|
|
11
|
+
import { isDropAfterHalf, setDropIndicator, clearDropIndicator } from '../../common/drop-position'
|
|
11
12
|
import { useRef } from 'react'
|
|
12
13
|
|
|
13
14
|
const FormItem = Form.Item
|
|
@@ -30,28 +31,38 @@ export default function renderQm (form) {
|
|
|
30
31
|
e.dataTransfer.dropEffect = 'move'
|
|
31
32
|
const el = e.target.closest('.ant-space-compact')
|
|
32
33
|
if (dragIndexRef.current !== index && el) {
|
|
33
|
-
el
|
|
34
|
+
setDropIndicator(el, isDropAfterHalf(e, el))
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
function handleDragLeave (e) {
|
|
38
|
-
e.target.closest('.ant-space-compact')
|
|
39
|
+
const el = e.target.closest('.ant-space-compact')
|
|
40
|
+
if (el) {
|
|
41
|
+
clearDropIndicator(el)
|
|
42
|
+
}
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
function handleDrop (e, index, form) {
|
|
42
46
|
e.preventDefault()
|
|
43
47
|
const el = e.target.closest('.ant-space-compact')
|
|
44
|
-
el
|
|
48
|
+
clearDropIndicator(el)
|
|
45
49
|
const dragIndex = dragIndexRef.current
|
|
46
50
|
if (dragIndex === null || dragIndex === index) {
|
|
47
51
|
dragIndexRef.current = null
|
|
48
52
|
return
|
|
49
53
|
}
|
|
54
|
+
// bottom half of the row => insert after it, so the last
|
|
55
|
+
// sub-command can receive a drop (append to the end).
|
|
56
|
+
const insertAfter = isDropAfterHalf(e, el)
|
|
50
57
|
const commands = form.getFieldValue('commands') || []
|
|
51
58
|
const item = commands[dragIndex]
|
|
52
59
|
const newCommands = [...commands]
|
|
53
60
|
newCommands.splice(dragIndex, 1)
|
|
54
|
-
|
|
61
|
+
let insertIndex = insertAfter ? index + 1 : index
|
|
62
|
+
if (dragIndex < insertIndex) {
|
|
63
|
+
insertIndex = insertIndex - 1
|
|
64
|
+
}
|
|
65
|
+
newCommands.splice(insertIndex, 0, item)
|
|
55
66
|
form.setFieldValue('commands', newCommands)
|
|
56
67
|
dragIndexRef.current = null
|
|
57
68
|
}
|
|
@@ -9,6 +9,7 @@ import classnames from 'classnames'
|
|
|
9
9
|
import highlight from '../common/highlight'
|
|
10
10
|
import QmTransport from './quick-command-transport'
|
|
11
11
|
import onDrop from './on-drop'
|
|
12
|
+
import { isDropAfterHalf, setDropIndicator, clearDropIndicator } from '../../common/drop-position'
|
|
12
13
|
import copy from 'json-deep-copy'
|
|
13
14
|
import uid from '../../common/uid'
|
|
14
15
|
|
|
@@ -37,6 +38,10 @@ export default class QuickCommandsList extends List {
|
|
|
37
38
|
|
|
38
39
|
handleDragOver = e => {
|
|
39
40
|
e.preventDefault()
|
|
41
|
+
const el = e.target.closest('.item-list-unit')
|
|
42
|
+
if (el) {
|
|
43
|
+
setDropIndicator(el, isDropAfterHalf(e, el))
|
|
44
|
+
}
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
handleDragStart = e => {
|
|
@@ -47,14 +52,25 @@ export default class QuickCommandsList extends List {
|
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
handleDragEnter = e => {
|
|
50
|
-
e.
|
|
55
|
+
e.preventDefault()
|
|
56
|
+
const el = e.target.closest('.item-list-unit')
|
|
57
|
+
if (el) {
|
|
58
|
+
setDropIndicator(el, isDropAfterHalf(e, el))
|
|
59
|
+
}
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
handleDragLeave = e => {
|
|
54
|
-
e.target.closest('.item-list-unit')
|
|
63
|
+
const el = e.target.closest('.item-list-unit')
|
|
64
|
+
if (el) {
|
|
65
|
+
clearDropIndicator(el)
|
|
66
|
+
}
|
|
55
67
|
}
|
|
56
68
|
|
|
57
69
|
handleDrop = e => {
|
|
70
|
+
const el = e.target.closest('.item-list-unit')
|
|
71
|
+
if (el) {
|
|
72
|
+
clearDropIndicator(el)
|
|
73
|
+
}
|
|
58
74
|
onDrop(e, '.item-list-unit')
|
|
59
75
|
}
|
|
60
76
|
|