@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.
@@ -370,7 +370,12 @@ function parseQuickConnect (str) {
370
370
  opts.port = parseInt(port, 10)
371
371
  }
372
372
  if (username !== undefined && username !== '') {
373
- opts.username = username
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
- let jsonStr = aiResponse.response.trim()
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 = JSON.parse(jsonStr)
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(e('aiGenerateError') || 'AI generation failed: ' + error.message)
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 (!editorText) return []
133
+ function getGeneratedData (txt = editorText) {
134
+ if (!txt) return []
114
135
  let parsed = null
115
136
  try {
116
- parsed = fixBookmarkData(JSON.parse(editorText))
137
+ parsed = JSON.parse(txt)
117
138
  } catch (err) {
118
139
  return []
119
140
  }
120
141
  if (!parsed) return []
121
- return Array.isArray(parsed) ? parsed : [parsed]
142
+ const arr = Array.isArray(parsed) ? parsed : [parsed]
143
+ return arr.map(d => fixBookmarkData(d))
122
144
  }
123
145
 
124
- const createBookmark = async (bm) => {
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
- for (const item of parsed) {
159
- // set defaults like mcpAddBookmark would
160
- await createBookmark(item)
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('confirmBookmarkData') || 'Confirm Bookmark Data',
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 type='primary' onClick={handleConfirm}>
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/cetificate',
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 opts = {
21
- options: Object.keys(proxyTree)
22
- .map(d => {
23
- return {
24
- label: d,
25
- value: d
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
- {...opts}
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', 'username', 'term'],
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
- delete window.et.openBookmarkWithAIMode
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
- let finalToPath = toPath
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 = this.isFtp ? 1 : 30
762
- const BigFilesBatch = this.isFtp ? 1 : 3
763
- const foldersBatch = this.isFtp ? 1 : 50
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,
@@ -28,7 +28,7 @@ export default class ScrollFiles extends Component {
28
28
  const arr = hasPager
29
29
  ? list.slice(start, end)
30
30
  : list
31
- return arr.map(this.props.renderItem)
31
+ return arr.map((item, index) => this.props.renderItem(item, index))
32
32
  }
33
33
 
34
34
  renderPager () {
@@ -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
- // 1. We try the version variables first.
147
- // 2. We try your verified fish check: fish --version ...
148
- // 3. We use ps -p $$ to check the process name (highly reliable in Linux/Docker).
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.includes('fish')) return 'fish'
161
- if (shell.includes('zsh')) return 'zsh'
162
- if (shell.includes('bash')) return 'bash'
163
- return 'sh' // Fallback for sh/ash/dash
158
+ if (!shell) {
159
+ return 'sh'
160
+ }
161
+
162
+ return detectShellType(shell)
164
163
  }
@@ -953,6 +953,7 @@ class Term extends Component {
953
953
  this.term = term
954
954
  term.onSelectionChange(this.onSelectionChange)
955
955
  term.attachCustomKeyEventHandler(this.handleKeyboardEvent.bind(this))
956
+ this.fitAddon.fit()
956
957
  await this.remoteInit(term)
957
958
  }
958
959
 
@@ -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
- export default function TreeExpander (props) {
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
- export default function TreeListItem (props) {
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "3.6.16",
3
+ "version": "3.7.16",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",
@@ -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
- }