mbeditor 0.13.0 → 0.13.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +66 -1
- data/app/assets/javascripts/mbeditor/application.js +0 -1
- data/app/assets/javascripts/mbeditor/collaboration_service.js +22 -2
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +11 -87
- data/app/assets/javascripts/mbeditor/components/ImportDialog.js +9 -1
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +171 -205
- data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +35 -7
- data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +97 -75
- data/app/assets/javascripts/mbeditor/editor_plugins.js +7 -1
- data/app/assets/javascripts/mbeditor/file_service.js +8 -18
- data/app/assets/javascripts/mbeditor/search_service.js +20 -2
- data/app/assets/javascripts/mbeditor/tab_manager.js +7 -4
- data/app/assets/stylesheets/mbeditor/editor.css +138 -67
- data/app/channels/mbeditor/collaboration_channel.rb +8 -2
- data/app/controllers/mbeditor/editors_controller.rb +8 -26
- data/app/services/mbeditor/collaboration_doc_store.rb +42 -3
- data/app/services/mbeditor/duplicate_content_scanner.rb +105 -0
- data/app/services/mbeditor/js_globals_service.rb +12 -1
- data/app/services/mbeditor/rubocop_run_service.rb +17 -5
- data/app/services/mbeditor/schema_service.rb +8 -2
- data/app/services/mbeditor/search_replace_service.rb +11 -2
- data/app/services/mbeditor/test_runner_service.rb +3 -56
- data/lib/mbeditor/configuration.rb +0 -8
- data/lib/mbeditor/route_map.rb +0 -1
- data/lib/mbeditor/version.rb +1 -1
- data/lib/tasks/mbeditor.rake +23 -0
- metadata +4 -3
- data/app/assets/javascripts/mbeditor/components/TestRunPanel.js +0 -312
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fe5dce956e01ff66387b9ccf99fc8b163ce131a226158eda7b6cf2c8513eaa09
|
|
4
|
+
data.tar.gz: 37e4f12f4ccd1ca7d209e7fe134a25d813bbe175862d53a470a5042aff9d7797
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 60c74bb23b92719d4273d720af42813e2ecd3d639281901d47037e332697819cbe58c338eea9aeff334f3533d48d2e62c994b2122fb36fbded4bef94bd225b36
|
|
7
|
+
data.tar.gz: 1451f3b2effb5d9cffd9811b41ea72b07d174702af9e6dc9993d5fbc92d990109dbf834e957c1eced22e8d77b815173d14bf6465b51072002a0e1c482745dc69
|
data/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,72 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [0.13.1] - 2026-08-26
|
|
9
|
+
|
|
10
|
+
### Removed
|
|
11
|
+
- **The whole-suite test run is gone** — the status-bar "Tests" button, the
|
|
12
|
+
Test Run drawer, `POST /test_all`, and the `test_all_command` /
|
|
13
|
+
`test_all_timeout` settings. It was one blocking request holding a
|
|
14
|
+
development-server thread for the length of the suite, with no way to cancel
|
|
15
|
+
it and the runner's output buffered whole in the server process; on a real
|
|
16
|
+
host app that is twenty minutes of a frozen-looking button for a result the
|
|
17
|
+
terminal gives you faster and interruptibly. The per-file (and per-line) test
|
|
18
|
+
run stays — that one is scoped, finishes in seconds and is why the editor
|
|
19
|
+
runs tests at all. **A host app setting `test_all_command` or
|
|
20
|
+
`test_all_timeout` in its initializer must drop those lines.**
|
|
21
|
+
- **The ruby-lsp status-bar chip is gone**, along with its 10-second health
|
|
22
|
+
poll. ruby-lsp itself is untouched — definitions, hover, completion,
|
|
23
|
+
diagnostics and the rest all still run through it.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- `rake mbeditor:scan_duplicates` — scans the workspace for files that contain a
|
|
27
|
+
duplicated copy of themselves, for finding files damaged before the seeding fix below.
|
|
28
|
+
Set `MBEDITOR_WORKSPACE_ROOT` to scan a tree other than the resolved workspace.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- **Data loss: a saved file could contain two copies of itself.** When two editor
|
|
32
|
+
clients attached to a collaborative room the server had no state for — a fresh
|
|
33
|
+
room, or one evicted while both sat idle — each one seeded the shared document
|
|
34
|
+
from disk, and Yjs merged both inserts at offset 0 into two concatenated copies.
|
|
35
|
+
Saving then wrote the doubled buffer. The server now grants the right to seed a
|
|
36
|
+
room to exactly one client (`CollaborationDocStore.claim_seed`), never evicts a
|
|
37
|
+
room that still has subscribers, and a client whose cable reconnects re-publishes
|
|
38
|
+
its snapshot so a server that lost its state cannot hand out a second seed.
|
|
39
|
+
- **Status bar alignment.** Every item now sits in the same 8px cell instead of
|
|
40
|
+
carrying its own padding on top of the bar's (the branch, the offline chip
|
|
41
|
+
and the version each had a different one), the branch drops from 13px to the
|
|
42
|
+
bar's 11px, and `line-height: 1` on the bar *and* on its buttons puts an icon
|
|
43
|
+
and the number beside it in identical line boxes — Pico gives `<button>` its
|
|
44
|
+
own 1.5, so the problem counts sat in a 16.5px box next to an 11px icon.
|
|
45
|
+
- **The Problems panel's Fix button did nothing visible.** `rubocop -a` writes
|
|
46
|
+
its corrections to disk, but reports the offenses it just fixed alongside the
|
|
47
|
+
ones it left — each flagged both `corrected` and `correctable` — so the list
|
|
48
|
+
after a fix was byte-identical to the list before it and the button stayed
|
|
49
|
+
lit. Corrected offenses are now dropped from the response, open buffers are
|
|
50
|
+
re-read after the run, and clearing stale diagnostics now clears the React
|
|
51
|
+
marker map as well as Monaco's, which is what had left the panel, the tab
|
|
52
|
+
badges and the status-bar tallies disagreeing.
|
|
53
|
+
- **Quick Open matches loosely again.** MiniSearch was configured `fuzzy: false`,
|
|
54
|
+
so a single typo found nothing; fuzziness is now scaled per term. Folders sort
|
|
55
|
+
after files instead of ahead of every match, and the dialog and its text are
|
|
56
|
+
larger.
|
|
57
|
+
- **Search results read like VS Code's.** Hits group under a collapsible row per
|
|
58
|
+
file with a match count, the matched text is tinted with the accent colour and
|
|
59
|
+
is kept on screen at any panel width (eliding left and right around it), and
|
|
60
|
+
clicking a hit selects the match and leaves the cursor at its end.
|
|
61
|
+
- **Bulk import no longer opens on the previous import's folder** — the folder
|
|
62
|
+
you actually acted on is offered first — and it now reports what it wrote and
|
|
63
|
+
reveals the destination in the tree.
|
|
64
|
+
- `ReactRailsUJS` no longer reports as undefined; it ships inside the react-rails
|
|
65
|
+
gem, outside the workspace, so no static scan of the tree could ever find it.
|
|
66
|
+
- `<<~JSX` heredocs highlight as JavaScript (as do `<<~TSX`/`<<~TS`).
|
|
67
|
+
- The Open Editors list no longer runs its filenames underneath the split and
|
|
68
|
+
close buttons.
|
|
69
|
+
|
|
70
|
+
### Changed
|
|
71
|
+
- **The activity bar is a third narrower** (60px to 40px), and the editor
|
|
72
|
+
toolbar's per-file Test button is gone; running the test under the cursor is
|
|
73
|
+
unaffected.
|
|
9
74
|
|
|
10
75
|
## [0.13.0] - 2026-08-20
|
|
11
76
|
|
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
//= require mbeditor/components/TestResultsPanel
|
|
28
28
|
//= require mbeditor/components/LogPanel
|
|
29
29
|
//= require mbeditor/components/ProblemsPanel
|
|
30
|
-
//= require mbeditor/components/TestRunPanel
|
|
31
30
|
//= require mbeditor/components/CodeReviewPanel
|
|
32
31
|
//= require mbeditor/components/EditorPanel
|
|
33
32
|
//= require mbeditor/components/FileTree
|
|
@@ -225,6 +225,9 @@ var CollaborationService = (function () {
|
|
|
225
225
|
binding: null,
|
|
226
226
|
undoManager: null,
|
|
227
227
|
lateJoin: false,
|
|
228
|
+
// Set from the sync handshake: the server grants the right to seed an empty
|
|
229
|
+
// room's shared doc from disk to exactly one client (see claim_seed).
|
|
230
|
+
seedGranted: false,
|
|
228
231
|
onSeeded: null,
|
|
229
232
|
attachRequested: false,
|
|
230
233
|
degraded: false,
|
|
@@ -280,10 +283,14 @@ var CollaborationService = (function () {
|
|
|
280
283
|
if (d) window.Y.applyUpdate(room.doc, _b64ToU8(d), REMOTE);
|
|
281
284
|
});
|
|
282
285
|
}
|
|
286
|
+
room.seedGranted = !!data.seed;
|
|
283
287
|
room.synced = true;
|
|
284
288
|
_maybeAttach(room);
|
|
285
289
|
} else if (data.type === 'doc_update') {
|
|
286
290
|
if (data.update) window.Y.applyUpdate(room.doc, _b64ToU8(data.update), REMOTE);
|
|
291
|
+
// A client that deferred attaching (empty room, seed granted to someone
|
|
292
|
+
// else) is waiting for exactly this: the seeder's content has landed.
|
|
293
|
+
_maybeAttach(room);
|
|
287
294
|
} else if (data.type === 'awareness') {
|
|
288
295
|
if (data.awareness && room.awareness && window.awarenessProtocol) {
|
|
289
296
|
// REMOTE origin keeps the outbound handler from echoing peer state back.
|
|
@@ -312,7 +319,13 @@ var CollaborationService = (function () {
|
|
|
312
319
|
room.subscription = WebSocketService.subscribeCollaboration(path, {
|
|
313
320
|
// Awareness is never server-persisted, so peers lost our caret with the
|
|
314
321
|
// connection and would not see it again until we happened to move.
|
|
315
|
-
|
|
322
|
+
// The server may have lost the room entirely (restart): re-publish our
|
|
323
|
+
// content so the next opener late-joins instead of being granted a seed
|
|
324
|
+
// that would merge a second copy into what we still hold.
|
|
325
|
+
connected: function () {
|
|
326
|
+
_sendLocalAwareness(room);
|
|
327
|
+
if (room.binding) pushSnapshot(path);
|
|
328
|
+
},
|
|
316
329
|
received: function (data) { _onMessage(room, data); }
|
|
317
330
|
});
|
|
318
331
|
});
|
|
@@ -346,6 +359,11 @@ var CollaborationService = (function () {
|
|
|
346
359
|
|
|
347
360
|
function _maybeAttach(room) {
|
|
348
361
|
if (room.binding || !room.synced || !room.attachRequested || !room.model) return;
|
|
362
|
+
// Shared doc still empty and the seed was granted to another client: its
|
|
363
|
+
// content is on its way. Binding now would hand the model to an empty Y.Text
|
|
364
|
+
// and wipe the buffer, so wait for the first doc_update (or the fallback
|
|
365
|
+
// timer, which lets the natively-loaded content stand).
|
|
366
|
+
if (room.text.length === 0 && !room.seedGranted && room.model.getValue().length > 0) return;
|
|
349
367
|
_doAttach(room);
|
|
350
368
|
}
|
|
351
369
|
|
|
@@ -353,10 +371,12 @@ var CollaborationService = (function () {
|
|
|
353
371
|
var model = room.model;
|
|
354
372
|
room.lateJoin = room.text.length > 0; // server already had shared state
|
|
355
373
|
|
|
356
|
-
if (!room.lateJoin && model.getValue().length > 0) {
|
|
374
|
+
if (!room.lateJoin && room.seedGranted && model.getValue().length > 0) {
|
|
357
375
|
// First opener whose disk content is already in the model: seed the shared
|
|
358
376
|
// doc from it before constructing the binding, otherwise the binding would
|
|
359
377
|
// overwrite the model to match the (empty) Y.Text and wipe the content.
|
|
378
|
+
// Only ever the client the SERVER picked — two clients each seeding an
|
|
379
|
+
// empty room merge into two concatenated copies of the file.
|
|
360
380
|
room.doc.transact(function () { room.text.insert(0, model.getValue()); });
|
|
361
381
|
}
|
|
362
382
|
|
|
@@ -24,16 +24,13 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
24
24
|
var onContentChange = _ref.onContentChange;
|
|
25
25
|
var markers = _ref.markers;
|
|
26
26
|
var gitAvailable = _ref.gitAvailable === true;
|
|
27
|
-
var testAvailable = _ref.testAvailable === true;
|
|
28
27
|
var onFormat = _ref.onFormat;
|
|
29
28
|
var onSave = _ref.onSave;
|
|
30
|
-
var onRunTest = _ref.onRunTest;
|
|
31
29
|
var onRunTestAtCursor = _ref.onRunTestAtCursor;
|
|
32
30
|
var onShowHistory = _ref.onShowHistory;
|
|
33
31
|
var treeData = _ref.treeData || [];
|
|
34
32
|
var testResult = _ref.testResult;
|
|
35
33
|
var testPanelFile = _ref.testPanelFile;
|
|
36
|
-
var testLoading = _ref.testLoading;
|
|
37
34
|
var testInlineVisible = _ref.testInlineVisible;
|
|
38
35
|
var editorPrefs = _ref.editorPrefs || {};
|
|
39
36
|
var monacoReady = _ref.monacoReady !== false; // undefined means Monaco already loaded (legacy callers)
|
|
@@ -1323,9 +1320,18 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1323
1320
|
var column = tab.gotoCol || 1;
|
|
1324
1321
|
var maxColumn = model.getLineMaxColumn(line);
|
|
1325
1322
|
if (column > maxColumn) column = maxColumn;
|
|
1323
|
+
// With both bounds the match itself is selected and the cursor ends up
|
|
1324
|
+
// just past it — where you'd start typing. With only gotoCol the
|
|
1325
|
+
// selection is empty, which is exactly setPosition.
|
|
1326
|
+
var endColumn = Math.min(tab.gotoEndCol || column, maxColumn);
|
|
1326
1327
|
|
|
1327
1328
|
editor.revealLineInCenter(line);
|
|
1328
|
-
|
|
1329
|
+
// setSelection(IRange) parks the cursor at the range's end, so no
|
|
1330
|
+
// setPosition afterwards — that would collapse the selection again.
|
|
1331
|
+
editor.setSelection({
|
|
1332
|
+
startLineNumber: line, startColumn: column,
|
|
1333
|
+
endLineNumber: line, endColumn: endColumn
|
|
1334
|
+
});
|
|
1329
1335
|
editor.focus();
|
|
1330
1336
|
|
|
1331
1337
|
TabManager.saveTabViewState(tab.id, editor.saveViewState());
|
|
@@ -1333,7 +1339,7 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1333
1339
|
}, 50);
|
|
1334
1340
|
|
|
1335
1341
|
return function () { clearTimeout(timer); };
|
|
1336
|
-
}, [tab.gotoLine, tab.gotoCol, tab.content, tab.loading]);
|
|
1342
|
+
}, [tab.gotoLine, tab.gotoCol, tab.gotoEndCol, tab.content, tab.loading]);
|
|
1337
1343
|
|
|
1338
1344
|
// Apply RuboCop markers
|
|
1339
1345
|
useEffect(function () {
|
|
@@ -1803,74 +1809,6 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1803
1809
|
return src === derived;
|
|
1804
1810
|
};
|
|
1805
1811
|
|
|
1806
|
-
var testFileCandidates = function(relativePath) {
|
|
1807
|
-
if (!relativePath || !relativePath.endsWith('.rb')) return [];
|
|
1808
|
-
|
|
1809
|
-
var basename = relativePath.slice(0, -3);
|
|
1810
|
-
var dirParts = relativePath.split('/');
|
|
1811
|
-
var leafName = basename.split('/').pop();
|
|
1812
|
-
var candidates = [];
|
|
1813
|
-
|
|
1814
|
-
if (dirParts[0] === 'app' && dirParts.length > 1) {
|
|
1815
|
-
var subPath = dirParts.slice(1).join('/');
|
|
1816
|
-
var subDir = subPath.indexOf('/') !== -1 ? subPath.slice(0, subPath.lastIndexOf('/')) : '';
|
|
1817
|
-
candidates.push('test/' + (subDir ? subDir + '/' : '') + leafName + '_test.rb');
|
|
1818
|
-
candidates.push('spec/' + (subDir ? subDir + '/' : '') + leafName + '_spec.rb');
|
|
1819
|
-
}
|
|
1820
|
-
|
|
1821
|
-
if (dirParts[0] === 'lib') {
|
|
1822
|
-
var libSubPath = dirParts.slice(1).join('/');
|
|
1823
|
-
var libSubDir = libSubPath.indexOf('/') !== -1 ? libSubPath.slice(0, libSubPath.lastIndexOf('/')) : '';
|
|
1824
|
-
candidates.push('test/lib/' + (libSubDir ? libSubDir + '/' : '') + leafName + '_test.rb');
|
|
1825
|
-
candidates.push('test/' + (libSubDir ? libSubDir + '/' : '') + leafName + '_test.rb');
|
|
1826
|
-
candidates.push('spec/lib/' + (libSubDir ? libSubDir + '/' : '') + leafName + '_spec.rb');
|
|
1827
|
-
}
|
|
1828
|
-
|
|
1829
|
-
candidates.push('test/' + leafName + '_test.rb');
|
|
1830
|
-
candidates.push('spec/' + leafName + '_spec.rb');
|
|
1831
|
-
|
|
1832
|
-
return candidates.filter(function(candidate, index, list) {
|
|
1833
|
-
return list.indexOf(candidate) === index;
|
|
1834
|
-
});
|
|
1835
|
-
};
|
|
1836
|
-
|
|
1837
|
-
var treeHasPath = function(nodes, targetPath) {
|
|
1838
|
-
if (!targetPath) return false;
|
|
1839
|
-
var stack = (nodes || []).slice();
|
|
1840
|
-
|
|
1841
|
-
while (stack.length) {
|
|
1842
|
-
var node = stack.pop();
|
|
1843
|
-
if (!node) continue;
|
|
1844
|
-
if (node.path === targetPath) return true;
|
|
1845
|
-
if (node.children && node.children.length) {
|
|
1846
|
-
stack.push.apply(stack, node.children);
|
|
1847
|
-
}
|
|
1848
|
-
}
|
|
1849
|
-
|
|
1850
|
-
return false;
|
|
1851
|
-
};
|
|
1852
|
-
|
|
1853
|
-
var matchingTestFilePath = function(sourcePath) {
|
|
1854
|
-
var normalized = (sourcePath || '').replace(/^\/+/, '');
|
|
1855
|
-
if (!normalized || !normalized.endsWith('.rb')) return null;
|
|
1856
|
-
if (/^(test|spec)\//.test(normalized) && /_(test|spec)\.rb$/.test(normalized)) return normalized;
|
|
1857
|
-
|
|
1858
|
-
var candidates = testFileCandidates(normalized);
|
|
1859
|
-
for (var i = 0; i < candidates.length; i += 1) {
|
|
1860
|
-
if (treeHasPath(treeData, candidates[i])) return candidates[i];
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
return null;
|
|
1864
|
-
};
|
|
1865
|
-
|
|
1866
|
-
// treeHasPath walks the whole file tree once per candidate, and the Test
|
|
1867
|
-
// button asked for this on every render — so once per keystroke. treeData is
|
|
1868
|
-
// deliberately outside the React.memo comparator below, so it can only change
|
|
1869
|
-
// on a render this component was going to do anyway.
|
|
1870
|
-
var testFileForTab = useMemo(function () {
|
|
1871
|
-
return matchingTestFilePath(tab.path);
|
|
1872
|
-
}, [tab.path, treeData]);
|
|
1873
|
-
|
|
1874
1812
|
// Map a test method name to the best-matching line in the source file.
|
|
1875
1813
|
// Extracts keywords from the test name and scores each source line.
|
|
1876
1814
|
var mapTestToSourceLine = function(testName, sourceContent) {
|
|
@@ -2330,18 +2268,6 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
2330
2268
|
React.createElement('i', { className: 'fas fa-shoe-prints', style: { marginRight: editorPrefs.toolbarIconOnly ? 0 : '5px', flexShrink: 0 } }),
|
|
2331
2269
|
!editorPrefs.toolbarIconOnly && React.createElement('span', { className: 'ide-toolbar-label' }, isBlameLoading ? 'Loading...' : 'Blame')
|
|
2332
2270
|
),
|
|
2333
|
-
testAvailable && testFileForTab && React.createElement(
|
|
2334
|
-
'button',
|
|
2335
|
-
{
|
|
2336
|
-
className: 'ide-icon-btn',
|
|
2337
|
-
onClick: function() { if (onRunTest) onRunTest(); },
|
|
2338
|
-
disabled: testLoading,
|
|
2339
|
-
'aria-busy': !!testLoading,
|
|
2340
|
-
title: 'Run Tests'
|
|
2341
|
-
},
|
|
2342
|
-
!testLoading && React.createElement('i', { className: 'fas fa-flask', style: { marginRight: editorPrefs.toolbarIconOnly ? 0 : '5px', flexShrink: 0 } }),
|
|
2343
|
-
!editorPrefs.toolbarIconOnly && !testLoading && React.createElement('span', { className: 'ide-toolbar-label' }, 'Test')
|
|
2344
|
-
)
|
|
2345
2271
|
),
|
|
2346
2272
|
conflictCount > 0 && React.createElement(
|
|
2347
2273
|
'div', { className: 'mb-conflict-banner' },
|
|
@@ -2612,10 +2538,8 @@ window.EditorPanel = React.memo(EditorPanel, function(prev, next) {
|
|
|
2612
2538
|
prev.paneId === next.paneId &&
|
|
2613
2539
|
(prev.markers === next.markers || (prev.markers.length === 0 && next.markers.length === 0)) &&
|
|
2614
2540
|
prev.gitAvailable === next.gitAvailable &&
|
|
2615
|
-
prev.testAvailable === next.testAvailable &&
|
|
2616
2541
|
prev.testResult === next.testResult &&
|
|
2617
2542
|
prev.testPanelFile === next.testPanelFile &&
|
|
2618
|
-
prev.testLoading === next.testLoading &&
|
|
2619
2543
|
prev.testInlineVisible === next.testInlineVisible &&
|
|
2620
2544
|
prev.editorPrefs === next.editorPrefs &&
|
|
2621
2545
|
prev.monacoReady === next.monacoReady;
|
|
@@ -88,7 +88,15 @@ var ImportDialog = function ImportDialog(_ref) {
|
|
|
88
88
|
options.push(destOption(prefix, label, hint));
|
|
89
89
|
};
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
// The folder this upload was started from is always first and always the
|
|
92
|
+
// active one. Offering it after the suggestions meant that once you had
|
|
93
|
+
// imported a folder anywhere, that same folder scored top of the suggestion
|
|
94
|
+
// list next time and sat at the head of the options — so the dialog read as
|
|
95
|
+
// "it remembered where I last put things". With nothing selected it was
|
|
96
|
+
// worse: the previous destination was the first option and the actually
|
|
97
|
+
// chosen one ("Workspace root") was last.
|
|
98
|
+
offer(initialFolder, initialFolder || 'Workspace root',
|
|
99
|
+
initialFolder ? 'selected folder' : 'create the folders from the root');
|
|
92
100
|
suggestions.forEach(function (s) {
|
|
93
101
|
var bits = [];
|
|
94
102
|
if (s.files) bits.push(s.files + ' matching file' + (s.files === 1 ? '' : 's'));
|