mbeditor 0.12.8 → 0.13.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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +207 -0
  3. data/app/assets/javascripts/mbeditor/application.js +7 -1
  4. data/app/assets/javascripts/mbeditor/collaboration_service.js +31 -0
  5. data/app/assets/javascripts/mbeditor/color_provider.js +5 -0
  6. data/app/assets/javascripts/mbeditor/components/CollapsibleSection.js +3 -1
  7. data/app/assets/javascripts/mbeditor/components/EditorPanel.js +135 -40
  8. data/app/assets/javascripts/mbeditor/components/FileTree.js +6 -1
  9. data/app/assets/javascripts/mbeditor/components/GitPanel.js +10 -1
  10. data/app/assets/javascripts/mbeditor/components/ImportConflictModal.js +15 -8
  11. data/app/assets/javascripts/mbeditor/components/ImportDialog.js +216 -0
  12. data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +629 -137
  13. data/app/assets/javascripts/mbeditor/components/ModelGraph.js +25 -6
  14. data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +218 -13
  15. data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +39 -33
  16. data/app/assets/javascripts/mbeditor/components/TabBar.js +11 -2
  17. data/app/assets/javascripts/mbeditor/components/TestRunPanel.js +312 -0
  18. data/app/assets/javascripts/mbeditor/editor_plugins.js +444 -116
  19. data/app/assets/javascripts/mbeditor/file_import.js +78 -0
  20. data/app/assets/javascripts/mbeditor/file_service.js +122 -18
  21. data/app/assets/javascripts/mbeditor/git_service.js +130 -8
  22. data/app/assets/javascripts/mbeditor/history_service.js +33 -38
  23. data/app/assets/javascripts/mbeditor/log_service.js +4 -0
  24. data/app/assets/javascripts/mbeditor/search_service.js +30 -2
  25. data/app/assets/javascripts/mbeditor/tab_manager.js +59 -6
  26. data/app/assets/javascripts/mbeditor/websocket_service.js +88 -4
  27. data/app/assets/stylesheets/mbeditor/editor.css +302 -55
  28. data/app/channels/mbeditor/channel_authentication.rb +7 -0
  29. data/app/channels/mbeditor/editor_channel.rb +6 -3
  30. data/app/controllers/mbeditor/application_controller.rb +5 -0
  31. data/app/controllers/mbeditor/editors_controller.rb +173 -29
  32. data/app/controllers/mbeditor/git_controller.rb +3 -3
  33. data/app/controllers/mbeditor/logs_controller.rb +3 -1
  34. data/app/services/mbeditor/collaboration_doc_store.rb +7 -0
  35. data/app/services/mbeditor/editor_state_service.rb +36 -26
  36. data/app/services/mbeditor/exclusion_matcher.rb +12 -10
  37. data/app/services/mbeditor/file_operation_service.rb +38 -3
  38. data/app/services/mbeditor/file_tree_service.rb +30 -2
  39. data/app/services/mbeditor/git_combined_diff_service.rb +13 -3
  40. data/app/services/mbeditor/git_commit_detail_service.rb +20 -16
  41. data/app/services/mbeditor/git_diff_service.rb +5 -1
  42. data/app/services/mbeditor/git_info_service.rb +20 -8
  43. data/app/services/mbeditor/git_line_diff_service.rb +6 -2
  44. data/app/services/mbeditor/git_service.rb +65 -15
  45. data/app/services/mbeditor/js_definition_service.rb +3 -1
  46. data/app/services/mbeditor/js_globals_service.rb +7 -4
  47. data/app/services/mbeditor/js_members_service.rb +3 -2
  48. data/app/services/mbeditor/js_program_service.rb +15 -5
  49. data/app/services/mbeditor/js_syntax_check_service.rb +15 -4
  50. data/app/services/mbeditor/process_runner.rb +42 -12
  51. data/app/services/mbeditor/ri_definition_service.rb +8 -1
  52. data/app/services/mbeditor/route_service.rb +45 -8
  53. data/app/services/mbeditor/rubocop_run_service.rb +86 -0
  54. data/app/services/mbeditor/ruby_definition_service.rb +23 -4
  55. data/app/services/mbeditor/schema_service.rb +65 -64
  56. data/app/services/mbeditor/search_replace_service.rb +75 -10
  57. data/app/services/mbeditor/test_runner_service.rb +98 -10
  58. data/lib/mbeditor/cable_log_filter.rb +8 -2
  59. data/lib/mbeditor/configuration.rb +12 -1
  60. data/lib/mbeditor/editor_bootstrap.rb +18 -12
  61. data/lib/mbeditor/engine.rb +22 -3
  62. data/lib/mbeditor/exception_log.rb +2 -2
  63. data/lib/mbeditor/pending_migrations.rb +33 -0
  64. data/lib/mbeditor/rack/handle_pending_migrations.rb +25 -15
  65. data/lib/mbeditor/rack/pending_migration_bypass.rb +104 -0
  66. data/lib/mbeditor/rack/silence_ping_request.rb +10 -2
  67. data/lib/mbeditor/route_map.rb +2 -0
  68. data/lib/mbeditor/ruby_lsp_client.rb +71 -12
  69. data/lib/mbeditor/version.rb +1 -1
  70. metadata +7 -2
@@ -516,4 +516,13 @@ var GitPanel = function GitPanel(_ref) {
516
516
  );
517
517
  };
518
518
 
519
- window.GitPanel = GitPanel;
519
+ // Same convention as FileTreeMemo: compare the data props only. The handlers
520
+ // are re-created on every MbeditorApp render but each one only calls a setState
521
+ // or a service, so they are stable in effect. Without this the panel — commit
522
+ // history, file lists, the Redmine card — re-rendered on every keystroke in the
523
+ // editor, because MbeditorApp is subscribed to the whole store.
524
+ window.GitPanel = React.memo(GitPanel, function (prev, next) {
525
+ return prev.gitInfo === next.gitInfo &&
526
+ prev.error === next.error &&
527
+ prev.redmineEnabled === next.redmineEnabled;
528
+ });
@@ -7,12 +7,10 @@
7
7
  //
8
8
  // Errors from the same pass render here too, so a drop that both conflicts
9
9
  // and fails needs one dialog rather than two.
10
- var ImportConflictModal = function ImportConflictModal(_ref) {
11
- var conflicts = _ref.conflicts || [];
12
- var errors = _ref.errors || [];
13
- var onResolve = _ref.onResolve;
14
- var modalRef = React.useRef(null);
15
-
10
+ // Focus containment for a modal: initial focus in, Tab cycles inside, Escape
11
+ // dismisses, focus restored on unmount. Shared with ImportDialog — both are
12
+ // overlays over an editor that will happily steal focus back otherwise.
13
+ window.useModalFocusTrap = function useModalFocusTrap(modalRef, onDismiss) {
16
14
  React.useEffect(function () {
17
15
  var previouslyFocused = document.activeElement;
18
16
  var modal = modalRef.current;
@@ -25,7 +23,7 @@ var ImportConflictModal = function ImportConflictModal(_ref) {
25
23
  if (e.key === 'Escape') {
26
24
  e.preventDefault();
27
25
  e.stopPropagation();
28
- onResolve('skip');
26
+ onDismiss();
29
27
  return;
30
28
  }
31
29
  if (e.key !== 'Tab' || !modal) return;
@@ -57,7 +55,16 @@ var ImportConflictModal = function ImportConflictModal(_ref) {
57
55
  previouslyFocused.focus();
58
56
  }
59
57
  };
60
- }, [onResolve]);
58
+ }, [onDismiss]);
59
+ };
60
+
61
+ var ImportConflictModal = function ImportConflictModal(_ref) {
62
+ var conflicts = _ref.conflicts || [];
63
+ var errors = _ref.errors || [];
64
+ var onResolve = _ref.onResolve;
65
+ var modalRef = React.useRef(null);
66
+
67
+ window.useModalFocusTrap(modalRef, React.useCallback(function () { onResolve('skip'); }, [onResolve]));
61
68
 
62
69
  var button = function (label, mode, className) {
63
70
  return React.createElement(
@@ -0,0 +1,216 @@
1
+ 'use strict';
2
+
3
+ // Upload dialog: the pointed-at half of the drag-and-drop import.
4
+ //
5
+ // Everything past "pick files" is the existing import pipeline — the same
6
+ // entries shape, the same /import call, the same ImportConflictModal for
7
+ // replace/keep-both/skip. The only thing this adds is *where* the files land,
8
+ // which drag-and-drop answers by what you dropped onto and a file picker
9
+ // cannot answer at all.
10
+ //
11
+ // Destination suggestions come from the file tree already in memory
12
+ // (SearchService's index), so picking a folder that lives at
13
+ // app/assets/javascripts/ux/component offers that prefix rather than dumping
14
+ // `ux/component/` at the workspace root. The root stays on offer as the
15
+ // "create the structure from scratch" option.
16
+ var ImportDialog = function ImportDialog(_ref) {
17
+ var initialFolder = _ref.initialFolder || '';
18
+ var docs = _ref.docs || [];
19
+ var onCancel = _ref.onCancel;
20
+ var onImport = _ref.onImport;
21
+
22
+ var modalRef = React.useRef(null);
23
+ var filesInputRef = React.useRef(null);
24
+ var folderInputRef = React.useRef(null);
25
+
26
+ var _picked = React.useState(null);
27
+ var picked = _picked[0];
28
+ var setPicked = _picked[1];
29
+
30
+ var _dest = React.useState(initialFolder);
31
+ var dest = _dest[0];
32
+ var setDest = _dest[1];
33
+
34
+ window.useModalFocusTrap(modalRef, onCancel);
35
+
36
+ // webkitdirectory has no JSX/React prop, and React strips unknown lowercase
37
+ // attributes on <input>, so it is set on the DOM node directly.
38
+ React.useEffect(function () {
39
+ var el = folderInputRef.current;
40
+ if (!el) return;
41
+ el.setAttribute('webkitdirectory', '');
42
+ el.setAttribute('directory', '');
43
+ }, []);
44
+
45
+ var entries = (picked && picked.entries) || [];
46
+
47
+ var suggestions = React.useMemo(function () {
48
+ if (entries.length === 0) return [];
49
+ return FileImport.suggestDestinations(entries, docs);
50
+ }, [picked, docs]);
51
+
52
+ var existingFiles = React.useMemo(function () {
53
+ var set = new Set();
54
+ docs.forEach(function (d) { if (d.type === 'file') set.add(d.path); });
55
+ return set;
56
+ }, [docs]);
57
+
58
+ var onPick = function (e) {
59
+ var result = FileImport.entriesFromFileList(e.target.files);
60
+ e.target.value = ''; // so picking the same folder twice still fires change
61
+ if (result.entries.length === 0) return;
62
+ setPicked(result);
63
+ };
64
+
65
+ var normalisedDest = String(dest || '').trim().replace(/^\/+|\/+$/g, '');
66
+
67
+ var destOption = function (prefix, label, hint) {
68
+ var active = normalisedDest === prefix;
69
+ return React.createElement(
70
+ 'button',
71
+ {
72
+ key: '$' + prefix,
73
+ type: 'button',
74
+ className: 'import-dest-option' + (active ? ' active' : ''),
75
+ onClick: function () { setDest(prefix); }
76
+ },
77
+ React.createElement('i', { className: (active ? 'fas fa-dot-circle' : 'far fa-circle') + ' import-dest-radio' }),
78
+ React.createElement('span', { className: 'import-dest-label' }, label),
79
+ hint && React.createElement('span', { className: 'import-dest-hint' }, hint)
80
+ );
81
+ };
82
+
83
+ var offered = {};
84
+ var options = [];
85
+ var offer = function (prefix, label, hint) {
86
+ if (offered['$' + prefix]) return;
87
+ offered['$' + prefix] = true;
88
+ options.push(destOption(prefix, label, hint));
89
+ };
90
+
91
+ if (initialFolder) offer(initialFolder, initialFolder, 'selected folder');
92
+ suggestions.forEach(function (s) {
93
+ var bits = [];
94
+ if (s.files) bits.push(s.files + ' matching file' + (s.files === 1 ? '' : 's'));
95
+ if (s.dirs) bits.push(s.dirs + ' matching folder' + (s.dirs === 1 ? '' : 's'));
96
+ offer(s.prefix, s.prefix, bits.join(', '));
97
+ });
98
+ offer('', 'Workspace root', 'create the folders from the root');
99
+
100
+ var targets = entries.map(function (e) {
101
+ return FileImport.joinPath(normalisedDest, e.relativePath);
102
+ });
103
+ var replacing = targets.filter(function (t) { return existingFiles.has(t); }).length;
104
+
105
+ return React.createElement(
106
+ 'div',
107
+ { className: 'schema-modal-overlay', onClick: onCancel },
108
+ React.createElement(
109
+ 'div',
110
+ {
111
+ ref: modalRef,
112
+ className: 'schema-modal import-dialog',
113
+ role: 'dialog',
114
+ 'aria-modal': 'true',
115
+ 'aria-label': 'Upload files',
116
+ tabIndex: -1,
117
+ onClick: function (e) { e.stopPropagation(); }
118
+ },
119
+ React.createElement(
120
+ 'div', { className: 'schema-modal-header' },
121
+ React.createElement('div', { className: 'schema-modal-title' },
122
+ React.createElement('i', { className: 'fas fa-upload', style: { marginRight: '8px', opacity: 0.7 } }),
123
+ 'Upload files'),
124
+ React.createElement(
125
+ 'button',
126
+ { className: 'schema-modal-close', onClick: onCancel, title: 'Close' },
127
+ React.createElement('i', { className: 'fas fa-times' })
128
+ )
129
+ ),
130
+ React.createElement(
131
+ 'div', { className: 'schema-modal-body' },
132
+
133
+ React.createElement(
134
+ 'div', { className: 'import-pick-row' },
135
+ React.createElement(
136
+ 'button',
137
+ { type: 'button', className: 'import-pick-btn',
138
+ onClick: function () { filesInputRef.current && filesInputRef.current.click(); } },
139
+ React.createElement('i', { className: 'far fa-file' }), ' Choose files…'
140
+ ),
141
+ React.createElement(
142
+ 'button',
143
+ { type: 'button', className: 'import-pick-btn',
144
+ onClick: function () { folderInputRef.current && folderInputRef.current.click(); } },
145
+ React.createElement('i', { className: 'far fa-folder' }), ' Choose folder…'
146
+ ),
147
+ React.createElement('input', {
148
+ ref: filesInputRef, type: 'file', multiple: true,
149
+ style: { display: 'none' }, onChange: onPick
150
+ }),
151
+ React.createElement('input', {
152
+ ref: folderInputRef, type: 'file', multiple: true,
153
+ style: { display: 'none' }, onChange: onPick
154
+ })
155
+ ),
156
+
157
+ entries.length === 0 && React.createElement(
158
+ 'div', { className: 'import-dialog-empty' },
159
+ 'Pick files or a folder to upload. You can also drag them onto the explorer.'
160
+ ),
161
+
162
+ picked && picked.truncated && React.createElement(
163
+ 'div', { className: 'import-dialog-warning' },
164
+ 'Only the first ' + FileImport.MAX_ENTRIES + ' files will be uploaded.'
165
+ ),
166
+
167
+ entries.length > 0 && React.createElement(
168
+ React.Fragment, null,
169
+ React.createElement('div', { className: 'import-dialog-section-title' }, 'Destination'),
170
+ React.createElement('div', { className: 'import-dest-options' }, options),
171
+ React.createElement('input', {
172
+ className: 'import-dest-input',
173
+ type: 'text',
174
+ value: dest,
175
+ placeholder: 'workspace root',
176
+ spellCheck: false,
177
+ autoComplete: 'off',
178
+ 'aria-label': 'Destination folder',
179
+ onChange: function (e) { setDest(e.target.value); }
180
+ }),
181
+
182
+ React.createElement('div', { className: 'import-dialog-section-title' },
183
+ entries.length + ' file' + (entries.length === 1 ? '' : 's') +
184
+ (replacing > 0 ? ' — ' + replacing + ' will replace an existing file' : '')),
185
+ React.createElement(
186
+ 'ul', { className: 'import-conflict-list' },
187
+ targets.map(function (t, i) {
188
+ return React.createElement(
189
+ 'li', { key: t + '#' + i, title: t },
190
+ t,
191
+ existingFiles.has(t) && React.createElement(
192
+ 'span', { className: 'import-target-replaces' }, 'replaces')
193
+ );
194
+ })
195
+ )
196
+ )
197
+ ),
198
+ React.createElement(
199
+ 'div', { className: 'import-conflict-actions' },
200
+ React.createElement('button', { className: 'import-conflict-btn', onClick: onCancel }, 'Cancel'),
201
+ React.createElement(
202
+ 'button',
203
+ {
204
+ className: 'import-conflict-btn import-conflict-btn-primary',
205
+ disabled: entries.length === 0,
206
+ onClick: function () { onImport(entries, normalisedDest, picked); }
207
+ },
208
+ entries.length === 0 ? 'Upload' : 'Upload ' + entries.length + ' file' + (entries.length === 1 ? '' : 's')
209
+ )
210
+ )
211
+ )
212
+ );
213
+ };
214
+
215
+ // Expose globally for sprockets require
216
+ window.ImportDialog = ImportDialog;