@electerm/electerm-react 5.1.23 → 5.2.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 (37) hide show
  1. package/client/common/build-map.js +12 -0
  2. package/client/common/db.js +7 -5
  3. package/client/common/import-task.js +174 -0
  4. package/client/components/bookmark-form/ai-bookmark-form.jsx +18 -15
  5. package/client/components/bookmark-form/common/bookmark-select.jsx +6 -5
  6. package/client/components/bookmark-form/common/fields.jsx +1 -1
  7. package/client/components/bookmark-form/common/profile-item.jsx +12 -2
  8. package/client/components/bookmark-form/config/rdp.js +1 -1
  9. package/client/components/bookmark-form/tree-select.jsx +5 -16
  10. package/client/components/common/import-progress.jsx +90 -0
  11. package/client/components/layout/layout.jsx +6 -4
  12. package/client/components/main/main.jsx +2 -0
  13. package/client/components/profile/profile-form-rdp.jsx +1 -1
  14. package/client/components/quick-commands/quick-command-transport-mod.jsx +24 -18
  15. package/client/components/rdp/rdp-cred-form.jsx +55 -0
  16. package/client/components/rdp/rdp-session.jsx +97 -6
  17. package/client/components/setting-panel/keywords-transport.jsx +1 -4
  18. package/client/components/setting-panel/setting-modal.jsx +8 -2
  19. package/client/components/setting-panel/start-session-select.jsx +5 -16
  20. package/client/components/spice/spice-session.jsx +40 -0
  21. package/client/components/terminal/shortcut-bar-entry.jsx +4 -2
  22. package/client/components/terminal/shortcut-bar.jsx +59 -2
  23. package/client/components/terminal/shortcut-bar.styl +4 -2
  24. package/client/components/terminal/terminal-color-query.mjs +32 -27
  25. package/client/components/terminal/terminal.jsx +13 -9
  26. package/client/components/tree-list/bookmark-transport.jsx +2 -1
  27. package/client/components/tree-list/bookmark-upload.js +56 -41
  28. package/client/components/tree-list/tree-list-row.jsx +11 -4
  29. package/client/components/tree-list/tree-list-rows.js +6 -3
  30. package/client/components/tree-list/tree-list.jsx +4 -2
  31. package/client/store/common.js +19 -1
  32. package/client/store/init-state.js +3 -0
  33. package/client/store/load-data.js +6 -1
  34. package/client/store/store.js +3 -12
  35. package/client/store/sync.js +31 -2
  36. package/client/store/watch.js +10 -1
  37. package/package.json +1 -1
@@ -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}
@@ -193,7 +193,13 @@ export default Store => {
193
193
  type,
194
194
  authType
195
195
  } = tab
196
- if (!profile || authType !== 'profiles') {
196
+ // SSH picks auth type explicitly (password/privateKey/profiles radio),
197
+ // other types only have the profile dropdown, so a selected profile
198
+ // always means profile auth for them
199
+ const useProfile = type === connectionMap.ssh
200
+ ? authType === 'profiles'
201
+ : true
202
+ if (!profile || !useProfile) {
197
203
  return tab
198
204
  }
199
205
  let p = window.store.profiles.find(x => x.id === profile)
@@ -224,10 +230,17 @@ export default Store => {
224
230
  ...tab,
225
231
  ...filtered
226
232
  }
233
+ } else if (type === connectionMap.ftp) {
234
+ const filtered = pickBy(p.ftp, (value) => value !== undefined && value !== '')
235
+ return {
236
+ ...tab,
237
+ ...filtered
238
+ }
227
239
  }
228
240
  delete p.rdp
229
241
  delete p.vnc
230
242
  delete p.telnet
243
+ delete p.ftp
231
244
  const filtered = pickBy(p, (value) => value !== undefined && value !== '')
232
245
  return {
233
246
  ...tab,
@@ -455,6 +468,11 @@ export default Store => {
455
468
  let i = len - 1
456
469
  for (;i >= 0; i--) {
457
470
  const f = profiles[i]
471
+ // migrate old rdp profile key userName -> username
472
+ if (f.rdp?.userName !== undefined) {
473
+ f.rdp.username = f.rdp.userName
474
+ delete f.rdp.userName
475
+ }
458
476
  if (f.name) {
459
477
  continue
460
478
  }
@@ -169,6 +169,9 @@ export default () => {
169
169
 
170
170
  // touch-only shortcut bar (see components/terminal/shortcut-bar-entry.jsx)
171
171
  shortcutBarVisible: false,
172
+ // px the bar is lifted above a page-overlaying system input panel
173
+ // (iOS / HarmonyOS) — see shortcut-bar.jsx
174
+ shortcutBarKbOffset: 0,
172
175
 
173
176
  // sidebar
174
177
  openedSideBar: ls.getItem(openedSidebarKey) || '',
@@ -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])
@@ -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)
@@ -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.23",
3
+ "version": "5.2.0",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",