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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c8e586fb734529e92147e9fb6935782e79baeb9722c833e8df0a97cc2c86c637
4
- data.tar.gz: 5da9242367bd195c715af433c8fb12fa440a610cacb5a1ff871cb8361c82233a
3
+ metadata.gz: a2d9c1ba81f945f5ce0772455f51e66a110fa43a1e24f62c96e6859312c4a968
4
+ data.tar.gz: d01eb3a6edd6e221e92c25e093c7c5944b32c50dafad6a4cacfa953e5c0b1925
5
5
  SHA512:
6
- metadata.gz: f403b63083bc139fc6546477c42fb476283213f0409a287ec3ffe46cc1a90cb10f197d6521216efce8121d910fdb8b2c94eefd8cd0abd8829725c852f043e57a
7
- data.tar.gz: 219a64ac7ecf5e2ded2081058f64f0bb4b0ce8373eac4f3cdcd8f9ca87aacd887676e680c31f360848dbcc255cfcdd176eaf67389569c64143639bf2643538f6
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 and laying them out radially around whichever model has
54
- the most associations. Search centres the view on a model; hovering an edge
55
- names the relation.
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`, `current_user`, etc. — e.g. `proc { current_user&.name }`. When `nil` or it returns a blank value, each browser falls back to a generated, locally-persisted, user-editable name. Collaboration activates once another participant actually connects, not merely when Action Cable is up (see [Collaborative pairing](#collaborative-pairing-optional)). |
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
- collabEnabled && collabIdentity && React.createElement(
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" }),