@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
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
import {
|
|
6
6
|
Tag
|
|
7
7
|
} from 'antd'
|
|
8
|
+
import { isDropAfterHalf, setDropIndicator, clearDropIndicator } from '../../common/drop-position'
|
|
8
9
|
|
|
9
10
|
export default class AddrBookmarkItem extends Component {
|
|
10
11
|
handleClick = () => {
|
|
@@ -31,6 +32,15 @@ export default class AddrBookmarkItem extends Component {
|
|
|
31
32
|
|
|
32
33
|
handleDragOver = e => {
|
|
33
34
|
e.preventDefault()
|
|
35
|
+
// only the hovered item should show an indicator
|
|
36
|
+
document.querySelectorAll(
|
|
37
|
+
'.addr-bookmark-item.dnd-before, .addr-bookmark-item.dnd-after'
|
|
38
|
+
).forEach(el => clearDropIndicator(el))
|
|
39
|
+
setDropIndicator(e.currentTarget, isDropAfterHalf(e, e.currentTarget))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
handleDragLeave = e => {
|
|
43
|
+
clearDropIndicator(e.currentTarget)
|
|
34
44
|
}
|
|
35
45
|
|
|
36
46
|
handleDragStart = e => {
|
|
@@ -39,13 +49,15 @@ export default class AddrBookmarkItem extends Component {
|
|
|
39
49
|
|
|
40
50
|
handleDrop = e => {
|
|
41
51
|
e.preventDefault()
|
|
52
|
+
clearDropIndicator(e.currentTarget)
|
|
42
53
|
const { store } = window
|
|
43
54
|
const [host, idDragged] = e.dataTransfer.getData('idDragged').split('#')
|
|
44
|
-
const idDrop = e.
|
|
55
|
+
const idDrop = e.currentTarget.getAttribute('data-id').split('#')[1]
|
|
45
56
|
const dataName = host
|
|
46
57
|
? 'addressBookmarks'
|
|
47
58
|
: 'addressBookmarksLocal'
|
|
48
|
-
|
|
59
|
+
const insertAfter = isDropAfterHalf(e, e.currentTarget)
|
|
60
|
+
store.adjustOrder(dataName, idDragged, idDrop, insertAfter)
|
|
49
61
|
}
|
|
50
62
|
|
|
51
63
|
render () {
|
|
@@ -64,6 +76,7 @@ export default class AddrBookmarkItem extends Component {
|
|
|
64
76
|
data-id={id}
|
|
65
77
|
draggable
|
|
66
78
|
onDragOver={this.handleDragOver}
|
|
79
|
+
onDragLeave={this.handleDragLeave}
|
|
67
80
|
onDragStart={this.handleDragStart}
|
|
68
81
|
onDrop={this.handleDrop}
|
|
69
82
|
>
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* code compare utilities
|
|
3
|
+
* - binary file detection (by extension + content)
|
|
4
|
+
* - line splitting and size limits
|
|
5
|
+
* - file extension to Prism language mapping for syntax highlighting
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getFileExt } from './file-read'
|
|
9
|
+
import { maxEditFileSize } from '../../common/constants'
|
|
10
|
+
|
|
11
|
+
// extensions that are definitely binary and should never be compared as text
|
|
12
|
+
const binaryExtMap = {
|
|
13
|
+
png: 1,
|
|
14
|
+
jpg: 1,
|
|
15
|
+
jpeg: 1,
|
|
16
|
+
gif: 1,
|
|
17
|
+
bmp: 1,
|
|
18
|
+
ico: 1,
|
|
19
|
+
webp: 1,
|
|
20
|
+
tiff: 1,
|
|
21
|
+
tif: 1,
|
|
22
|
+
heic: 1,
|
|
23
|
+
heif: 1,
|
|
24
|
+
avif: 1,
|
|
25
|
+
raw: 1,
|
|
26
|
+
cr2: 1,
|
|
27
|
+
nef: 1,
|
|
28
|
+
arw: 1,
|
|
29
|
+
psd: 1,
|
|
30
|
+
ai: 1,
|
|
31
|
+
eps: 1,
|
|
32
|
+
zip: 1,
|
|
33
|
+
tar: 1,
|
|
34
|
+
gz: 1,
|
|
35
|
+
tgz: 1,
|
|
36
|
+
bz2: 1,
|
|
37
|
+
xz: 1,
|
|
38
|
+
lz: 1,
|
|
39
|
+
lzma: 1,
|
|
40
|
+
'7z': 1,
|
|
41
|
+
rar: 1,
|
|
42
|
+
jar: 1,
|
|
43
|
+
war: 1,
|
|
44
|
+
ear: 1,
|
|
45
|
+
apk: 1,
|
|
46
|
+
aab: 1,
|
|
47
|
+
xpi: 1,
|
|
48
|
+
crx: 1,
|
|
49
|
+
exe: 1,
|
|
50
|
+
dll: 1,
|
|
51
|
+
so: 1,
|
|
52
|
+
dylib: 1,
|
|
53
|
+
bin: 1,
|
|
54
|
+
class: 1,
|
|
55
|
+
o: 1,
|
|
56
|
+
obj: 1,
|
|
57
|
+
a: 1,
|
|
58
|
+
lib: 1,
|
|
59
|
+
pyc: 1,
|
|
60
|
+
pyo: 1,
|
|
61
|
+
node: 1,
|
|
62
|
+
wasm: 1,
|
|
63
|
+
swf: 1,
|
|
64
|
+
pdf: 1,
|
|
65
|
+
doc: 1,
|
|
66
|
+
docx: 1,
|
|
67
|
+
xls: 1,
|
|
68
|
+
xlsx: 1,
|
|
69
|
+
ppt: 1,
|
|
70
|
+
pptx: 1,
|
|
71
|
+
odt: 1,
|
|
72
|
+
ods: 1,
|
|
73
|
+
odp: 1,
|
|
74
|
+
mp3: 1,
|
|
75
|
+
mp4: 1,
|
|
76
|
+
avi: 1,
|
|
77
|
+
mov: 1,
|
|
78
|
+
wmv: 1,
|
|
79
|
+
flv: 1,
|
|
80
|
+
ogg: 1,
|
|
81
|
+
wav: 1,
|
|
82
|
+
aac: 1,
|
|
83
|
+
m4a: 1,
|
|
84
|
+
m4v: 1,
|
|
85
|
+
webm: 1,
|
|
86
|
+
mkv: 1,
|
|
87
|
+
mpg: 1,
|
|
88
|
+
mpeg: 1,
|
|
89
|
+
'3gp': 1,
|
|
90
|
+
opus: 1,
|
|
91
|
+
db: 1,
|
|
92
|
+
sqlite: 1,
|
|
93
|
+
sqlite3: 1,
|
|
94
|
+
mdb: 1,
|
|
95
|
+
accdb: 1,
|
|
96
|
+
iso: 1,
|
|
97
|
+
img: 1,
|
|
98
|
+
dmg: 1,
|
|
99
|
+
vmdk: 1,
|
|
100
|
+
qcow2: 1,
|
|
101
|
+
vdi: 1,
|
|
102
|
+
wim: 1,
|
|
103
|
+
ttf: 1,
|
|
104
|
+
otf: 1,
|
|
105
|
+
woff: 1,
|
|
106
|
+
woff2: 1,
|
|
107
|
+
eot: 1,
|
|
108
|
+
pak: 1,
|
|
109
|
+
dat: 1,
|
|
110
|
+
bson: 1,
|
|
111
|
+
msg: 1,
|
|
112
|
+
eml: 1,
|
|
113
|
+
pdb: 1,
|
|
114
|
+
ax: 1,
|
|
115
|
+
nib: 1,
|
|
116
|
+
mo: 1
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// map file extensions to Prism / refractor language names
|
|
120
|
+
// only includes languages that react-diff-viewer-continued can highlight
|
|
121
|
+
const extToLangMap = {
|
|
122
|
+
js: 'javascript',
|
|
123
|
+
jsx: 'jsx',
|
|
124
|
+
mjs: 'javascript',
|
|
125
|
+
cjs: 'javascript',
|
|
126
|
+
ts: 'typescript',
|
|
127
|
+
tsx: 'tsx',
|
|
128
|
+
json: 'json',
|
|
129
|
+
html: 'html',
|
|
130
|
+
htm: 'html',
|
|
131
|
+
xml: 'xml',
|
|
132
|
+
svg: 'xml',
|
|
133
|
+
css: 'css',
|
|
134
|
+
scss: 'scss',
|
|
135
|
+
sass: 'sass',
|
|
136
|
+
less: 'less',
|
|
137
|
+
styl: 'stylus',
|
|
138
|
+
py: 'python',
|
|
139
|
+
rb: 'ruby',
|
|
140
|
+
go: 'go',
|
|
141
|
+
rs: 'rust',
|
|
142
|
+
java: 'java',
|
|
143
|
+
kt: 'kotlin',
|
|
144
|
+
c: 'c',
|
|
145
|
+
h: 'c',
|
|
146
|
+
cpp: 'cpp',
|
|
147
|
+
cc: 'cpp',
|
|
148
|
+
cxx: 'cpp',
|
|
149
|
+
hpp: 'cpp',
|
|
150
|
+
cs: 'csharp',
|
|
151
|
+
php: 'php',
|
|
152
|
+
swift: 'swift',
|
|
153
|
+
sh: 'bash',
|
|
154
|
+
bash: 'bash',
|
|
155
|
+
zsh: 'bash',
|
|
156
|
+
fish: 'bash',
|
|
157
|
+
yml: 'yaml',
|
|
158
|
+
yaml: 'yaml',
|
|
159
|
+
toml: 'toml',
|
|
160
|
+
md: 'markdown',
|
|
161
|
+
markdown: 'markdown',
|
|
162
|
+
sql: 'sql',
|
|
163
|
+
graphql: 'graphql',
|
|
164
|
+
gql: 'graphql',
|
|
165
|
+
dockerfile: 'docker',
|
|
166
|
+
vue: 'markup',
|
|
167
|
+
svelte: 'markup',
|
|
168
|
+
lua: 'lua',
|
|
169
|
+
r: 'r',
|
|
170
|
+
rkt: 'racket',
|
|
171
|
+
clj: 'clojure',
|
|
172
|
+
cljs: 'clojure',
|
|
173
|
+
edn: 'clojure',
|
|
174
|
+
scala: 'scala',
|
|
175
|
+
pl: 'perl',
|
|
176
|
+
pm: 'perl',
|
|
177
|
+
dart: 'dart',
|
|
178
|
+
ex: 'elixir',
|
|
179
|
+
exs: 'elixir',
|
|
180
|
+
erl: 'erlang',
|
|
181
|
+
hs: 'haskell',
|
|
182
|
+
nim: 'nim',
|
|
183
|
+
zig: 'zig',
|
|
184
|
+
v: 'verilog',
|
|
185
|
+
vim: 'vim',
|
|
186
|
+
ini: 'properties',
|
|
187
|
+
conf: 'properties',
|
|
188
|
+
cfg: 'properties',
|
|
189
|
+
properties: 'properties'
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// upper bound of lines to diff, keeps memory usage reasonable
|
|
193
|
+
// react-diff-viewer-continued uses jsdiff's Myers algorithm, which is far more
|
|
194
|
+
// memory-efficient than the previous hand-written LCS DP table, so we can
|
|
195
|
+
// safely support larger files.
|
|
196
|
+
export const maxDiffLines = 10000
|
|
197
|
+
|
|
198
|
+
export function getExt (name = '') {
|
|
199
|
+
return (getFileExt(name).ext || '').toLowerCase()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function isBinaryExt (name = '') {
|
|
203
|
+
return binaryExtMap[getExt(name)] === 1
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Whether the two files can be compared as text/code at all.
|
|
208
|
+
* Only non-directory, non-binary-ext, not-too-large files qualify.
|
|
209
|
+
*/
|
|
210
|
+
export function canCodeCompare (file1 = {}, file2 = {}) {
|
|
211
|
+
if (!file1 || !file2) {
|
|
212
|
+
return false
|
|
213
|
+
}
|
|
214
|
+
if (file1.isDirectory || file2.isDirectory) {
|
|
215
|
+
return false
|
|
216
|
+
}
|
|
217
|
+
if (isBinaryExt(file1.name) || isBinaryExt(file2.name)) {
|
|
218
|
+
return false
|
|
219
|
+
}
|
|
220
|
+
if ((file1.size || 0) > maxEditFileSize || (file2.size || 0) > maxEditFileSize) {
|
|
221
|
+
return false
|
|
222
|
+
}
|
|
223
|
+
return true
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Check whether already-decoded text content looks binary (contains null bytes).
|
|
228
|
+
* utf8-decoded binary data usually contains \u0000.
|
|
229
|
+
*/
|
|
230
|
+
export function looksBinary (text = '') {
|
|
231
|
+
return text.includes('\u0000')
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function splitLines (text = '') {
|
|
235
|
+
if (text === '') {
|
|
236
|
+
return []
|
|
237
|
+
}
|
|
238
|
+
return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n')
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Map a filename to a Prism/refractor language name
|
|
243
|
+
* for syntax highlighting in the diff viewer.
|
|
244
|
+
* Returns undefined if no mapping is found.
|
|
245
|
+
*/
|
|
246
|
+
export function getHighlightLanguage (name = '') {
|
|
247
|
+
const ext = getExt(name)
|
|
248
|
+
if (!ext) {
|
|
249
|
+
// check for special filenames
|
|
250
|
+
const base = (name || '').toLowerCase()
|
|
251
|
+
if (base === 'dockerfile') {
|
|
252
|
+
return 'docker'
|
|
253
|
+
}
|
|
254
|
+
if (base === 'makefile' || base === 'gnumakefile') {
|
|
255
|
+
return 'makefile'
|
|
256
|
+
}
|
|
257
|
+
return undefined
|
|
258
|
+
}
|
|
259
|
+
return extToLangMap[ext]
|
|
260
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* code compare component
|
|
3
|
+
* reads both file contents and renders a side-by-side diff
|
|
4
|
+
* using react-diff-viewer-continued (word-level diff + syntax highlight)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useState, useEffect, useMemo } from 'react'
|
|
8
|
+
import { Spin } from 'antd'
|
|
9
|
+
import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer-continued'
|
|
10
|
+
import resolve from '../../common/resolve'
|
|
11
|
+
import { refs } from '../common/ref'
|
|
12
|
+
import { typeMap } from '../../common/constants'
|
|
13
|
+
import isColorDark from '../../common/is-color-dark'
|
|
14
|
+
import {
|
|
15
|
+
looksBinary,
|
|
16
|
+
isBinaryExt,
|
|
17
|
+
splitLines,
|
|
18
|
+
maxDiffLines,
|
|
19
|
+
getHighlightLanguage
|
|
20
|
+
} from './code-compare-utils'
|
|
21
|
+
import './code-compare.styl'
|
|
22
|
+
|
|
23
|
+
const e = window.translate
|
|
24
|
+
|
|
25
|
+
export default function CodeCompare (props) {
|
|
26
|
+
const { file1, file2, tab = {} } = props
|
|
27
|
+
const [loading, setLoading] = useState(true)
|
|
28
|
+
const [error, setError] = useState('')
|
|
29
|
+
const [text1, setText1] = useState('')
|
|
30
|
+
const [text2, setText2] = useState('')
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
let cancelled = false
|
|
34
|
+
async function load () {
|
|
35
|
+
setLoading(true)
|
|
36
|
+
setError('')
|
|
37
|
+
setText1('')
|
|
38
|
+
setText2('')
|
|
39
|
+
// check binary by extension before reading
|
|
40
|
+
if (isBinaryExt(file1 && file1.name) || isBinaryExt(file2 && file2.name)) {
|
|
41
|
+
setError('binary file - not supported for code compare')
|
|
42
|
+
setLoading(false)
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const [t1, t2] = await Promise.all([
|
|
47
|
+
readFileContent(file1, tab),
|
|
48
|
+
readFileContent(file2, tab)
|
|
49
|
+
])
|
|
50
|
+
if (cancelled) {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
if (looksBinary(t1) || looksBinary(t2)) {
|
|
54
|
+
setError(e('binary file') + ' - ' + e('not supported for code compare'))
|
|
55
|
+
setLoading(false)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
const a = splitLines(t1)
|
|
59
|
+
const b = splitLines(t2)
|
|
60
|
+
if (a.length > maxDiffLines || b.length > maxDiffLines) {
|
|
61
|
+
setError(e('file too large for code compare') + ' (max ' + maxDiffLines + ' ' + e('lines') + ')')
|
|
62
|
+
setLoading(false)
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
setText1(t1)
|
|
66
|
+
setText2(t2)
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (!cancelled) {
|
|
69
|
+
setError(err && err.message ? err.message : String(err))
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!cancelled) {
|
|
73
|
+
setLoading(false)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
load()
|
|
77
|
+
return () => {
|
|
78
|
+
cancelled = true
|
|
79
|
+
}
|
|
80
|
+
}, [file1, file2, tab])
|
|
81
|
+
|
|
82
|
+
const { useDarkTheme, styles } = useMemo(() => {
|
|
83
|
+
const themeConf = window.store?.getUiThemeConfig?.() || {}
|
|
84
|
+
const dark = isColorDark(themeConf.main || '#121214')
|
|
85
|
+
const vars = {
|
|
86
|
+
diffViewerBackground: themeConf['main-light'] || (dark ? '#1e1e1e' : '#fff'),
|
|
87
|
+
diffViewerColor: themeConf.text || (dark ? '#ddd' : '#333'),
|
|
88
|
+
gutterBackground: themeConf['main-dark'] || (dark ? '#2a2a2a' : '#f5f5f5'),
|
|
89
|
+
gutterColor: themeConf['text-dark'] || (dark ? '#888' : '#999'),
|
|
90
|
+
addedBackground: themeConf.success || '#06D6A0',
|
|
91
|
+
addedColor: dark ? '#06D6A0' : '#058f6b',
|
|
92
|
+
removedBackground: themeConf.error || '#EF476F',
|
|
93
|
+
removedColor: dark ? '#EF476F' : '#c93659',
|
|
94
|
+
wordAddedBackground: 'rgba(6, 214, 160, 0.25)',
|
|
95
|
+
wordRemovedBackground: 'rgba(239, 71, 111, 0.25)',
|
|
96
|
+
addedGutterBackground: 'rgba(6, 214, 160, 0.15)',
|
|
97
|
+
removedGutterBackground: 'rgba(239, 71, 111, 0.15)',
|
|
98
|
+
codeFoldBackground: themeConf['main-dark'] || (dark ? '#252525' : '#f0f0f0'),
|
|
99
|
+
codeFoldContentColor: themeConf['text-dark'] || (dark ? '#666' : '#bbb'),
|
|
100
|
+
emptyLineBackground: themeConf['main-dark'] || (dark ? '#1a1a1a' : '#fafafa'),
|
|
101
|
+
diffViewerTitleBackground: themeConf['main-dark'] || (dark ? '#2a2a2a' : '#f0f0f0'),
|
|
102
|
+
diffViewerTitleColor: themeConf.text || (dark ? '#ddd' : '#333'),
|
|
103
|
+
diffViewerTitleBorderColor: themeConf.primary || '#08c'
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
useDarkTheme: dark,
|
|
107
|
+
styles: {
|
|
108
|
+
variables: {
|
|
109
|
+
light: vars,
|
|
110
|
+
dark: vars
|
|
111
|
+
},
|
|
112
|
+
diffContainer: {
|
|
113
|
+
fontFamily: "'Maple Mono', 'Menlo', 'Consolas', monospace",
|
|
114
|
+
fontSize: '13px',
|
|
115
|
+
lineHeight: '1.5'
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}, [])
|
|
120
|
+
|
|
121
|
+
const highlightLanguage = useMemo(() => {
|
|
122
|
+
const lang1 = getHighlightLanguage(file1?.name)
|
|
123
|
+
const lang2 = getHighlightLanguage(file2?.name)
|
|
124
|
+
return lang1 === lang2 ? lang1 : undefined
|
|
125
|
+
}, [file1, file2])
|
|
126
|
+
|
|
127
|
+
const name1 = file1 ? file1.name : ''
|
|
128
|
+
const name2 = file2 ? file2.name : ''
|
|
129
|
+
const identical = text1 === text2 && !loading && !error
|
|
130
|
+
|
|
131
|
+
let body = null
|
|
132
|
+
if (error) {
|
|
133
|
+
body = <div className='code-compare-message'>{error}</div>
|
|
134
|
+
} else if (identical) {
|
|
135
|
+
body = <div className='code-compare-message'>{e('files are identical')}</div>
|
|
136
|
+
} else {
|
|
137
|
+
body = (
|
|
138
|
+
<div className='code-compare-body'>
|
|
139
|
+
<ReactDiffViewer
|
|
140
|
+
oldValue={text1}
|
|
141
|
+
newValue={text2}
|
|
142
|
+
splitView
|
|
143
|
+
compareMethod={DiffMethod.WORDS}
|
|
144
|
+
useDarkTheme={useDarkTheme}
|
|
145
|
+
styles={styles}
|
|
146
|
+
leftTitle={name1}
|
|
147
|
+
rightTitle={name2}
|
|
148
|
+
hideLineNumbers={false}
|
|
149
|
+
showDiffOnly
|
|
150
|
+
extraLinesSurroundingDiff={3}
|
|
151
|
+
hideSummary
|
|
152
|
+
codeFoldMessageRenderer={() => <span />}
|
|
153
|
+
disableWorker
|
|
154
|
+
highlightLanguage={highlightLanguage}
|
|
155
|
+
/>
|
|
156
|
+
</div>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<div className='code-compare-wrap'>
|
|
162
|
+
<Spin spinning={loading}>
|
|
163
|
+
{body}
|
|
164
|
+
</Spin>
|
|
165
|
+
</div>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function readFileContent (file, tab) {
|
|
170
|
+
if (!file) {
|
|
171
|
+
return ''
|
|
172
|
+
}
|
|
173
|
+
const p = resolve(file.path, file.name)
|
|
174
|
+
if (file.type === typeMap.remote) {
|
|
175
|
+
const sftpEntry = refs.get('sftp-' + tab.id)
|
|
176
|
+
const sftp = sftpEntry && sftpEntry.sftp
|
|
177
|
+
if (!sftp) {
|
|
178
|
+
throw new Error(e('sftp not ready'))
|
|
179
|
+
}
|
|
180
|
+
return sftp.readFile(p)
|
|
181
|
+
}
|
|
182
|
+
return window.fs.readFile(p)
|
|
183
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* file compare modal - shows side by side comparison of two files' attributes
|
|
3
|
+
* and an optional code/content diff tab for text files
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
|
-
import React from 'react'
|
|
6
|
+
import React, { lazy, Suspense } from 'react'
|
|
7
|
+
import { Spin, Tabs } from 'antd'
|
|
6
8
|
import Modal from '../common/modal'
|
|
7
9
|
import resolve from '../../common/resolve'
|
|
8
10
|
import time from '../../common/time'
|
|
@@ -15,6 +17,15 @@ import './file-compare-modal.styl'
|
|
|
15
17
|
const e = window.translate
|
|
16
18
|
const formatTime = time
|
|
17
19
|
|
|
20
|
+
// lazy load the code compare component so it is only bundled when needed
|
|
21
|
+
const CodeCompare = lazy(() => import('./code-compare'))
|
|
22
|
+
|
|
23
|
+
const Loading = () => (
|
|
24
|
+
<div style={{ padding: 40, textAlign: 'center' }}>
|
|
25
|
+
<Spin />
|
|
26
|
+
</div>
|
|
27
|
+
)
|
|
28
|
+
|
|
18
29
|
export default class FileCompareModal extends React.PureComponent {
|
|
19
30
|
state = {
|
|
20
31
|
visible: false,
|
|
@@ -22,7 +33,8 @@ export default class FileCompareModal extends React.PureComponent {
|
|
|
22
33
|
file2: {},
|
|
23
34
|
tab: null,
|
|
24
35
|
uidTree: {},
|
|
25
|
-
gidTree: {}
|
|
36
|
+
gidTree: {},
|
|
37
|
+
activeTab: 'info'
|
|
26
38
|
}
|
|
27
39
|
|
|
28
40
|
componentDidMount () {
|
|
@@ -32,7 +44,8 @@ export default class FileCompareModal extends React.PureComponent {
|
|
|
32
44
|
showFileCompareModal (data) {
|
|
33
45
|
this.setState({
|
|
34
46
|
...data,
|
|
35
|
-
visible: true
|
|
47
|
+
visible: true,
|
|
48
|
+
activeTab: 'info'
|
|
36
49
|
})
|
|
37
50
|
}
|
|
38
51
|
|
|
@@ -40,36 +53,17 @@ export default class FileCompareModal extends React.PureComponent {
|
|
|
40
53
|
this.setState({
|
|
41
54
|
file1: {},
|
|
42
55
|
file2: {},
|
|
43
|
-
visible: false
|
|
56
|
+
visible: false,
|
|
57
|
+
activeTab: 'info'
|
|
44
58
|
})
|
|
45
59
|
}
|
|
46
60
|
|
|
47
|
-
|
|
48
|
-
const {
|
|
49
|
-
visible,
|
|
50
|
-
file1,
|
|
51
|
-
file2,
|
|
52
|
-
tab = {},
|
|
53
|
-
uidTree = {},
|
|
54
|
-
gidTree = {}
|
|
55
|
-
} = this.state
|
|
56
|
-
if (!visible) {
|
|
57
|
-
return null
|
|
58
|
-
}
|
|
61
|
+
renderInfo (file1, file2, tab, uidTree, gidTree) {
|
|
59
62
|
const {
|
|
60
63
|
host,
|
|
61
64
|
port,
|
|
62
65
|
username
|
|
63
66
|
} = tab
|
|
64
|
-
const title = e('compare')
|
|
65
|
-
const ps = {
|
|
66
|
-
open: visible,
|
|
67
|
-
width: 800,
|
|
68
|
-
title,
|
|
69
|
-
footer: null,
|
|
70
|
-
onCancel: this.onClose
|
|
71
|
-
}
|
|
72
|
-
|
|
73
67
|
const fp1 = resolve(file1.path, file1.name)
|
|
74
68
|
const fp2 = resolve(file2.path, file2.name)
|
|
75
69
|
const ffp1 = file1.type === 'local'
|
|
@@ -111,8 +105,8 @@ export default class FileCompareModal extends React.PureComponent {
|
|
|
111
105
|
],
|
|
112
106
|
[
|
|
113
107
|
e('size'),
|
|
114
|
-
file1.isDirectory ? '-' : filesize(file1.size || 0)
|
|
115
|
-
file2.isDirectory ? '-' : filesize(file2.size || 0)
|
|
108
|
+
file1.isDirectory ? '-' : `${filesize(file1.size || 0)}(${file1.size || 0}byte)`,
|
|
109
|
+
file2.isDirectory ? '-' : `${filesize(file2.size || 0)}(${file2.size || 0}byte)`
|
|
116
110
|
],
|
|
117
111
|
[e('mode'), permission1, permission2],
|
|
118
112
|
[e('fullPath'), ffp1, ffp2],
|
|
@@ -122,23 +116,74 @@ export default class FileCompareModal extends React.PureComponent {
|
|
|
122
116
|
[e('modifyTime'), formatTime(file1.modifyTime), formatTime(file2.modifyTime)]
|
|
123
117
|
]
|
|
124
118
|
|
|
119
|
+
return (
|
|
120
|
+
<table className='file-compare-table'>
|
|
121
|
+
<tbody>
|
|
122
|
+
{rows.map(([label, v1, v2], i) => {
|
|
123
|
+
const same = String(v1 ?? '') === String(v2 ?? '')
|
|
124
|
+
const cls = same ? '' : 'file-compare-diff'
|
|
125
|
+
return (
|
|
126
|
+
<tr key={i}>
|
|
127
|
+
<td className='bold file-compare-label'>{label}</td>
|
|
128
|
+
<td className={cls}>{v1}</td>
|
|
129
|
+
<td className={cls}>{v2}</td>
|
|
130
|
+
</tr>
|
|
131
|
+
)
|
|
132
|
+
})}
|
|
133
|
+
</tbody>
|
|
134
|
+
</table>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
render () {
|
|
139
|
+
const {
|
|
140
|
+
visible,
|
|
141
|
+
file1,
|
|
142
|
+
file2,
|
|
143
|
+
tab = {},
|
|
144
|
+
uidTree = {},
|
|
145
|
+
gidTree = {},
|
|
146
|
+
activeTab
|
|
147
|
+
} = this.state
|
|
148
|
+
if (!visible) {
|
|
149
|
+
return null
|
|
150
|
+
}
|
|
151
|
+
const title = e('compare')
|
|
152
|
+
const isCodeTab = activeTab === 'code'
|
|
153
|
+
const width = isCodeTab ? '90%' : 800
|
|
154
|
+
const ps = {
|
|
155
|
+
open: visible,
|
|
156
|
+
width,
|
|
157
|
+
title,
|
|
158
|
+
footer: null,
|
|
159
|
+
onCancel: this.onClose
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const tabItems = [
|
|
163
|
+
{
|
|
164
|
+
key: 'info',
|
|
165
|
+
label: e('info'),
|
|
166
|
+
children: this.renderInfo(file1, file2, tab, uidTree, gidTree)
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
key: 'code',
|
|
170
|
+
label: e('code'),
|
|
171
|
+
children: (
|
|
172
|
+
<Suspense fallback={<Loading />}>
|
|
173
|
+
<CodeCompare file1={file1} file2={file2} tab={tab} />
|
|
174
|
+
</Suspense>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
]
|
|
178
|
+
|
|
125
179
|
return (
|
|
126
180
|
<Modal {...ps}>
|
|
127
|
-
<
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
<tr key={i}>
|
|
134
|
-
<td className='bold file-compare-label'>{label}</td>
|
|
135
|
-
<td className={cls}>{v1}</td>
|
|
136
|
-
<td className={cls}>{v2}</td>
|
|
137
|
-
</tr>
|
|
138
|
-
)
|
|
139
|
-
})}
|
|
140
|
-
</tbody>
|
|
141
|
-
</table>
|
|
181
|
+
<Tabs
|
|
182
|
+
activeKey={activeTab}
|
|
183
|
+
onChange={key => this.setState({ activeTab: key })}
|
|
184
|
+
items={tabItems}
|
|
185
|
+
destroyInactiveTabPane={false}
|
|
186
|
+
/>
|
|
142
187
|
</Modal>
|
|
143
188
|
)
|
|
144
189
|
}
|