mbeditor 0.12.0 → 0.12.2
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 +115 -3
- data/README.md +38 -1
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +78 -0
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +6 -1
- data/app/assets/javascripts/mbeditor/components/ModelGraph.js +797 -254
- data/app/assets/javascripts/mbeditor/file_service.js +9 -0
- data/app/assets/stylesheets/mbeditor/editor.css +154 -2
- data/app/controllers/mbeditor/editors_controller.rb +44 -4
- data/app/services/mbeditor/model_graph_service.rb +13 -3
- data/app/services/mbeditor/route_service.rb +79 -0
- data/lib/mbeditor/configuration.rb +10 -3
- data/lib/mbeditor/engine.rb +17 -3
- data/lib/mbeditor/route_map.rb +1 -0
- data/lib/mbeditor/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a2d9c1ba81f945f5ce0772455f51e66a110fa43a1e24f62c96e6859312c4a968
|
|
4
|
+
data.tar.gz: d01eb3a6edd6e221e92c25e093c7c5944b32c50dafad6a4cacfa953e5c0b1925
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f8c189fa9f7215d5a376ab321a10186645c6671a2c1de874c3f3b254e3ebebb20a2c102e0c5357fb0ee0183eb0c634d2f26c7f7860901cec8e941f56ca26bd75
|
|
7
|
+
data.tar.gz: d0f8bdb6abe76b88e8438c0f31e78055d49fe09dfb3c88df1031312c33985b8ad2906f72482cc32a7c986a4019dee19226e42c96fa6d99b52f1da1cdb4faa790
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,118 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.12.2] - 2026-08-03
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Inline route hints in controllers.** Every action in a controller file is
|
|
14
|
+
annotated with the verb and path that reach it — `GET /orders/:id` beside
|
|
15
|
+
`def show` — and a public action nothing routes to is flagged `no route`.
|
|
16
|
+
Hovering adds the named-route helper. Answering "is this actually reachable?"
|
|
17
|
+
previously meant reading `config/routes.rb` and expanding `resources` in your
|
|
18
|
+
head.
|
|
19
|
+
|
|
20
|
+
Routes come from the host app's own route set rather than by parsing
|
|
21
|
+
`config/routes.rb`. mbeditor runs inside the app, so the routes are already
|
|
22
|
+
built — and they are the only source that accounts for `resources` expansion,
|
|
23
|
+
`member`/`collection` blocks, scopes, constraints and mounted engines.
|
|
24
|
+
|
|
25
|
+
- **`config.model_graph_max_models`** (default 1000, was a hard-coded 300).
|
|
26
|
+
A schema over the cap silently lost models and only said "(truncated)".
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- **Model boxes are sized to their contents** rather than all being one width,
|
|
30
|
+
so a long model or column name is no longer truncated while a model called
|
|
31
|
+
`Tag` wastes most of its box. Layer positions accumulate per layer, since a
|
|
32
|
+
fixed stride would let a wide box overlap the next layer.
|
|
33
|
+
- **Cluster blocks are packed in two dimensions** instead of stacked in a single
|
|
34
|
+
column, which left a schema with one big core and several small islands
|
|
35
|
+
running off the bottom with the right-hand side empty.
|
|
36
|
+
- **The dummy app now carries a real ActiveRecord schema** — 20 models and 44
|
|
37
|
+
associations covering a hub, a chain, a self-reference, a join table, a
|
|
38
|
+
polymorphic association and unconnected islands — so the model graph can be
|
|
39
|
+
demonstrated and tested against something representative.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
- **mbeditor could stop a host app from booting.** The pending-migrations
|
|
43
|
+
middleware was installed whenever `ActiveRecord::Migration::CheckPending` was
|
|
44
|
+
defined, but Rails only puts that middleware in the stack when
|
|
45
|
+
`config.active_record.migration_error` is `:page_load`. Any app loading
|
|
46
|
+
ActiveRecord with a different setting raised "No such middleware to insert
|
|
47
|
+
before" during boot. It now tests the same condition Rails does.
|
|
48
|
+
- **Long labels drew straight out past the edge of their model box.** SVG text
|
|
49
|
+
neither wraps nor ellipsises; labels are now measured against the font the
|
|
50
|
+
theme actually resolves and cut to fit.
|
|
51
|
+
- **"no database connection" hung below the box it belonged to** — the box
|
|
52
|
+
height did not count that placeholder line.
|
|
53
|
+
- **The model search is a real dropdown.** The native `<datalist>` rendered in
|
|
54
|
+
the browser's own chrome: unstyleable, and unable to show the table name and
|
|
55
|
+
column count beside each model. Arrow keys and Enter work as before.
|
|
56
|
+
- **The titlebar wrapped to two lines in a narrow window**, pushing the toolbar
|
|
57
|
+
out of its 32px row. It no longer wraps, and below the width where the toolbar
|
|
58
|
+
drops its button labels the title gives way to the icon alone.
|
|
59
|
+
|
|
60
|
+
## [0.12.1] - 2026-08-03
|
|
61
|
+
|
|
62
|
+
### Added
|
|
63
|
+
- **The collaboration name now comes from the signed-in user automatically.**
|
|
64
|
+
Previously it required a `user_name_callback`; with none set the editor fell
|
|
65
|
+
straight through to a generated name like "Witty Operator" even on an
|
|
66
|
+
authenticated instance. When no callback is configured, the name is now read
|
|
67
|
+
off `current_user`, trying `name`, `full_name`, `display_name`, `username`,
|
|
68
|
+
`login` and `email` in order. New `config.user_name_methods` names your own
|
|
69
|
+
column instead — `%w[preferred_name]` — so the common case no longer needs a
|
|
70
|
+
callback at all. An explicit `user_name_callback` still wins.
|
|
71
|
+
|
|
72
|
+
This works when your auth library exposes `current_user` to an
|
|
73
|
+
`ActionController::Base` subclass, which Devise and Sorcery do. A
|
|
74
|
+
`current_user` written by hand on your own `ApplicationController` is **not**
|
|
75
|
+
reachable — the engine's controllers do not inherit from it — so that case
|
|
76
|
+
still needs a callback reading `session` directly.
|
|
77
|
+
|
|
78
|
+
### Changed
|
|
79
|
+
- **The model graph is laid out by dependency, not by traversal order.** It now
|
|
80
|
+
uses the Sugiyama layered method — break cycles, layer by longest path, cut
|
|
81
|
+
crossings with the median heuristic, then straighten — which is what Graphviz
|
|
82
|
+
`dot`, and so Rails ERD, uses for this picture. The previous radial layout
|
|
83
|
+
became one enormous circle on a real schema, fitted so far out that no box was
|
|
84
|
+
legible. A model's position now tells you something: depth reads left to
|
|
85
|
+
right, and each connected part of the schema is drawn as its own area.
|
|
86
|
+
|
|
87
|
+
- **Your own presence chip only appears once someone else is connected.** Action
|
|
88
|
+
Cable is up in any normal dev setup, so it used to sit in the toolbar
|
|
89
|
+
permanently announcing a session of one.
|
|
90
|
+
|
|
91
|
+
### Fixed
|
|
92
|
+
- **The model graph was unusable on a large schema.** On 300 models and 513
|
|
93
|
+
associations a single zoom tick cost 27 ms; it is now 0.2 ms. Three causes,
|
|
94
|
+
all compounding: the entire layout ran on every render, the pan/zoom transform
|
|
95
|
+
sat in the render output so each tick rebuilt ~7,000 SVG elements, and the
|
|
96
|
+
wheel handler read `getBoundingClientRect()` per tick, forcing a synchronous
|
|
97
|
+
reflow of the whole scene.
|
|
98
|
+
- **Fit-to-pane produced a zoom the controls refused to honour.** It clamped to
|
|
99
|
+
`min(1, ...)` but not to the minimum zoom, so a large graph fitted at ~0.04
|
|
100
|
+
against a 0.2 floor — and the first scroll snapped up to the floor, a fivefold
|
|
101
|
+
jump anchored on the cursor that looked like the diagram leaping somewhere
|
|
102
|
+
random. The floor is now low enough to frame a few hundred models, and the fit
|
|
103
|
+
clamps to the same range the wheel enforces. Opening the tab also no longer
|
|
104
|
+
leaves the graph unfitted when the pane has not been sized yet.
|
|
105
|
+
- **Hovering a model now shows what its associations actually are** — macro,
|
|
106
|
+
name, direction and `through:` — and dims every unrelated edge, which is the
|
|
107
|
+
only way to follow one model's relationships among hundreds of lines.
|
|
108
|
+
- **Clicking a model zooms to it** instead of opening its schema; the schema is
|
|
109
|
+
an explicit button in the model's header, so navigating no longer throws a
|
|
110
|
+
modal at you.
|
|
111
|
+
- **The model-graph search field drew its text over a magnifier icon.** The icon
|
|
112
|
+
comes from the vendored Pico stylesheet, whose selector outranks a plain class
|
|
113
|
+
rule — disabling the browser's own `type="search"` decorations did not touch
|
|
114
|
+
it.
|
|
115
|
+
- **A failing `user_name_callback` was swallowed silently.** Any exception
|
|
116
|
+
returned a bare `nil`, which made a broken callback indistinguishable from an
|
|
117
|
+
unconfigured one: you got the generated name and no clue why. The usual cause
|
|
118
|
+
is a `NameError` on a `current_user` the engine cannot see. It is now logged
|
|
119
|
+
as `[mbeditor] user name lookup failed: ...` and still never breaks the
|
|
120
|
+
request.
|
|
121
|
+
|
|
10
122
|
## [0.12.0] - 2026-07-31
|
|
11
123
|
|
|
12
124
|
### Added
|
|
@@ -50,9 +162,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
50
162
|
|
|
51
163
|
- **A model graph — an entity diagram of the host app's ActiveRecord models.**
|
|
52
164
|
The activity-bar button opens it as a full-width editor tab, drawing each
|
|
53
|
-
model with its fields
|
|
54
|
-
the
|
|
55
|
-
|
|
165
|
+
model with its fields. Search centres the view on a model; hovering an edge
|
|
166
|
+
names the relation. (Released with a radial layout; replaced by a layered one
|
|
167
|
+
in the next version — see Unreleased.)
|
|
56
168
|
|
|
57
169
|
Associations are read by **reflection, not by parsing model files**. mbeditor
|
|
58
170
|
runs inside the host app, so `reflect_on_all_associations` is right there and
|
data/README.md
CHANGED
|
@@ -162,7 +162,8 @@ See [Resilient Routing](#resilient-routing) for details.
|
|
|
162
162
|
|
|
163
163
|
| Option | Default | Description |
|
|
164
164
|
|--------|---------|-------------|
|
|
165
|
-
| `user_name_callback` | `nil` | Proc resolving the display name shown on your caret during realtime collaboration. Executed via `instance_exec` inside the controller (like `authenticate_with`), so it can read `session`, `cookies`, `
|
|
165
|
+
| `user_name_callback` | `nil` | Proc resolving the display name shown on your caret during realtime collaboration. Only needed when the automatic lookup can't reach your user — see `user_name_methods` below. Executed via `instance_exec` inside the engine's controller (like `authenticate_with`), so it can read `session`, `cookies`, `params`, and anything your auth library exposes to an `ActionController::Base` subclass — e.g. `proc { User.find_by(id: session[:user_id])&.name }`. **It cannot see helper methods defined on your own `ApplicationController`**, because the engine's controllers do not inherit from it; a hand-rolled `current_user` living there will raise `NameError`, which is logged as `[mbeditor] user name lookup failed` and falls back to the generated name. |
|
|
166
|
+
| `user_name_methods` | `%w[name full_name display_name username login email]` | Attributes tried on `current_user`, in order, when no `user_name_callback` is set — the first non-blank one becomes your collaboration display name. This is what makes an authenticated editor show real names with no extra wiring, provided your auth library exposes `current_user` to `ActionController::Base` (Devise and Sorcery do; a `current_user` you wrote yourself on `ApplicationController` does not). Set it to your own column instead of writing a callback, e.g. `%w[preferred_name]`. |
|
|
166
167
|
|
|
167
168
|
## JavaScript intelligence
|
|
168
169
|
|
|
@@ -376,6 +377,42 @@ common cause of *"the other person's edits never show up."* For pairing, run a s
|
|
|
376
377
|
worker (`WEB_CONCURRENCY=0`, or `bundle exec rails server` which is single-process by
|
|
377
378
|
default). A multi-worker setup would additionally need a cross-process cable adapter, but
|
|
378
379
|
the in-memory buffer still would not be shared — single-process is the supported mode.
|
|
380
|
+
|
|
381
|
+
#### Showing real usernames
|
|
382
|
+
|
|
383
|
+
Each participant's caret and presence chip are labelled. With no configuration
|
|
384
|
+
the label is a generated name like *Witty Operator*, persisted per browser and
|
|
385
|
+
editable by clicking your own chip.
|
|
386
|
+
|
|
387
|
+
**If your auth library exposes `current_user` to controllers** — Devise and
|
|
388
|
+
Sorcery both do, because they include their helpers into `ActionController::Base`
|
|
389
|
+
— it already works with no configuration at all. mbeditor reads the first
|
|
390
|
+
non-blank of `name`, `full_name`, `display_name`, `username`, `login`, `email`.
|
|
391
|
+
|
|
392
|
+
To point it at a different column:
|
|
393
|
+
|
|
394
|
+
```ruby
|
|
395
|
+
Mbeditor.configure do |c|
|
|
396
|
+
c.user_name_methods = %w[preferred_name email]
|
|
397
|
+
end
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
**If `current_user` is your own method on `ApplicationController`**, mbeditor
|
|
401
|
+
cannot see it — the engine's controllers inherit from `ActionController::Base`,
|
|
402
|
+
not from your `ApplicationController`. Resolve the user yourself instead; the
|
|
403
|
+
proc runs in the engine's controller, so `session`, `cookies` and `params` are
|
|
404
|
+
all available:
|
|
405
|
+
|
|
406
|
+
```ruby
|
|
407
|
+
Mbeditor.configure do |c|
|
|
408
|
+
c.user_name_callback = proc { User.find_by(id: session[:user_id])&.name }
|
|
409
|
+
end
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
An explicit `user_name_callback` always wins over the automatic lookup. If it
|
|
413
|
+
raises or returns blank you get the generated name, and the reason is logged as
|
|
414
|
+
`[mbeditor] user name lookup failed: …` rather than silently swallowed.
|
|
415
|
+
|
|
379
416
|
## Search performance
|
|
380
417
|
|
|
381
418
|
Project search and JS definition lookups pick a backend per call:
|
|
@@ -90,6 +90,7 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
90
90
|
|
|
91
91
|
var blameDecorationsRef = useRef([]);
|
|
92
92
|
var gitLineDecorationsRef = useRef([]);
|
|
93
|
+
var routeDecorationsRef = useRef([]);
|
|
93
94
|
// Latest git line-diff refresh, read by the poll effect so its interval does
|
|
94
95
|
// not have to be torn down whenever the active tab changes.
|
|
95
96
|
var gitLineRefreshRef = useRef(null);
|
|
@@ -1530,6 +1531,83 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1530
1531
|
}
|
|
1531
1532
|
}, [isBlameVisible, tab.path, blameData, isBlameLoading]);
|
|
1532
1533
|
|
|
1534
|
+
// Inline route hints for controllers.
|
|
1535
|
+
//
|
|
1536
|
+
// Every `def` in a controller gets the verb and path that reach it drawn after
|
|
1537
|
+
// the line, and a public action nothing routes to is called out — the question
|
|
1538
|
+
// "is this actually reachable?" otherwise means reading config/routes.rb and
|
|
1539
|
+
// mentally expanding `resources`. Routes come from the host app's own route
|
|
1540
|
+
// set, which is the only thing that knows how those expand.
|
|
1541
|
+
//
|
|
1542
|
+
// Decorations, not view zones: a hint belongs on the line, and a zone would
|
|
1543
|
+
// push the code around every time a file was opened.
|
|
1544
|
+
useEffect(function () {
|
|
1545
|
+
var editor = monacoRef.current;
|
|
1546
|
+
if (!editor || !window.monaco) return;
|
|
1547
|
+
|
|
1548
|
+
var clear = function () {
|
|
1549
|
+
if (!routeDecorationsRef.current.length) return;
|
|
1550
|
+
routeDecorationsRef.current = editor.deltaDecorations(routeDecorationsRef.current, []);
|
|
1551
|
+
};
|
|
1552
|
+
|
|
1553
|
+
if (!tab.path || !/^app\/controllers\/.+_controller\.rb$/.test(tab.path)) {
|
|
1554
|
+
clear();
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
var cancelled = false;
|
|
1559
|
+
FileService.getRoutes(tab.path).then(function (data) {
|
|
1560
|
+
if (cancelled) return;
|
|
1561
|
+
var actions = (data && data.actions) || {};
|
|
1562
|
+
var model = editor.getModel();
|
|
1563
|
+
if (!model) return;
|
|
1564
|
+
|
|
1565
|
+
var decorations = [];
|
|
1566
|
+
var text = model.getValue().split('\n');
|
|
1567
|
+
// Only methods above the first `private`/`protected` are candidates for a
|
|
1568
|
+
// route; flagging a helper below it as unrouted would be noise.
|
|
1569
|
+
var visibilityEnded = false;
|
|
1570
|
+
|
|
1571
|
+
text.forEach(function (line, i) {
|
|
1572
|
+
if (/^\s*(private|protected)\s*$/.test(line)) { visibilityEnded = true; return; }
|
|
1573
|
+
var m = line.match(/^\s*def\s+(?:self\.)?([a-zA-Z_][a-zA-Z0-9_]*[?!=]?)/);
|
|
1574
|
+
if (!m) return;
|
|
1575
|
+
|
|
1576
|
+
var name = m[1];
|
|
1577
|
+
var routes = actions[name];
|
|
1578
|
+
var lineNo = i + 1;
|
|
1579
|
+
var col = line.length + 1;
|
|
1580
|
+
|
|
1581
|
+
if (routes && routes.length) {
|
|
1582
|
+
var label = routes.map(function (r) { return r.verb + ' ' + r.path; }).join(' · ');
|
|
1583
|
+
decorations.push({
|
|
1584
|
+
range: new window.monaco.Range(lineNo, col, lineNo, col),
|
|
1585
|
+
options: {
|
|
1586
|
+
after: { content: ' ' + label, inlineClassName: 'mbeditor-route-hint' },
|
|
1587
|
+
hoverMessage: routes.map(function (r) {
|
|
1588
|
+
return { value: '`' + r.verb + ' ' + r.path + '`' + (r.name ? ' — `' + r.name + '_path`' : '') };
|
|
1589
|
+
}),
|
|
1590
|
+
showIfCollapsed: true
|
|
1591
|
+
}
|
|
1592
|
+
});
|
|
1593
|
+
} else if (!visibilityEnded && !/[?!=]$/.test(name) && name !== 'initialize') {
|
|
1594
|
+
decorations.push({
|
|
1595
|
+
range: new window.monaco.Range(lineNo, col, lineNo, col),
|
|
1596
|
+
options: {
|
|
1597
|
+
after: { content: ' no route', inlineClassName: 'mbeditor-route-hint-none' },
|
|
1598
|
+
hoverMessage: [{ value: 'No route in this application dispatches to this action.' }],
|
|
1599
|
+
showIfCollapsed: true
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
});
|
|
1604
|
+
|
|
1605
|
+
routeDecorationsRef.current = editor.deltaDecorations(routeDecorationsRef.current, decorations);
|
|
1606
|
+
}).catch(function () { /* hints are additive; a failure just means none */ });
|
|
1607
|
+
|
|
1608
|
+
return function () { cancelled = true; clear(); };
|
|
1609
|
+
}, [tab.path, tab.externalContentVersion, monacoReady]);
|
|
1610
|
+
|
|
1533
1611
|
// Render Blame block headers (author + summary) above contiguous commit regions.
|
|
1534
1612
|
useEffect(function () {
|
|
1535
1613
|
if (!monacoRef.current || !window.monaco || !isBlameVisible || !blameData) return;
|
|
@@ -4217,7 +4217,12 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4217
4217
|
!toolbarIconOnly && " Git"
|
|
4218
4218
|
)
|
|
4219
4219
|
),
|
|
4220
|
-
|
|
4220
|
+
// Your own chip appears only once someone else is actually connected.
|
|
4221
|
+
// Alone it told you nothing — Action Cable is up in any normal dev setup,
|
|
4222
|
+
// so it sat in the toolbar permanently announcing a session of one. While
|
|
4223
|
+
// pairing it earns its place: it is how your peers see you, and it is the
|
|
4224
|
+
// control for renaming yourself.
|
|
4225
|
+
collabEnabled && collabIdentity && collabPeerIds.length > 0 && React.createElement(
|
|
4221
4226
|
React.Fragment,
|
|
4222
4227
|
null,
|
|
4223
4228
|
React.createElement("div", { className: "statusbar-sep" }),
|