@electerm/electerm-react 3.11.0 → 3.11.12

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.
@@ -96,6 +96,7 @@ export const serialBookmarkSchema = {
96
96
  xon: z.boolean().optional().describe('XON flow control'),
97
97
  xoff: z.boolean().optional().describe('XOFF flow control'),
98
98
  xany: z.boolean().optional().describe('XANY flow control'),
99
+ lineEnding: z.enum(['', '\r', '\n', '\r\n']).optional().describe('Line ending for Enter key: "" (none), "\\r" (CR), "\\n" (LF), "\\r\\n" (CR+LF)'),
99
100
  description: z.string().optional().describe('Bookmark description')
100
101
  }
101
102
 
@@ -180,6 +180,13 @@ export const commonParities = [
180
180
  'none', 'even', 'mark', 'odd', 'space'
181
181
  ]
182
182
 
183
+ export const commonLineEndings = [
184
+ { value: '', label: 'none' },
185
+ { value: '\r', label: 'CR' },
186
+ { value: '\n', label: 'LF' },
187
+ { value: '\r\n', label: 'CR+LF' }
188
+ ]
189
+
183
190
  export const maxBatchInput = 30
184
191
  export const windowControlWidth = 94
185
192
  export const baseUpdateCheckUrls = [
@@ -115,8 +115,11 @@ function parseQuickConnect (str) {
115
115
  }
116
116
 
117
117
  try {
118
+ // Strip trailing slashes (supports pasted URLs like host/ or ssh://host/)
119
+ const input = trimmed.replace(/\/+$/, '')
120
+
118
121
  // Detect protocol
119
- const protocolMatch = trimmed.match(/^(ssh|telnet|vnc|rdp|spice|serial|ftp|https?|electerm):\/\//i)
122
+ const protocolMatch = input.match(/^(ssh|telnet|vnc|rdp|spice|serial|ftp|https?|electerm):\/\//i)
120
123
 
121
124
  let protocol = ''
122
125
  let connectionString = ''
@@ -129,28 +132,28 @@ function parseQuickConnect (str) {
129
132
  if (protocol === 'http' || protocol === 'https') {
130
133
  protocol = 'web'
131
134
  }
132
- connectionString = trimmed.slice(protocolMatch[0].length)
135
+ connectionString = input.slice(protocolMatch[0].length)
133
136
  } else {
134
137
  // Shortcut format - default to SSH
135
138
  // Match user@host or user@host:port or just host or host:port
136
139
  // Use last colon to determine port for host:port format
137
- if (/^[\w.-]+@[\w.-]+/.test(trimmed)) {
140
+ if (/^[\w.-]+@[\w.-]+/.test(input)) {
138
141
  // user@host or user@host:port
139
142
  protocol = 'ssh'
140
- connectionString = trimmed
141
- } else if (/^[\w.-]+:.*:[\d]+$/.test(trimmed)) {
143
+ connectionString = input
144
+ } else if (/^[\w.-]+:.*:[\d]+$/.test(input)) {
142
145
  // host:port format with colons in hostname (e.g., localhost:23344, zxd:localhost:23344)
143
146
  // Check if the last colon is followed by digits (port number)
144
147
  protocol = 'ssh'
145
- connectionString = trimmed
146
- } else if (/^[\w.-]+:[\d]+$/.test(trimmed)) {
148
+ connectionString = input
149
+ } else if (/^[\w.-]+:[\d]+$/.test(input)) {
147
150
  // host:port (no username, simple format like host:22)
148
151
  protocol = 'ssh'
149
- connectionString = trimmed
150
- } else if (/^[\w.-]+$/.test(trimmed)) {
152
+ connectionString = input
153
+ } else if (/^[\w.-]+$/.test(input)) {
151
154
  // just host
152
155
  protocol = 'ssh'
153
- connectionString = trimmed
156
+ connectionString = input
154
157
  } else {
155
158
  return null
156
159
  }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Sanitize a filename for cross-platform file transfers.
3
+ *
4
+ * When transferring files between different OS (Linux <-> Windows <-> macOS),
5
+ * filenames may contain characters that are illegal on the destination OS.
6
+ * Windows is the most restrictive common platform, so we use its rules as
7
+ * the baseline for maximum compatibility.
8
+ *
9
+ * Rules applied:
10
+ * - Remove control characters (0x00-0x1F)
11
+ * - Replace reserved characters: < > : " / \ | ? * with _
12
+ * - Remove leading/trailing dots and spaces (Windows restriction)
13
+ * - Reject reserved Windows device names: CON, PRN, AUX, NUL, COM1-9, LPT1-9
14
+ * - Limit filename length to 255 bytes (common filesystem limit)
15
+ * - Fallback to 'unnamed' if result is empty
16
+ */
17
+
18
+ // Characters illegal on Windows (and problematic on many systems)
19
+ // eslint-disable-next-line no-control-regex
20
+ const ILLEGAL_CHARS = /[<>:"/\\|?\x00-\x1f]/g
21
+
22
+ // Leading/trailing dots and spaces are problematic on Windows
23
+ const LEADING_TRAILING = /^[.\s]+|[.\s]+$/g
24
+
25
+ // Reserved Windows device names (case-insensitive)
26
+ const RESERVED_NAMES = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/i
27
+
28
+ const MAX_FILENAME_LENGTH = 255
29
+
30
+ const REPLACEMENT_CHAR = '_'
31
+
32
+ export default function sanitizeFilename (name) {
33
+ if (!name || typeof name !== 'string') {
34
+ return 'unnamed'
35
+ }
36
+
37
+ let safe = name
38
+ // Replace illegal characters
39
+ .replace(ILLEGAL_CHARS, REPLACEMENT_CHAR)
40
+ // Strip leading/trailing dots and spaces
41
+ .replace(LEADING_TRAILING, '')
42
+
43
+ // Handle reserved Windows device names by appending underscore
44
+ if (RESERVED_NAMES.test(safe)) {
45
+ safe = safe + REPLACEMENT_CHAR
46
+ }
47
+
48
+ // Truncate to max length
49
+ if (safe.length > MAX_FILENAME_LENGTH) {
50
+ const ext = safe.lastIndexOf('.')
51
+ if (ext > 0) {
52
+ // Preserve extension when truncating
53
+ const extension = safe.slice(ext)
54
+ safe = safe.slice(0, MAX_FILENAME_LENGTH - extension.length) + extension
55
+ } else {
56
+ safe = safe.slice(0, MAX_FILENAME_LENGTH)
57
+ }
58
+ }
59
+
60
+ // Fallback for empty result
61
+ if (!safe) {
62
+ return 'unnamed'
63
+ }
64
+
65
+ return safe
66
+ }
@@ -81,6 +81,7 @@ const bookmarkSchema = {
81
81
  xon: 'boolean - enable XON flow control, default is false',
82
82
  xoff: 'boolean - enable XOFF flow control, default is false',
83
83
  xany: 'boolean - enable XANY flow control, default is false',
84
+ lineEnding: 'string - line ending for Enter key: "" (none), "\\r" (CR), "\\n" (LF), "\\r\\n" (CR+LF)',
84
85
  runScripts: 'array - run scripts after connected ({delay,script})',
85
86
  description: 'string - bookmark description'
86
87
  },
@@ -1,5 +1,5 @@
1
1
  import { formItemLayout } from '../../../common/form-layout.js'
2
- import { terminalSerialType, commonBaudRates, commonDataBits, commonStopBits, commonParities } from '../../../common/constants.js'
2
+ import { terminalSerialType, commonBaudRates, commonDataBits, commonStopBits, commonParities, commonLineEndings } from '../../../common/constants.js'
3
3
  import defaultSettings from '../../../common/default-setting.js'
4
4
  import { createBaseInitValues, getTerminalBackgroundDefaults } from '../common/init-values.js'
5
5
  import { commonFields } from './common-fields.js'
@@ -57,6 +57,7 @@ const serialConfig = {
57
57
  { type: 'switch', name: 'xon', label: 'xon', valuePropName: 'checked' },
58
58
  { type: 'switch', name: 'xoff', label: 'xoff', valuePropName: 'checked' },
59
59
  { type: 'switch', name: 'xany', label: 'xany', valuePropName: 'checked' },
60
+ { type: 'select', name: 'lineEnding', label: 'lineEnding', options: commonLineEndings.map(d => ({ value: d.value, label: d.label })) },
60
61
  commonFields.runScripts,
61
62
  commonFields.description,
62
63
  { type: 'input', name: 'type', label: 'type', hidden: true }
@@ -0,0 +1,45 @@
1
+ import React, { useMemo, useCallback } from 'react'
2
+ import { Select } from 'antd'
3
+
4
+ const e = window.translate
5
+
6
+ export default function FontSelect ({
7
+ value,
8
+ onChange,
9
+ placeholder,
10
+ style
11
+ }) {
12
+ const { fonts = [] } = window.et || {}
13
+
14
+ const options = useMemo(() => {
15
+ return fonts.map(f => ({
16
+ value: f,
17
+ label: (
18
+ <span
19
+ className='iblock'
20
+ style={{ fontFamily: f }}
21
+ >
22
+ {f}
23
+ </span>
24
+ )
25
+ }))
26
+ }, [fonts])
27
+
28
+ const handleChange = useCallback((vals) => {
29
+ onChange(vals)
30
+ }, [onChange])
31
+
32
+ return (
33
+ <Select
34
+ mode='tags'
35
+ value={value}
36
+ onChange={handleChange}
37
+ className='width-100'
38
+ placeholder={placeholder || e('selectFontFamily')}
39
+ showSearch
40
+ options={options}
41
+ filterOption={(input, option) =>
42
+ (option?.value ?? '').toLowerCase().includes(input.toLowerCase())}
43
+ />
44
+ )
45
+ }
@@ -35,7 +35,7 @@ import {
35
35
  } from '../../common/constants'
36
36
  import { SplitViewIcon } from '../icons/split-view'
37
37
  import { refs } from '../common/ref'
38
- import safeName from '../../common/safe-name'
38
+ import sanitizeFilename from '../../common/sanitize-filename.js'
39
39
  import { HeartbeatIcon } from '../icons/heartbeat'
40
40
  import './session.styl'
41
41
 
@@ -343,7 +343,7 @@ export default class SessionWrapper extends Component {
343
343
  height
344
344
  } = this.calcTermWidthHeight()
345
345
  const themeConfig = copy(window.store.getThemeConfig())
346
- const logName = safeName(`${tab.title ? tab.title + '_' : ''}${tab.host ? tab.host + '_' : ''}${tab.id}`)
346
+ const logName = sanitizeFilename(`${tab.title ? tab.title + '_' : ''}${tab.host ? tab.host + '_' : ''}${tab.id}`)
347
347
  const pops = {
348
348
  ...this.props,
349
349
  sftpPathFollowSsh,
@@ -28,6 +28,7 @@ import { chooseSaveDirectory } from '../../common/choose-save-folder'
28
28
  import mapper from '../../common/auto-complete-data-mapper'
29
29
  import KeywordForm from './keywords-form'
30
30
  import Link from '../common/external-link'
31
+ import FontSelect from '../common/font-select'
31
32
  import HelpIcon from '../common/help-icon'
32
33
  import KeywordsTransport from './keywords-transport'
33
34
  import fs from '../../common/fs'
@@ -427,36 +428,13 @@ export default class SettingTerminal extends Component {
427
428
  }
428
429
 
429
430
  renderFontFamily = () => {
430
- const { fonts = [] } = window.et
431
431
  const { fontFamily } = this.props.config
432
- const props = {
433
- mode: 'multiple',
434
- onChange: this.handleChangeFont,
435
- value: fontFamily.split(/, */g).filter(d => d.trim()),
436
- style: { width: '100%' }
437
- }
438
432
  return (
439
- <Select
440
- {...props}
441
- showSearch
442
- >
443
- {
444
- fonts.map(f => {
445
- return (
446
- <Option value={f} key={f}>
447
- <span
448
- className='font-option'
449
- style={{
450
- fontFamily: f
451
- }}
452
- >
453
- {f}
454
- </span>
455
- </Option>
456
- )
457
- })
458
- }
459
- </Select>
433
+ <FontSelect
434
+ onChange={this.handleChangeFont}
435
+ value={fontFamily.split(/, */g).filter(d => d.trim())}
436
+ style={{ width: '100%' }}
437
+ />
460
438
  )
461
439
  }
462
440
 
@@ -3,11 +3,11 @@ import {
3
3
  Input,
4
4
  InputNumber,
5
5
  Space,
6
- Select,
7
6
  Button,
8
7
  Modal
9
8
  } from 'antd'
10
9
  import { ColorPicker } from '../bookmark-form/common/color-picker.jsx'
10
+ import FontSelect from '../common/font-select'
11
11
 
12
12
  const { TextArea } = Input
13
13
  const e = window.translate
@@ -24,16 +24,16 @@ export default function TextBgModal ({
24
24
  const [text, setText] = useState(initialText)
25
25
  const [fontSize, setFontSize] = useState(initialSize)
26
26
  const [color, setColor] = useState(initialColor)
27
- const [fontFamily, setFontFamily] = useState(initialFontFamily)
28
-
29
- const { fonts = [] } = window.et || {}
27
+ const [fontFamily, setFontFamily] = useState(
28
+ initialFontFamily.split(/, */g).filter(d => d.trim())
29
+ )
30
30
 
31
31
  const handleOk = () => {
32
32
  onOk({
33
33
  text,
34
34
  fontSize,
35
35
  color,
36
- fontFamily
36
+ fontFamily: fontFamily.join(', ')
37
37
  })
38
38
  }
39
39
 
@@ -43,7 +43,7 @@ export default function TextBgModal ({
43
43
  setText(initialText)
44
44
  setFontSize(initialSize)
45
45
  setColor(initialColor)
46
- setFontFamily(initialFontFamily)
46
+ setFontFamily(initialFontFamily.split(/, */g).filter(d => d.trim()))
47
47
  }
48
48
 
49
49
  const footer = (
@@ -108,30 +108,11 @@ export default function TextBgModal ({
108
108
 
109
109
  <div>
110
110
  <b>{e('fontFamily')}</b>
111
- <Select
111
+ <FontSelect
112
112
  value={fontFamily}
113
113
  onChange={setFontFamily}
114
- style={{ width: '100%' }}
115
114
  placeholder={e('selectFontFamily')}
116
- showSearch
117
- >
118
- {
119
- fonts.map(f => {
120
- return (
121
- <Select.Option value={f} key={f}>
122
- <span
123
- className='font-option'
124
- style={{
125
- fontFamily: f
126
- }}
127
- >
128
- {f}
129
- </span>
130
- </Select.Option>
131
- )
132
- })
133
- }
134
- </Select>
115
+ />
135
116
  </div>
136
117
  </Space>
137
118
  </div>
@@ -309,7 +309,7 @@ export default function SyncForm (props) {
309
309
  )
310
310
  }
311
311
  function createPasswordItem () {
312
- if (syncType === syncTypes.cloud || syncType === syncTypes.webdav) {
312
+ if (syncType === syncTypes.cloud) {
313
313
  return null
314
314
  }
315
315
  return (
@@ -36,6 +36,7 @@ import time from '../../common/time'
36
36
  import { filesize } from 'filesize'
37
37
  import { createTransferProps } from './transfer-common'
38
38
  import generate from '../../common/uid'
39
+ import sanitizeFilename from '../../common/sanitize-filename'
39
40
  import { refsStatic, refs, filesRef } from '../common/ref'
40
41
  import iconsMap from '../sys-menu/icons-map'
41
42
 
@@ -174,7 +175,7 @@ export default class FileSection extends React.Component {
174
175
  ? item.replace(/^remote:/, '')
175
176
  : item
176
177
  const { name } = getFolderFromFilePath(fromPath, isRemote)
177
- const toPath = resolve(path, name)
178
+ const toPath = resolve(path, sanitizeFilename(name))
178
179
  res.push({
179
180
  typeFrom: isRemote ? typeMap.remote : typeMap.local,
180
181
  typeTo: type,
@@ -270,7 +271,7 @@ export default class FileSection extends React.Component {
270
271
  if (!toFile.id || !toFile.isDirectory) {
271
272
  toFile = {
272
273
  type,
273
- ...getFolderFromFilePath(this.props[type + 'Path']),
274
+ ...getFolderFromFilePath(this.props[type + 'Path'], type === typeMap.remote),
274
275
  isDirectory: false
275
276
  }
276
277
  }
@@ -326,7 +327,7 @@ export default class FileSection extends React.Component {
326
327
  toFile = {
327
328
  ...toFile,
328
329
  ...getFolderFromFilePath(
329
- resolve(toFile.path, toFile.name)
330
+ resolve(toFile.path, sanitizeFilename(toFile.name))
330
331
  ),
331
332
  id: undefined
332
333
  }
@@ -367,7 +368,7 @@ export default class FileSection extends React.Component {
367
368
  return this.doTransferSelected(
368
369
  null,
369
370
  files,
370
- resolve(toFile.path, toFile.name),
371
+ resolve(toFile.path, sanitizeFilename(toFile.name)),
371
372
  toFile.type,
372
373
  operation
373
374
  )
@@ -784,7 +785,7 @@ export default class FileSection extends React.Component {
784
785
  if (toPathBase) {
785
786
  toPath = toPathBase
786
787
  }
787
- toPath = resolve(toPath, name)
788
+ toPath = resolve(toPath, sanitizeFilename(name))
788
789
  const obj = {
789
790
  host: this.props.tab?.host,
790
791
  tabType: this.props.tab?.type,
@@ -54,6 +54,9 @@ export function handleTerminalSelectionReplace (event, ctx) {
54
54
  const isPrintable = key && key.length === 1
55
55
  if (!isBackspace && !isDelete && !isPrintable) return false
56
56
 
57
+ const info = getSelectionReplaceInfo(ctx.term)
58
+ if (!info) return false
59
+
57
60
  if (event && event.preventDefault) {
58
61
  event.preventDefault()
59
62
  }
@@ -61,9 +64,6 @@ export function handleTerminalSelectionReplace (event, ctx) {
61
64
  event.stopPropagation()
62
65
  }
63
66
 
64
- const info = getSelectionReplaceInfo(ctx.term)
65
- if (!info) return false
66
-
67
67
  const { startX, endX, cursorX } = info
68
68
  const move = startX - cursorX
69
69
  if (move > 0) {
@@ -146,6 +146,12 @@ export function shortcutExtend (Cls) {
146
146
  if (isInAntdInput()) {
147
147
  return
148
148
  }
149
+ // During IME composition, let xterm handle the event through its
150
+ // CompositionHelper. Intercepting here breaks composition (e.g. first
151
+ // char lost, closing bracket duplicated when typing Chinese inside brackets).
152
+ if (event.isComposing) {
153
+ return
154
+ }
149
155
  if (handleTerminalSelectionReplace(event, this)) {
150
156
  return false
151
157
  }
@@ -156,12 +162,6 @@ export function shortcutExtend (Cls) {
156
162
  !altKey &&
157
163
  !ctrlKey
158
164
  ) {
159
- // If IME is composing, let the browser delete the composition char only
160
- // Returning false tells xterm not to process the event (and not to call
161
- // preventDefault), so the native textarea backspace still works for IME.
162
- if (event.isComposing) {
163
- return false
164
- }
165
165
  this.props.onDelKeyPressed()
166
166
  const delKey = this.props.config.backspaceMode === '^?' ? 8 : 127
167
167
  const altDelDelKey = delKey === 8 ? 127 : 8
@@ -1263,6 +1263,17 @@ class Term extends Component {
1263
1263
  this.handleError({ message: text, from, srcId })
1264
1264
  }
1265
1265
  })
1266
+ // Guard: component was unmounted while createTerm was pending.
1267
+ // The child process is already running; connect briefly to trigger its cleanup.
1268
+ if (this.onClose) {
1269
+ if (r && r.port) {
1270
+ try {
1271
+ const tmpSock = new WebSocket(this.buildWsUrl(r.port))
1272
+ tmpSock.onopen = () => tmpSock.close()
1273
+ } catch (_e) {}
1274
+ }
1275
+ return
1276
+ }
1266
1277
  if (typeof r === 'string' && r.includes('fail')) {
1267
1278
  return this.promote()
1268
1279
  }
@@ -172,8 +172,28 @@ export default Store => {
172
172
 
173
173
  // ==================== Bookmark APIs ====================
174
174
 
175
+ const bookmarkSensitiveFields = [
176
+ 'password', 'privateKey', 'passphrase', 'certificate', 'proxy',
177
+ 'connectionHoppings', 'sshTunnels'
178
+ ]
179
+ const bookmarkFeatureFields = [
180
+ 'connectionHoppings', 'sshTunnels', 'quickCommands', 'runScripts'
181
+ ]
182
+
183
+ function sanitizeBookmark (b) {
184
+ const safe = Object.fromEntries(
185
+ Object.entries(b).filter(([k]) => !bookmarkSensitiveFields.includes(k))
186
+ )
187
+ for (const key of bookmarkFeatureFields) {
188
+ if (Array.isArray(b[key]) && b[key].length) {
189
+ safe[`has${key.charAt(0).toUpperCase() + key.slice(1)}`] = true
190
+ }
191
+ }
192
+ return safe
193
+ }
194
+
175
195
  Store.prototype.mcpListBookmarks = function () {
176
- return deepCopy(window.store.bookmarks)
196
+ return deepCopy(window.store.bookmarks).map(sanitizeBookmark)
177
197
  }
178
198
 
179
199
  Store.prototype.mcpGetBookmark = function (args) {
@@ -182,7 +202,7 @@ export default Store => {
182
202
  if (!bookmark) {
183
203
  throw new Error(`Bookmark not found: ${args.id}`)
184
204
  }
185
- return deepCopy(bookmark)
205
+ return deepCopy(sanitizeBookmark(bookmark))
186
206
  }
187
207
 
188
208
  Store.prototype.mcpAddBookmark = async function (args) {
@@ -21,49 +21,51 @@ import dataCompare from '../common/data-compare'
21
21
 
22
22
  export default store => {
23
23
  for (const name of dbNamesForWatch) {
24
+ window[`watch${name}Running`] = false
24
25
  window[`watch${name}`] = autoRun(async () => {
25
- if (window.migrating) {
26
+ if (window.migrating || window[`watch${name}Running`]) {
26
27
  return
27
28
  }
28
- const old = refsStatic.get('oldState-' + name)
29
- const n = store.getItems(name)
30
- const { updated, added, removed } = dataCompare(
31
- old,
32
- n
33
- )
34
- for (const item of removed) {
35
- await remove(name, item.id)
36
- }
37
- for (const item of updated) {
38
- await update(item.id, item, name, false)
39
- }
40
- for (const item of added) {
41
- await insert(name, item)
42
- }
43
- const newOrder = (n || []).map(d => d.id)
44
- await update(
45
- `${name}:order`,
46
- newOrder
47
- )
48
- refsStatic.add('oldState-' + name, deepCopy(n) || [])
49
- if (name === 'bookmarks') {
50
- store.bookmarksMap = new Map(
51
- n.map(d => [d.id, d])
29
+ window[`watch${name}Running`] = true
30
+ try {
31
+ const old = refsStatic.get('oldState-' + name)
32
+ const n = store.getItems(name)
33
+ const { updated, added, removed } = dataCompare(
34
+ old,
35
+ n
52
36
  )
53
- }
54
- await store.updateLastDataUpdateTime()
55
- if (dbNamesForSync.includes(name)) {
56
- const syncSetting = store.config.syncSetting || {}
57
- const { autoSync, autoSyncInterval, autoSyncDirection } = syncSetting
58
- if (autoSync && autoSyncInterval === 0) {
59
- if (autoSyncDirection === 'download') {
60
- await store.downloadSettingAll()
61
- } else {
62
- await store.uploadSettingAll()
37
+ await Promise.all([
38
+ ...removed.map(item => remove(name, item.id)),
39
+ ...updated.map(item => update(item.id, item, name, false)),
40
+ added.length ? insert(name, added) : Promise.resolve()
41
+ ])
42
+ const newOrder = (n || []).map(d => d.id)
43
+ await update(
44
+ `${name}:order`,
45
+ newOrder
46
+ )
47
+ refsStatic.add('oldState-' + name, deepCopy(n) || [])
48
+ if (name === 'bookmarks') {
49
+ store.bookmarksMap = new Map(
50
+ n.map(d => [d.id, d])
51
+ )
52
+ }
53
+ await store.updateLastDataUpdateTime()
54
+ if (dbNamesForSync.includes(name)) {
55
+ const syncSetting = store.config.syncSetting || {}
56
+ const { autoSync, autoSyncInterval, autoSyncDirection } = syncSetting
57
+ if (autoSync && autoSyncInterval === 0) {
58
+ if (autoSyncDirection === 'download') {
59
+ await store.downloadSettingAll()
60
+ } else {
61
+ await store.uploadSettingAll()
62
+ }
63
63
  }
64
64
  }
65
+ return store[name]
66
+ } finally {
67
+ window[`watch${name}Running`] = false
65
68
  }
66
- return store[name]
67
69
  })
68
70
  window[`watch${name}`].start()
69
71
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "3.11.0",
3
+ "version": "3.11.12",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",
@@ -1,19 +0,0 @@
1
- /**
2
- * convert string to safe name
3
- * from https://github.com/Jaliborc/safe-filename/blob/master/index.js
4
- */
5
-
6
- export default (name) => {
7
- return name
8
- .replace(/\.$/, '')
9
- .replace('?', '❓')
10
- .replace('\\', ' ⃥')
11
- .replace('/', '⟋')
12
- .replace('|', '│')
13
- .replace(':', '꞉')
14
- .replace('<', 'ᐸ')
15
- .replace('>', 'ᐳ')
16
- .replace('>', 'ᐳ')
17
- .replace('"', 'ᐦ')
18
- .replace('*', '꘎')
19
- }