@electerm/electerm-react 5.1.25 → 5.3.0

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 (55) hide show
  1. package/client/common/build-map.js +12 -0
  2. package/client/common/constants.js +12 -0
  3. package/client/common/db.js +7 -5
  4. package/client/common/import-task.js +174 -0
  5. package/client/common/term-of-use.jsx +69 -0
  6. package/client/components/ai/ai-config-props.js +8 -0
  7. package/client/components/bookmark-form/ai-bookmark-form.jsx +18 -15
  8. package/client/components/bookmark-form/bookmark-schema.js +1 -0
  9. package/client/components/bookmark-form/common/bookmark-select.jsx +6 -5
  10. package/client/components/bookmark-form/common/fields.jsx +1 -1
  11. package/client/components/bookmark-form/common/profile-item.jsx +12 -2
  12. package/client/components/bookmark-form/config/common-fields.js +11 -0
  13. package/client/components/bookmark-form/config/rdp.js +1 -1
  14. package/client/components/bookmark-form/tree-select.jsx +5 -16
  15. package/client/components/common/import-progress.jsx +90 -0
  16. package/client/components/footer/footer-entry.jsx +1 -1
  17. package/client/components/main/main.jsx +2 -0
  18. package/client/components/profile/profile-form-rdp.jsx +1 -1
  19. package/client/components/quick-commands/quick-command-transport-mod.jsx +24 -18
  20. package/client/components/rdp/rdp-cred-form.jsx +55 -0
  21. package/client/components/rdp/rdp-session.jsx +97 -6
  22. package/client/components/setting-panel/keywords-transport.jsx +1 -4
  23. package/client/components/setting-panel/setting-modal.jsx +8 -2
  24. package/client/components/setting-panel/start-session-select.jsx +5 -16
  25. package/client/components/setting-sync/setting-sync.jsx +15 -7
  26. package/client/components/sidebar/info-modal.jsx +7 -0
  27. package/client/components/spice/spice-session.jsx +40 -0
  28. package/client/components/terminal/attach-addon-custom.js +12 -10
  29. package/client/components/terminal/shell.js +22 -11
  30. package/client/components/terminal/shortcut-bar-entry.jsx +52 -1
  31. package/client/components/terminal/terminal-color-query.mjs +32 -27
  32. package/client/components/terminal/terminal-interactive.jsx +35 -0
  33. package/client/components/terminal/terminal.jsx +24 -15
  34. package/client/components/terminal-info/activity.jsx +5 -1
  35. package/client/components/terminal-info/base.jsx +37 -22
  36. package/client/components/terminal-info/disk.jsx +5 -1
  37. package/client/components/terminal-info/local-info-resolver.js +89 -0
  38. package/client/components/terminal-info/network.jsx +5 -1
  39. package/client/components/terminal-info/run-cmd.jsx +42 -3
  40. package/client/components/terminal-info/sys-info.jsx +56 -0
  41. package/client/components/terminal-info/terminal-info.jsx +4 -1
  42. package/client/components/terminal-info/terminal-info.styl +22 -7
  43. package/client/components/tree-list/bookmark-transport.jsx +2 -1
  44. package/client/components/tree-list/bookmark-upload.js +56 -41
  45. package/client/components/tree-list/tree-list-row.jsx +11 -4
  46. package/client/components/tree-list/tree-list-rows.js +6 -3
  47. package/client/components/tree-list/tree-list.jsx +4 -2
  48. package/client/store/common.js +38 -3
  49. package/client/store/load-data.js +6 -1
  50. package/client/store/setting.js +7 -4
  51. package/client/store/store.js +3 -12
  52. package/client/store/sync.js +31 -2
  53. package/client/store/tab.js +16 -11
  54. package/client/store/watch.js +10 -1
  55. package/package.json +1 -1
@@ -9,6 +9,7 @@ import TerminalInfoNetwork from './network'
9
9
  import TerminalInfoResource from './resource'
10
10
  import TerminalInfoActivities from './activity'
11
11
  import TerminalInfoDisk from './disk'
12
+ import TerminalInfoSys from './sys-info'
12
13
  import RunCmd from './run-cmd'
13
14
  import { runCmd } from '../terminal/terminal-apis'
14
15
  import './terminal-info.styl'
@@ -21,7 +22,8 @@ export default class TerminalInfoContent extends PureComponent {
21
22
  swap: {},
22
23
  activities: [],
23
24
  disks: [],
24
- network: {}
25
+ network: {},
26
+ sysInfo: null
25
27
  }
26
28
 
27
29
  setStateRef = (...args) => {
@@ -44,6 +46,7 @@ export default class TerminalInfoContent extends PureComponent {
44
46
  return (
45
47
  <>
46
48
  <TerminalInfoBase {...props} {...state} />
49
+ <TerminalInfoSys {...props} {...state} />
47
50
  <TerminalInfoUp {...props} {...state} />
48
51
  <TerminalInfoResource
49
52
  {...props} {...state}
@@ -1,4 +1,3 @@
1
-
2
1
  .terminal-info-icon
3
2
  color var(--text-dark)
4
3
  &:hover
@@ -23,11 +22,27 @@
23
22
  display block
24
23
  .terminal-info-section
25
24
  padding 10px 0
26
- .terminal-info-act
27
- .terminal-info-disk
28
- .terminal-info-network
29
- max-height 260px
30
- overflow-y scroll
25
+ .terminal-info-sys
26
+ border-bottom 1px solid var(--main-darker)
27
+ .terminal-info-sys-rows
28
+ display flex
29
+ flex-direction column
30
+ gap 4px
31
+ .terminal-info-sys-row
32
+ display flex
33
+ gap 8px
34
+ align-items baseline
35
+ font-size 12px
36
+ .terminal-info-sys-key
37
+ flex none
38
+ width 80px
39
+ color var(--text-dark)
40
+ text-transform capitalize
41
+ .terminal-info-sys-val
42
+ flex 1
43
+ overflow hidden
44
+ text-overflow ellipsis
45
+ white-space nowrap
31
46
  .activity-item
32
47
  max-height 1.6em
33
48
  line-height 1.6
@@ -36,4 +51,4 @@
36
51
  max-height none
37
52
  .info-panel-wrap-pin
38
53
  .toggle-info-panel-wrap
39
- color var(--success)
54
+ color var(--success)
@@ -10,11 +10,12 @@ import {
10
10
  } from '@ant-design/icons'
11
11
  import { Button, Space } from 'antd'
12
12
  import Upload from '../common/upload'
13
+ import { beforeBookmarkUpload } from './bookmark-upload'
13
14
 
14
15
  const e = window.translate
15
16
 
16
17
  export default class BookmarkTransport extends PureComponent {
17
- beforeUpload = async (file) => {}
18
+ beforeUpload = beforeBookmarkUpload
18
19
 
19
20
  handleDownload = () => {}
20
21
 
@@ -8,7 +8,9 @@ import { action } from 'manate'
8
8
  import uid from '../../common/uid'
9
9
  import time from '../../common/time'
10
10
  import { fixBookmarks } from '../../common/db-fix'
11
- import delay from '../../common/wait'
11
+ import { runImportTask } from '../../common/import-task'
12
+
13
+ const e = window.translate
12
14
 
13
15
  function fixBookmarksId (bookmarks) {
14
16
  return bookmarks.map(item => {
@@ -21,6 +23,7 @@ function fixBookmarksId (bookmarks) {
21
23
  return item
22
24
  }).filter(Boolean)
23
25
  }
26
+
24
27
  export const bookmarkUpload = action(async (file) => {
25
28
  const { store } = window
26
29
  const { bookmarks, bookmarkGroups } = store
@@ -58,50 +61,62 @@ export const bookmarkUpload = action(async (file) => {
58
61
 
59
62
  const fixed = fixBookmarks(bookmarks1)
60
63
 
61
- fixed.forEach(bg => {
62
- if (!bmTree.has(bg.id)) {
63
- store.bookmarks.push(bg)
64
- }
64
+ await runImportTask({
65
+ title: e('import'),
66
+ batch: 200,
67
+ stopWatchers: ['bookmarks', 'bookmarkGroups'],
68
+ steps: [
69
+ {
70
+ label: e('bookmarks'),
71
+ items: fixed,
72
+ process: (chunk) => {
73
+ chunk.forEach(bg => {
74
+ if (!bmTree.has(bg.id)) {
75
+ store.bookmarks.push(bg)
76
+ }
77
+ })
78
+ }
79
+ },
80
+ {
81
+ label: e('bookmarkCategory'),
82
+ items: bookmarkGroups1,
83
+ process: (chunk) => {
84
+ chunk.forEach(bg => {
85
+ if (!bmgTree.has(bg.id)) {
86
+ store.bookmarkGroups.push(bg)
87
+ } else {
88
+ const bg1 = store.bookmarkGroups.find(
89
+ b => b.id === bg.id
90
+ )
91
+ bg1.bookmarkIds = uniq(
92
+ [
93
+ ...(bg1.bookmarkIds || []),
94
+ ...(bg.bookmarkIds || [])
95
+ ]
96
+ )
97
+ bg1.bookmarkGroupIds = uniq(
98
+ [
99
+ ...(bg1.bookmarkGroupIds || []),
100
+ ...(bg.bookmarkGroupIds || [])
101
+ ]
102
+ )
103
+ }
104
+ })
105
+ }
106
+ },
107
+ {
108
+ label: e('bookmarks'),
109
+ weight: 1,
110
+ run: () => {
111
+ store.fixBookmarkGroups()
112
+ }
113
+ }
114
+ ]
65
115
  })
66
116
 
67
- bookmarkGroups1.forEach(bg => {
68
- if (!bmgTree.has(bg.id)) {
69
- store.bookmarkGroups.push(bg)
70
- } else {
71
- const bg1 = store.bookmarkGroups.find(
72
- b => b.id === bg.id
73
- )
74
- bg1.bookmarkIds = uniq(
75
- [
76
- ...(bg1.bookmarkIds || []),
77
- ...(bg.bookmarkIds || [])
78
- ]
79
- )
80
- bg1.bookmarkGroupIds = uniq(
81
- [
82
- ...(bg1.bookmarkGroupIds || []),
83
- ...(bg.bookmarkGroupIds || [])
84
- ]
85
- )
86
- }
87
- })
88
-
89
- store.fixBookmarkGroups()
90
-
91
117
  return false
92
118
  })
93
119
 
94
120
  export async function beforeBookmarkUpload (file) {
95
- const names = [
96
- 'bookmarks',
97
- 'bookmarkGroups'
98
- ]
99
- for (const name of names) {
100
- window[`watch${name}`].stop()
101
- }
102
- bookmarkUpload(file)
103
- await delay(1000)
104
- for (const name of names) {
105
- window[`watch${name}`].start()
106
- }
121
+ return bookmarkUpload(file)
107
122
  }
@@ -1,14 +1,20 @@
1
+ import { memo } from 'react'
1
2
  import TreeExpander from './tree-expander'
2
3
  import TreeListItem from './tree-list-item'
3
4
  import TreeItemOp from './tree-item-op'
4
5
  import { treeLevelIndent } from './tree-list-layout'
5
6
  import createName from '../../common/create-title'
6
7
 
7
- export default function TreeListRow (props) {
8
+ // memoized: VirtualTreeList re-renders every scroll frame and re-invokes
9
+ // renderItem for the whole visible window; without memo each row (including
10
+ // the antd Popconfirm/Tooltip in TreeItemOp) re-renders per frame.
11
+ // `expanded` is a boolean (not the expandedKeys array) because the array is
12
+ // mutated in place, so identity-based memo comparison would miss changes.
13
+ function TreeListRow (props) {
8
14
  const {
9
15
  row,
10
16
  keyword,
11
- expandedKeys,
17
+ expanded,
12
18
  activeItemId,
13
19
  searchSelectedRowKey,
14
20
  staticList,
@@ -34,7 +40,6 @@ export default function TreeListRow (props) {
34
40
  item?.bookmarkIds?.length ||
35
41
  item?.bookmarkGroupIds?.length
36
42
  )
37
- const isGroupExpanded = Boolean(keyword) || expandedKeys.includes(item.id)
38
43
  const itemProps = {
39
44
  item,
40
45
  isGroup,
@@ -94,7 +99,7 @@ export default function TreeListRow (props) {
94
99
  level={parentId}
95
100
  group={item}
96
101
  hasChildren={groupHasChildren}
97
- shouldOpen={isGroupExpanded}
102
+ shouldOpen={expanded}
98
103
  onExpand={handleExpand}
99
104
  onUnExpand={handleUnExpand}
100
105
  />
@@ -114,3 +119,5 @@ export default function TreeListRow (props) {
114
119
  </div>
115
120
  )
116
121
  }
122
+
123
+ export default memo(TreeListRow)
@@ -37,7 +37,10 @@ export function buildVisibleTreeRows ({
37
37
  sortTitle,
38
38
  sortHost
39
39
  }) {
40
- const groupTree = bookmarkGroupTree || {}
40
+ const groupTree = bookmarkGroupTree || new Map()
41
+ const getGroup = id => groupTree instanceof Map
42
+ ? groupTree.get(id)
43
+ : groupTree[id]
41
44
  const rows = []
42
45
  const matchedRowKeys = []
43
46
  const expandedKeySet = new Set(expandedKeys || [])
@@ -72,7 +75,7 @@ export function buildVisibleTreeRows ({
72
75
  }
73
76
  const hasMatch = (group.bookmarkIds || []).some(bookmarkMatches) ||
74
77
  (group.bookmarkGroupIds || []).some(id => {
75
- return groupHasMatchedBookmarks(groupTree[id])
78
+ return groupHasMatchedBookmarks(getGroup(id))
76
79
  })
77
80
  groupMatchCache.set(group.id, hasMatch)
78
81
  return hasMatch
@@ -100,7 +103,7 @@ export function buildVisibleTreeRows ({
100
103
  : `#${group.id}`
101
104
 
102
105
  for (const groupId of group.bookmarkGroupIds || []) {
103
- visitGroup(groupTree[groupId], nextParentId, depth + 1)
106
+ visitGroup(getGroup(groupId), nextParentId, depth + 1)
104
107
  }
105
108
 
106
109
  const bookmarkIds = group.bookmarkIds || []
@@ -740,11 +740,13 @@ export default class ItemListTree extends Component {
740
740
  }
741
741
 
742
742
  renderVirtualRow = (row, editor) => {
743
+ const { keyword } = this.state
744
+ const { expandedKeys } = this.props
743
745
  return (
744
746
  <TreeListRow
745
747
  row={row}
746
- keyword={this.state.keyword}
747
- expandedKeys={this.props.expandedKeys}
748
+ keyword={keyword}
749
+ expanded={Boolean(keyword) || expandedKeys.includes(row.item.id)}
748
750
  activeItemId={this.props.activeItemId}
749
751
  searchSelectedRowKey={this.state.searchSelectedRowKey}
750
752
  staticList={this.props.staticList}
@@ -15,10 +15,13 @@ import {
15
15
  connectionMap,
16
16
  lastAiChatSessionIdKey,
17
17
  mobileBreakpoint,
18
- splitMap
18
+ splitMap,
19
+ settingAiId,
20
+ settingSyncId
19
21
  } from '../common/constants'
20
22
  import * as ls from '../common/safe-local-storage'
21
23
  import { refs, refsStatic } from '../components/common/ref'
24
+ import { requireTermOfUse } from '../common/term-of-use'
22
25
  import { action } from 'manate'
23
26
  import uid from '../common/uid'
24
27
  import deepCopy from 'json-deep-copy'
@@ -56,7 +59,9 @@ export default Store => {
56
59
  }
57
60
 
58
61
  Store.prototype.toggleAIConfig = function () {
59
- window.store.showAIConfigModal = true
62
+ requireTermOfUse('ai', () => {
63
+ window.store.showAIConfigModal = true
64
+ })
60
65
  }
61
66
 
62
67
  Store.prototype.onResize = debounce(async function () {
@@ -99,6 +104,18 @@ export default Store => {
99
104
  }
100
105
 
101
106
  Store.prototype.setSettingItem = function (v) {
107
+ // entering the AI / sync setting page requires the term of use
108
+ // confirmation first (when the term is defined)
109
+ if (
110
+ v && (
111
+ v.id === settingAiId || v.id === settingSyncId
112
+ )
113
+ ) {
114
+ const type = v.id === settingAiId ? 'ai' : 'sync'
115
+ return requireTermOfUse(type, () => {
116
+ window.store.settingItem = v
117
+ })
118
+ }
102
119
  window.store.settingItem = v
103
120
  }
104
121
 
@@ -193,7 +210,13 @@ export default Store => {
193
210
  type,
194
211
  authType
195
212
  } = tab
196
- if (!profile || authType !== 'profiles') {
213
+ // SSH picks auth type explicitly (password/privateKey/profiles radio),
214
+ // other types only have the profile dropdown, so a selected profile
215
+ // always means profile auth for them
216
+ const useProfile = type === connectionMap.ssh
217
+ ? authType === 'profiles'
218
+ : true
219
+ if (!profile || !useProfile) {
197
220
  return tab
198
221
  }
199
222
  let p = window.store.profiles.find(x => x.id === profile)
@@ -224,10 +247,17 @@ export default Store => {
224
247
  ...tab,
225
248
  ...filtered
226
249
  }
250
+ } else if (type === connectionMap.ftp) {
251
+ const filtered = pickBy(p.ftp, (value) => value !== undefined && value !== '')
252
+ return {
253
+ ...tab,
254
+ ...filtered
255
+ }
227
256
  }
228
257
  delete p.rdp
229
258
  delete p.vnc
230
259
  delete p.telnet
260
+ delete p.ftp
231
261
  const filtered = pickBy(p, (value) => value !== undefined && value !== '')
232
262
  return {
233
263
  ...tab,
@@ -455,6 +485,11 @@ export default Store => {
455
485
  let i = len - 1
456
486
  for (;i >= 0; i--) {
457
487
  const f = profiles[i]
488
+ // migrate old rdp profile key userName -> username
489
+ if (f.rdp?.userName !== undefined) {
490
+ f.rdp.username = f.rdp.userName
491
+ delete f.rdp.userName
492
+ }
458
493
  if (f.name) {
459
494
  continue
460
495
  }
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import { dbNames, getData, fetchInitData } from '../common/db'
6
+ import deepCopy from 'json-deep-copy'
6
7
  import parseInt10 from '../common/parse-int10'
7
8
  import { infoTabs, statusMap, defaultEnvLang } from '../common/constants'
8
9
  import generate from '../common/id-with-stamp'
@@ -237,7 +238,11 @@ export default (Store) => {
237
238
  .then(arr => {
238
239
  for (const { name, data } of arr) {
239
240
  const dt = JSON.parse(data || '[]')
240
- refsStatic.add('oldState-' + name, dt)
241
+ // seed the watcher snapshot with a *copy* - manate wraps the exact
242
+ // array assigned into the store, so seeding with `dt` itself would
243
+ // alias snapshot and store to one backing array; the watcher's
244
+ // no-change early-return would then skip DB writes forever
245
+ refsStatic.add('oldState-' + name, deepCopy(dt))
241
246
  if (name === 'bookmarks') {
242
247
  ext.bookmarksMap = new Map(
243
248
  dt.map(d => [d.id, d])
@@ -14,6 +14,7 @@ import { buildNewTheme } from '../common/terminal-theme'
14
14
  import getInitItem from '../common/init-setting-item'
15
15
  import newTerm from '../common/new-terminal'
16
16
  import settingList from '../common/setting-list'
17
+ import { requireTermOfUse } from '../common/term-of-use'
17
18
 
18
19
  const e = window.translate
19
20
 
@@ -103,11 +104,13 @@ export default Store => {
103
104
  ) {
104
105
  return store.hideSettingModal()
105
106
  }
106
- store.storeAssign({
107
- settingTab: settingMap.setting
107
+ requireTermOfUse('sync', () => {
108
+ store.storeAssign({
109
+ settingTab: settingMap.setting
110
+ })
111
+ store.setSettingItem(settingList().find(d => d.id === settingSyncId))
112
+ store.openSettingModal()
108
113
  })
109
- store.setSettingItem(settingList().find(d => d.id === settingSyncId))
110
- store.openSettingModal()
111
114
  }
112
115
 
113
116
  Store.prototype.openTerminalThemes = function () {
@@ -42,6 +42,7 @@ import {
42
42
  } from '../common/constants'
43
43
  import getInitItem from '../common/init-setting-item'
44
44
  import createTitle from '../common/create-title'
45
+ import buildMap from '../common/build-map'
45
46
  import {
46
47
  theme
47
48
  } from 'antd'
@@ -278,12 +279,7 @@ class Store {
278
279
  const {
279
280
  bookmarks
280
281
  } = window.store
281
- return bookmarks.reduce((p, v) => {
282
- return {
283
- ...p,
284
- [v.id]: v
285
- }
286
- }, {})
282
+ return buildMap(bookmarks)
287
283
  }
288
284
 
289
285
  hasSshConfig () {
@@ -296,12 +292,7 @@ class Store {
296
292
  const {
297
293
  bookmarkGroups
298
294
  } = window.store
299
- return bookmarkGroups.reduce((p, v) => {
300
- return {
301
- ...p,
302
- [v.id]: v
303
- }
304
- }, {})
295
+ return buildMap(bookmarkGroups)
305
296
  }
306
297
 
307
298
  get hasOldConnectionHoppingBookmark () {
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { get, pick, debounce } from 'lodash-es'
6
6
  import copy from 'json-deep-copy'
7
+ import { action } from 'manate'
7
8
  import {
8
9
  settingMap, packInfo, syncTypes, syncDataMaps
9
10
  } from '../common/constants'
@@ -13,6 +14,9 @@ import download from '../common/download'
13
14
  import { fixBookmarks } from '../common/db-fix'
14
15
  import dayjs from 'dayjs'
15
16
  import parseJsonSafe from '../common/parse-json-safe'
17
+ import { runImportTask } from '../common/import-task'
18
+
19
+ const e = window.translate
16
20
 
17
21
  const {
18
22
  version: packVer
@@ -694,15 +698,40 @@ export default (Store) => {
694
698
  const { store } = window
695
699
  const objs = JSON.parse(txt)
696
700
  const { names } = store.getDataSyncNames(true)
701
+ const fixed = {}
697
702
  for (const n of names) {
698
- let arr = objs[n]
703
+ let arr = objs[n] || []
699
704
  if (n === settingMap.terminalThemes) {
700
705
  arr = store.fixThemes(arr)
701
706
  } else if (n === settingMap.bookmarks) {
702
707
  arr = fixBookmarks(arr)
703
708
  }
704
- store.setItems(n, objs[n])
709
+ fixed[n] = arr
705
710
  }
711
+ // clear all targets first - importAll replaces data sets, not appends.
712
+ // watchers are stopped inside runImportTask, so this is silent until restart
713
+ action(() => {
714
+ for (const n of names) {
715
+ store.setItems(n, [])
716
+ }
717
+ })()
718
+ await runImportTask({
719
+ title: e('import'),
720
+ batch: 200,
721
+ stopWatchers: names,
722
+ // one chunked step per data set (bookmarks, groups, themes, ...),
723
+ // so the progress bar advances per batch instead of per data set
724
+ steps: names.map((n) => {
725
+ const arr = fixed[n]
726
+ return {
727
+ label: n,
728
+ items: arr,
729
+ process: (chunk) => {
730
+ store[n].push(...chunk)
731
+ }
732
+ }
733
+ })
734
+ })
706
735
  store.updateConfig(stripServerManagedKeys(objs.config))
707
736
  if (objs.config?.theme) {
708
737
  store.setTheme(objs.config.theme)
@@ -111,17 +111,22 @@ export default Store => {
111
111
  // Remove old tab
112
112
  tabs.splice(index, 1)
113
113
 
114
- setTimeout(() => {
115
- if (store.activeTabId === tabId) {
116
- store.activeTabId = newTab.id
117
- }
118
-
119
- // Update batch current tab ID if needed
120
- const batchProp = `activeTabId${oldBatch}`
121
- if (store[batchProp] === tabId) {
122
- store[batchProp] = newTab.id
123
- }
124
- }, 0)
114
+ // Update active tab id synchronously (same as addTab) so the new tab's
115
+ // container is already visible (display:block) by the time its Terminal
116
+ // mounts. Deferring this via setTimeout(0) left the container hidden at
117
+ // mount time, causing the initial fit()/createTerm() to use xterm's
118
+ // default 80x24 size instead of the real fitted size - which then got
119
+ // baked into the remote pty and only partially corrected later, leaving
120
+ // full-screen apps like tmux/vim rendering at the wrong size.
121
+ if (store.activeTabId === tabId) {
122
+ store.activeTabId = newTab.id
123
+ }
124
+
125
+ // Update batch current tab ID if needed
126
+ const batchProp = `activeTabId${oldBatch}`
127
+ if (store[batchProp] === tabId) {
128
+ store[batchProp] = newTab.id
129
+ }
125
130
  }
126
131
 
127
132
  Store.prototype.reloadAllTabs = function () {
@@ -31,6 +31,16 @@ export default store => {
31
31
  old,
32
32
  n
33
33
  )
34
+ const newOrder = (n || []).map(d => d.id)
35
+ // with large collections (thousands of bookmarks) this autoRun fires
36
+ // often; when neither content nor order changed, skip the deep-copy
37
+ // snapshot and the DB writes entirely
38
+ const orderChanged = !old ||
39
+ old.length !== (n || []).length ||
40
+ old.some((d, i) => d.id !== newOrder[i])
41
+ if (!updated.length && !added.length && !removed.length && !orderChanged) {
42
+ return store[name]
43
+ }
34
44
  // Update snapshot immediately before async DB writes to prevent
35
45
  // race conditions: a second autoRun firing before the first
36
46
  // completes would see stale oldState and re-insert the same items,
@@ -41,7 +51,6 @@ export default store => {
41
51
  ...updated.map(item => update(item.id, item, name, false)),
42
52
  added.length ? insert(name, added) : Promise.resolve()
43
53
  ])
44
- const newOrder = (n || []).map(d => d.id)
45
54
  await update(
46
55
  `${name}:order`,
47
56
  newOrder
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "5.1.25",
3
+ "version": "5.3.0",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",