@electerm/electerm-react 3.2.0 → 3.3.8

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.
Files changed (36) hide show
  1. package/client/common/constants.js +1 -3
  2. package/client/components/batch-op/batch-op-alert.jsx +42 -0
  3. package/client/components/batch-op/batch-op-editor.jsx +202 -0
  4. package/client/components/batch-op/batch-op-logs.jsx +53 -0
  5. package/client/components/batch-op/batch-op-runner.jsx +315 -0
  6. package/client/components/bookmark-form/ai-bookmark-form.jsx +2 -1
  7. package/client/components/bookmark-form/bookmark-from-history-modal.jsx +2 -1
  8. package/client/components/common/auto-check-update.jsx +31 -0
  9. package/client/components/common/notification.styl +1 -1
  10. package/client/components/file-transfer/conflict-resolve.jsx +3 -0
  11. package/client/components/footer/batch-input.jsx +10 -7
  12. package/client/components/main/error-wrapper.jsx +18 -7
  13. package/client/components/main/main.jsx +6 -7
  14. package/client/components/setting-sync/auto-sync.jsx +53 -0
  15. package/client/components/setting-sync/data-import.jsx +69 -8
  16. package/client/components/sftp/address-bar.jsx +7 -1
  17. package/client/components/sidebar/bookmark-select.jsx +3 -2
  18. package/client/components/sidebar/history-item.jsx +3 -1
  19. package/client/components/sidebar/index.jsx +0 -9
  20. package/client/components/tabs/add-btn-menu.jsx +1 -1
  21. package/client/components/tabs/add-btn.jsx +9 -15
  22. package/client/components/tabs/quick-connect.jsx +6 -10
  23. package/client/components/terminal/terminal.jsx +4 -5
  24. package/client/components/tree-list/tree-list.jsx +115 -10
  25. package/client/components/tree-list/tree-list.styl +3 -0
  26. package/client/components/tree-list/tree-search.jsx +9 -1
  27. package/client/components/widgets/widget-form.jsx +6 -0
  28. package/client/store/common.js +0 -28
  29. package/client/store/load-data.js +3 -3
  30. package/client/store/mcp-handler.js +2 -2
  31. package/client/store/sync.js +25 -1
  32. package/client/store/tab.js +1 -1
  33. package/client/store/watch.js +10 -18
  34. package/client/views/index.pug +1 -2
  35. package/package.json +1 -1
  36. package/client/components/batch-op/batch-op.jsx +0 -694
@@ -21,6 +21,9 @@ function formatTimeAuto (strOrDigit) {
21
21
  if (isString(strOrDigit)) {
22
22
  return formatTime(strOrDigit)
23
23
  }
24
+ if (strOrDigit > 9999999999) {
25
+ return formatTime(strOrDigit)
26
+ }
24
27
  return formatTime(strOrDigit * 1000)
25
28
  }
26
29
 
@@ -136,12 +136,10 @@ export default class BatchInput extends Component {
136
136
  } = this.props
137
137
  const opts = {
138
138
  options: this.buildOptions(),
139
- placeholder: e('batchInput'),
140
139
  value: cmd,
141
140
  onChange: this.handleChange,
142
141
  defaultOpen: false,
143
142
  open,
144
- allowClear: true,
145
143
  className: 'batch-input-wrap'
146
144
  }
147
145
  const cls = classNames(
@@ -155,6 +153,15 @@ export default class BatchInput extends Component {
155
153
  placeholder: e('batchInput'),
156
154
  className: 'batch-input-holder'
157
155
  }
156
+ const textAreaProps = {
157
+ onPressEnter: this.handleEnter,
158
+ onClick: this.handleClick,
159
+ onBlur: this.handleBlur,
160
+ size: 'small',
161
+ autoSize: { minRows: 1 },
162
+ placeholder: e('batchInput'),
163
+ allowClear: true
164
+ }
158
165
  const tabSelectProps = {
159
166
  activeTabId: this.props.activeTabId,
160
167
  tabs: this.getTabs(),
@@ -179,11 +186,7 @@ export default class BatchInput extends Component {
179
186
  {...opts}
180
187
  >
181
188
  <Input.TextArea
182
- onPressEnter={this.handleEnter}
183
- onClick={this.handleClick}
184
- onBlur={this.handleBlur}
185
- size='small'
186
- autoSize={{ minRows: 1 }}
189
+ {...textAreaProps}
187
190
  />
188
191
  </AutoComplete>
189
192
  <TabSelect {...tabSelectProps} />
@@ -77,12 +77,9 @@ export default class ErrorBoundary extends React.PureComponent {
77
77
  }
78
78
 
79
79
  renderTroubleShoot = () => {
80
- const {
81
- bugs: {
82
- url: bugReportLink
83
- }
84
- } = packInfo
85
- const bugUrl = `${bugReportLink}/new/choose`
80
+ if (window.et.isWebApp) {
81
+ return this.renderContacts()
82
+ }
86
83
  return (
87
84
  <div className='pd1y wordbreak'>
88
85
  <h2>{e('troubleShoot')}</h2>
@@ -99,6 +96,20 @@ export default class ErrorBoundary extends React.PureComponent {
99
96
  )
100
97
  })
101
98
  }
99
+ {this.renderContacts()}
100
+ </div>
101
+ )
102
+ }
103
+
104
+ renderContacts () {
105
+ const {
106
+ bugs: {
107
+ url: bugReportLink
108
+ }
109
+ } = packInfo
110
+ const bugUrl = `${bugReportLink}/new/choose`
111
+ return (
112
+ <>
102
113
  <div className='pd1b'>
103
114
  <Link to={bugUrl}>{e('bugReport')}</Link>
104
115
  </div>
@@ -112,7 +123,7 @@ export default class ErrorBoundary extends React.PureComponent {
112
123
  className='mwm-100'
113
124
  />
114
125
  </div>
115
- </div>
126
+ </>
116
127
  )
117
128
  }
118
129
 
@@ -6,7 +6,6 @@ import UpdateCheck from './upgrade'
6
6
  import SettingModal from '../setting-panel/setting-modal'
7
7
  import TextEditor from '../text-editor/text-editor'
8
8
  import Sidebar from '../sidebar'
9
- import BatchOp from '../batch-op/batch-op'
10
9
  import CssOverwrite from '../bg/css-overwrite'
11
10
  import UiTheme from './ui-theme'
12
11
  import CustomCss from '../bg/custom-css.jsx'
@@ -34,6 +33,9 @@ import MoveItemModal from '../tree-list/move-item-modal'
34
33
  import InputContextMenu from '../common/input-context-menu'
35
34
  import WorkspaceSaveModal from '../tabs/workspace-save-modal'
36
35
  import BookmarkFromHistoryModal from '../bookmark-form/bookmark-from-history-modal'
36
+ import AutoSync from '../setting-sync/auto-sync'
37
+ import AutoCheckUpdate from '../common/auto-check-update'
38
+ import BatchOpRunner from '../batch-op/batch-op-runner'
37
39
  import { pick } from 'lodash-es'
38
40
  import deepCopy from 'json-deep-copy'
39
41
  import './wrapper.styl'
@@ -184,11 +186,6 @@ export default auto(function Index (props) {
184
186
  fileTransferChanged: JSON.stringify(copiedTransfer),
185
187
  fileTransfers: copiedTransfer
186
188
  }
187
- const batchOpProps = {
188
- transferHistory,
189
- showModal: store.showModal,
190
- innerWidth: store.innerWidth
191
- }
192
189
  const resProps = {
193
190
  resolutions: deepCopy(store.resolutions),
194
191
  openResolutionEdit
@@ -263,7 +260,6 @@ export default auto(function Index (props) {
263
260
  />
264
261
  <FileInfoModal />
265
262
  <SettingModal store={store} />
266
- <BatchOp {...batchOpProps} />
267
263
  <MoveItemModal store={store} />
268
264
  <div
269
265
  id='outside-context'
@@ -295,9 +291,12 @@ export default auto(function Index (props) {
295
291
  <ConnectionHoppingWarning {...warningProps} />
296
292
  <TerminalCmdSuggestions {...cmdSuggestionsProps} />
297
293
  <TransferQueue />
294
+ <AutoSync config={config} />
295
+ <AutoCheckUpdate config={config} />
298
296
  <WorkspaceSaveModal store={store} />
299
297
  <BookmarkFromHistoryModal />
300
298
  <NotificationContainer />
299
+ <BatchOpRunner />
301
300
  </div>
302
301
  </ConfigProvider>
303
302
  )
@@ -0,0 +1,53 @@
1
+ import { useEffect, useRef } from 'react'
2
+
3
+ export default function AutoSync ({ config }) {
4
+ const lastSyncTimeRef = useRef(0)
5
+ const intervalIdRef = useRef(null)
6
+
7
+ useEffect(() => {
8
+ if (
9
+ !config.syncSetting?.autoSync || config.syncSetting?.autoSyncInterval <= 0
10
+ ) {
11
+ clearInterval(intervalIdRef.current)
12
+ return
13
+ }
14
+ const checkAndSync = async () => {
15
+ const syncSetting = config.syncSetting || {}
16
+ const { autoSync, autoSyncInterval = 0, autoSyncDirection = 'upload' } = syncSetting
17
+
18
+ if (!autoSync) {
19
+ return
20
+ }
21
+
22
+ if (autoSyncInterval <= 0) {
23
+ return
24
+ }
25
+
26
+ const now = Date.now()
27
+ const intervalMs = autoSyncInterval * 60 * 1000
28
+ if (now - lastSyncTimeRef.current >= intervalMs) {
29
+ const { store } = window
30
+ if (autoSyncDirection === 'download') {
31
+ await store.downloadSettingAll()
32
+ } else {
33
+ await store.uploadSettingAll()
34
+ }
35
+ lastSyncTimeRef.current = now
36
+ }
37
+ }
38
+
39
+ intervalIdRef.current = setInterval(checkAndSync, 10000)
40
+
41
+ return () => {
42
+ if (intervalIdRef.current) {
43
+ clearInterval(intervalIdRef.current)
44
+ }
45
+ }
46
+ }, [
47
+ config.syncSetting?.autoSync,
48
+ config.syncSetting?.autoSyncInterval,
49
+ config.syncSetting?.autoSyncDirection
50
+ ])
51
+
52
+ return null
53
+ }
@@ -5,22 +5,65 @@
5
5
  import {
6
6
  Button,
7
7
  Switch,
8
- Tooltip
8
+ Select,
9
+ Space
9
10
  } from 'antd'
10
11
  import {
11
12
  ImportOutlined,
12
- ExportOutlined,
13
- InfoCircleOutlined
13
+ ExportOutlined
14
14
  } from '@ant-design/icons'
15
15
  import Upload from '../common/upload'
16
+ import HelpIcon from '../common/help-icon'
16
17
 
17
18
  const e = window.translate
18
19
 
20
+ const intervalOptions = [
21
+ { value: 0, label: e('autoSyncOnChange') },
22
+ { value: 5, label: '5 ' + e('minutes') },
23
+ { value: 10, label: '10 ' + e('minutes') },
24
+ { value: 15, label: '15 ' + e('minutes') },
25
+ { value: 30, label: '30 ' + e('minutes') },
26
+ { value: 60, label: '1 ' + e('hours') },
27
+ { value: 120, label: '2 ' + e('hours') },
28
+ { value: 360, label: '6 ' + e('hours') },
29
+ { value: 720, label: '12 ' + e('hours') },
30
+ { value: 1440, label: '24 ' + e('hours') }
31
+ ]
32
+
33
+ const directionOptions = [
34
+ { value: 'upload', label: e('uploadSettings') },
35
+ { value: 'download', label: e('downloadSettings') }
36
+ ]
37
+
19
38
  export default function DataTransport (props) {
20
39
  const txt = e('autoSync')
21
40
  const {
22
41
  store
23
42
  } = window
43
+
44
+ const syncSetting = props.config.syncSetting || {}
45
+ const autoSyncEnabled = syncSetting.autoSync || false
46
+ const autoSyncInterval = syncSetting.autoSyncInterval || 0
47
+ const autoSyncDirection = syncSetting.autoSyncDirection || 'upload'
48
+
49
+ function handleAutoSync (checked) {
50
+ store.updateSyncSetting({
51
+ autoSync: checked
52
+ })
53
+ }
54
+
55
+ function handleIntervalChange (value) {
56
+ store.updateSyncSetting({
57
+ autoSyncInterval: value
58
+ })
59
+ }
60
+
61
+ function handleDirectionChange (value) {
62
+ store.updateSyncSetting({
63
+ autoSyncDirection: value
64
+ })
65
+ }
66
+
24
67
  return (
25
68
  <div className='pd2 fix'>
26
69
  <div className='fleft'>
@@ -45,15 +88,33 @@ export default function DataTransport (props) {
45
88
  </div>
46
89
  <div className='fright'>
47
90
  <Switch
48
- checked={props.config.autoSync || false}
91
+ checked={autoSyncEnabled}
49
92
  checkedChildren={txt}
50
- onChange={store.handleAutoSync}
93
+ onChange={handleAutoSync}
51
94
  unCheckedChildren={txt}
52
95
  className='mg3l mg1r'
53
96
  />
54
- <Tooltip title={e('autoSyncTip')}>
55
- <InfoCircleOutlined />
56
- </Tooltip>
97
+ {autoSyncEnabled && (
98
+ <Space className='mg1l' size='small'>
99
+ <Select
100
+ value={autoSyncInterval}
101
+ onChange={handleIntervalChange}
102
+ options={intervalOptions}
103
+ style={{ width: 120 }}
104
+ popupMatchSelectWidth={false}
105
+ />
106
+ <Select
107
+ value={autoSyncDirection}
108
+ onChange={handleDirectionChange}
109
+ options={directionOptions}
110
+ style={{ width: 100 }}
111
+ popupMatchSelectWidth={false}
112
+ />
113
+ </Space>
114
+ )}
115
+ <HelpIcon
116
+ link='https://github.com/electerm/electerm/wiki/Auto-data-Sync'
117
+ />
57
118
  </div>
58
119
  </div>
59
120
  )
@@ -74,9 +74,15 @@ function renderAddonBefore (props, realPath) {
74
74
  }
75
75
 
76
76
  function renderAddonAfter (isLoadingRemote, onGoto, GoIcon, type) {
77
+ const handleClick = (e) => {
78
+ e.stopPropagation()
79
+ if (!isLoadingRemote) {
80
+ onGoto(type)
81
+ }
82
+ }
77
83
  return (
78
84
  <GoIcon
79
- onClick={isLoadingRemote ? () => null : () => onGoto(type)}
85
+ onClick={handleClick}
80
86
  />
81
87
  )
82
88
  }
@@ -6,7 +6,7 @@ import { auto } from 'manate/react'
6
6
  import TreeList from '../tree-list/tree-list'
7
7
 
8
8
  export default auto(function BookmarkSelect (props) {
9
- const { store, from } = props
9
+ const { store, from, autoFocus } = props
10
10
  const {
11
11
  listStyle,
12
12
  openedSideBar,
@@ -38,7 +38,8 @@ export default auto(function BookmarkSelect (props) {
38
38
  bookmarkGroups: store.getBookmarkGroupsTotal(),
39
39
  expandedKeys,
40
40
  leftSidebarWidth,
41
- bookmarkGroupTree: store.bookmarkGroupTree
41
+ bookmarkGroupTree: store.bookmarkGroupTree,
42
+ autoFocus
42
43
  }
43
44
  return (
44
45
  <TreeList
@@ -40,7 +40,9 @@ export default function HistoryItem (props) {
40
40
  e.stopPropagation()
41
41
  refsStatic.get('bookmark-from-history-modal')?.show(item.tab)
42
42
  }
43
-
43
+ if (!item.tab) {
44
+ return null
45
+ }
44
46
  const title = createTitleWithTag(item.tab)
45
47
  const tt = createTitle(item.tab)
46
48
  return (
@@ -6,7 +6,6 @@ import {
6
6
  PlusCircleOutlined,
7
7
  SettingOutlined,
8
8
  UpCircleOutlined,
9
- BarsOutlined,
10
9
  AppstoreOutlined,
11
10
  ThunderboltOutlined
12
11
  } from '@ant-design/icons'
@@ -92,7 +91,6 @@ export default function Sidebar (props) {
92
91
  openAbout,
93
92
  openSettingSync,
94
93
  openTerminalThemes,
95
- toggleBatchOp,
96
94
  setLeftSidePanelWidth
97
95
  } = store
98
96
  const {
@@ -102,7 +100,6 @@ export default function Sidebar (props) {
102
100
  shouldUpgrade
103
101
  } = upgradeInfo
104
102
  const showSetting = showModal === modals.setting
105
- const showBatchOp = showModal === modals.batchOps
106
103
  const settingActive = showSetting && settingTab === settingMap.setting && settingItem.id === 'setting-common'
107
104
  const syncActive = showSetting && settingTab === settingMap.setting && settingItem.id === 'setting-sync'
108
105
  const themeActive = showSetting && settingTab === settingMap.terminalThemes
@@ -190,12 +187,6 @@ export default function Sidebar (props) {
190
187
  spin={isSyncingSetting}
191
188
  />
192
189
  </SideIcon>
193
- <SideIcon
194
- title={e('batchOp')}
195
- active={showBatchOp}
196
- >
197
- <BarsOutlined className='iblock font20 control-icon' onClick={toggleBatchOp} />
198
- </SideIcon>
199
190
  <SideIcon
200
191
  title={e('widgets')}
201
192
  active={widgetsActive}
@@ -75,7 +75,7 @@ export default function AddBtnMenu ({
75
75
 
76
76
  let listContent
77
77
  if (activeTab === 'bookmarks') {
78
- listContent = <BookmarksList store={window.store} />
78
+ listContent = <BookmarksList store={window.store} autoFocus />
79
79
  } else {
80
80
  listContent = <History store={window.store} />
81
81
  }
@@ -40,15 +40,18 @@ export default class AddBtn extends Component {
40
40
  componentWillUnmount () {
41
41
  if (this.state.open) {
42
42
  document.removeEventListener('click', this.handleDocumentClick)
43
+ document.removeEventListener('keydown', this.handleKeyDown)
43
44
  }
44
45
  // Clean up portal container
45
46
  if (this.portalContainer) {
46
47
  document.body.removeChild(this.portalContainer)
47
48
  this.portalContainer = null
48
49
  }
49
- // Clear focus timeout
50
- if (this.çƒ) {
51
- clearTimeout(this.focusTimeout)
50
+ }
51
+
52
+ handleKeyDown = (e) => {
53
+ if (e.key === 'Escape') {
54
+ this.setState({ open: false })
52
55
  }
53
56
  }
54
57
 
@@ -56,8 +59,10 @@ export default class AddBtn extends Component {
56
59
  // Attach or detach document click listener only when menu open state changes
57
60
  if (this.state.open && !prevState.open) {
58
61
  document.addEventListener('click', this.handleDocumentClick)
62
+ document.addEventListener('keydown', this.handleKeyDown)
59
63
  } else if (!this.state.open && prevState.open) {
60
64
  document.removeEventListener('click', this.handleDocumentClick)
65
+ document.removeEventListener('keydown', this.handleKeyDown)
61
66
  }
62
67
  }
63
68
 
@@ -115,17 +120,6 @@ export default class AddBtn extends Component {
115
120
  )
116
121
  }
117
122
 
118
- focusSearchInput = () => {
119
- // Focus the search input after the menu renders
120
- this.focusTimeout = setTimeout(() => {
121
- const searchInput = this.menuRef.current?.querySelector('.add-menu-list .ant-input')
122
- if (searchInput) {
123
- searchInput.focus()
124
- searchInput.select()
125
- }
126
- }, 500)
127
- }
128
-
129
123
  handleAddBtnClick = () => {
130
124
  if (this.state.open) {
131
125
  this.setState({ open: false })
@@ -172,7 +166,7 @@ export default class AddBtn extends Component {
172
166
  menuPosition,
173
167
  menuTop,
174
168
  menuLeft
175
- }, this.focusSearchInput)
169
+ })
176
170
 
177
171
  window.openTabBatch = this.props.batch
178
172
  }
@@ -1,5 +1,5 @@
1
1
  import { useState, useRef, useEffect } from 'react'
2
- import { Button, Space } from 'antd'
2
+ import { Button, Space, Input } from 'antd'
3
3
  import { ArrowRightOutlined, ThunderboltOutlined } from '@ant-design/icons'
4
4
  import message from '../common/message'
5
5
  import InputAutoFocus from '../common/input-auto-focus'
@@ -29,18 +29,14 @@ export default function QuickConnect ({ batch, inputOnly }) {
29
29
  const [inputValue, setInputValue] = useState('')
30
30
  const inputRef = useRef(null)
31
31
 
32
- useEffect(() => {
33
- if (showInput && inputRef.current) {
34
- inputRef.current.focus()
35
- }
36
- }, [showInput])
37
-
38
- // When inputOnly is true, always show the input
32
+ // When inputOnly is true, always show the input (without auto-focus)
39
33
  useEffect(() => {
40
34
  if (inputOnly) {
41
35
  setShowInput(true)
36
+ } else if (showInput && inputRef.current) {
37
+ inputRef.current.focus()
42
38
  }
43
- }, [inputOnly])
39
+ }, [inputOnly, showInput])
44
40
 
45
41
  const handleToggle = () => {
46
42
  setShowInput(!showInput)
@@ -94,7 +90,7 @@ export default function QuickConnect ({ batch, inputOnly }) {
94
90
  <Button
95
91
  {...iconsProps1}
96
92
  />
97
- <InputAutoFocus {...inputProps} />
93
+ {inputOnly ? <Input {...inputProps} /> : <InputAutoFocus {...inputProps} />}
98
94
  <Button
99
95
  {...iconProps}
100
96
  />
@@ -378,11 +378,10 @@ class Term extends Component {
378
378
  }
379
379
 
380
380
  runQuickCommand = (cmd, inputOnly = false) => {
381
- this.term && this.attachAddon._sendData(
382
- cmd +
383
- (inputOnly ? '' : '\r')
384
- )
385
- this.term.focus()
381
+ if (this.term && this.attachAddon) {
382
+ this.attachAddon._sendData(cmd + (inputOnly ? '' : '\r'))
383
+ this.term.focus()
384
+ }
386
385
  }
387
386
 
388
387
  cd = (p) => {