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
|
@@ -117,6 +117,14 @@ var FileService = (function () {
|
|
|
117
117
|
return axios.get(window.mbeditorBasePath() + '/ping', { timeout: 4000 }).then(function(res) { return res.data; });
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
// Which routes reach each action in a controller. Returns { controller, actions }
|
|
121
|
+
// with an empty actions map for anything that is not a controller, so the
|
|
122
|
+
// caller does not have to know the naming convention.
|
|
123
|
+
function getRoutes(path) {
|
|
124
|
+
return axios.get(window.mbeditorBasePath() + '/routes?path=' + encodeURIComponent(path))
|
|
125
|
+
.then(function (res) { return res.data; });
|
|
126
|
+
}
|
|
127
|
+
|
|
120
128
|
function getState() {
|
|
121
129
|
return axios.get(window.mbeditorBasePath() + '/state').then(function(res) { return res.data; });
|
|
122
130
|
}
|
|
@@ -336,6 +344,7 @@ var FileService = (function () {
|
|
|
336
344
|
formatFile: formatFile,
|
|
337
345
|
runTests: runTests,
|
|
338
346
|
ping: ping,
|
|
347
|
+
getRoutes: getRoutes,
|
|
339
348
|
getState: getState,
|
|
340
349
|
saveState: saveState,
|
|
341
350
|
getBranchState: getBranchState,
|
|
@@ -61,6 +61,17 @@ html, body, #mbeditor-root {
|
|
|
61
61
|
font-size: 13px;
|
|
62
62
|
color: var(--ide-text);
|
|
63
63
|
font-weight: 500;
|
|
64
|
+
/* Never wrap: the titlebar is a fixed 32px row, so a second line pushed the
|
|
65
|
+
toolbar buttons out of it. */
|
|
66
|
+
white-space: nowrap;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* Below the width where the toolbar drops its button labels, drop the title
|
|
70
|
+
too. The layers icon beside it already identifies the editor, and the host
|
|
71
|
+
and port are in the browser's own address bar. */
|
|
72
|
+
@media (max-width: 1180px) {
|
|
73
|
+
.ide-titlebar-title { display: none; }
|
|
74
|
+
.ide-titlebar-icon { margin-right: 0; }
|
|
64
75
|
}
|
|
65
76
|
|
|
66
77
|
/* Fills the gap between the title and the button cluster. min-width: 0 lets it
|
|
@@ -3120,8 +3131,16 @@ button:not(.pico-btn) { margin-bottom: 0; }
|
|
|
3120
3131
|
margin-left: auto;
|
|
3121
3132
|
}
|
|
3122
3133
|
.ide-model-graph-search {
|
|
3123
|
-
width:
|
|
3124
|
-
|
|
3134
|
+
width: 170px;
|
|
3135
|
+
/* Two separate things drew over the text here. WebKit renders its own
|
|
3136
|
+
magnifier and clear button inside type="search"; and the vendored Pico
|
|
3137
|
+
stylesheet paints a magnifier as a background-image on every [type=search],
|
|
3138
|
+
positioned at the left, which the 8px padding put the text straight on top
|
|
3139
|
+
of. Disabling the native decorations alone fixed neither — the visible icon
|
|
3140
|
+
was Pico's. Both are off; the placeholder already says what the field is. */
|
|
3141
|
+
-webkit-appearance: none;
|
|
3142
|
+
appearance: none;
|
|
3143
|
+
padding: 3px 8px;
|
|
3125
3144
|
background: var(--ide-input-bg, #3c3c3c);
|
|
3126
3145
|
border: 1px solid var(--ide-border, #3c3c3c);
|
|
3127
3146
|
border-radius: 3px;
|
|
@@ -3243,3 +3262,136 @@ button:not(.pico-btn) { margin-bottom: 0; }
|
|
|
3243
3262
|
.statusbar-problems-error-icon { color: var(--ide-danger); }
|
|
3244
3263
|
.statusbar-problems-warning-icon { color: var(--ide-warning); }
|
|
3245
3264
|
.statusbar-problems-count { font-variant-numeric: tabular-nums; }
|
|
3265
|
+
|
|
3266
|
+
/* Search decorations off across engines — Chrome/Safari draw their own
|
|
3267
|
+
magnifier and clear button inside type="search". */
|
|
3268
|
+
.ide-model-graph-search::-webkit-search-decoration,
|
|
3269
|
+
.ide-model-graph-search::-webkit-search-cancel-button,
|
|
3270
|
+
.ide-model-graph-search::-webkit-search-results-button,
|
|
3271
|
+
.ide-model-graph-search::-webkit-search-results-decoration {
|
|
3272
|
+
-webkit-appearance: none;
|
|
3273
|
+
appearance: none;
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
/* Cluster areas — a connected part of the schema, drawn behind its models so
|
|
3277
|
+
the grouping is visible rather than implied by proximity alone. */
|
|
3278
|
+
.mg-region-box {
|
|
3279
|
+
fill: color-mix(in srgb, var(--ide-fg, #d4d4d4) 4%, transparent);
|
|
3280
|
+
stroke: color-mix(in srgb, var(--ide-fg, #d4d4d4) 12%, transparent);
|
|
3281
|
+
stroke-width: 1;
|
|
3282
|
+
}
|
|
3283
|
+
.mg-region-label {
|
|
3284
|
+
fill: color-mix(in srgb, var(--ide-fg, #d4d4d4) 45%, transparent);
|
|
3285
|
+
font-size: 11px;
|
|
3286
|
+
font-weight: 500;
|
|
3287
|
+
pointer-events: none;
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
/* Hovering a model dims every association it is not part of. With 500+ edges on
|
|
3291
|
+
screen the lines are otherwise indistinguishable from each other, and picking
|
|
3292
|
+
out one model's relationships is the whole reason to look at the diagram.
|
|
3293
|
+
Applied by toggling classes on the DOM rather than through React — re-rendering
|
|
3294
|
+
thousands of SVG nodes on mouseenter is what made this view unusable before. */
|
|
3295
|
+
/* The ~30 ms this costs on a 300-model graph is the browser restyling every
|
|
3296
|
+
edge and box, not anything in our code — toggling the classes themselves
|
|
3297
|
+
measures 1.6 ms. Switching to stroke-opacity/fill-opacity was tried on the
|
|
3298
|
+
theory that opacity stacking contexts were the cost; measured slower (38 ms
|
|
3299
|
+
vs 32 ms), so plain opacity stays. */
|
|
3300
|
+
.mg-focus-mode .mg-edge { opacity: 0.08; }
|
|
3301
|
+
.mg-focus-mode .mg-lit .mg-edge { opacity: 1; stroke-width: 2; }
|
|
3302
|
+
.mg-focus-mode .mg-node { opacity: 0.55; }
|
|
3303
|
+
.mg-focus-mode .mg-node:hover { opacity: 1; }
|
|
3304
|
+
|
|
3305
|
+
/* The association list shown while a model is hovered. Kept mounted and toggled
|
|
3306
|
+
with a class so moving between boxes doesn't thrash the DOM. */
|
|
3307
|
+
.mg-assoc-card {
|
|
3308
|
+
position: absolute;
|
|
3309
|
+
z-index: 30;
|
|
3310
|
+
max-width: 290px;
|
|
3311
|
+
padding: 8px 10px;
|
|
3312
|
+
background: var(--ide-panel-bg, #252526);
|
|
3313
|
+
border: 1px solid var(--ide-border, #3c3c3c);
|
|
3314
|
+
border-radius: 6px;
|
|
3315
|
+
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45);
|
|
3316
|
+
font-size: 11px;
|
|
3317
|
+
line-height: 1.5;
|
|
3318
|
+
color: var(--ide-text, #d4d4d4);
|
|
3319
|
+
pointer-events: none;
|
|
3320
|
+
}
|
|
3321
|
+
.mg-assoc-card-hidden { display: none; }
|
|
3322
|
+
.mg-assoc-title { font-weight: 600; margin-bottom: 4px; }
|
|
3323
|
+
.mg-assoc-count { color: var(--ide-text-muted, #858585); margin-bottom: 4px; }
|
|
3324
|
+
.mg-assoc-row { display: flex; align-items: baseline; gap: 5px; white-space: nowrap; }
|
|
3325
|
+
.mg-assoc-dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex-shrink: 0; }
|
|
3326
|
+
.mg-assoc-macro { color: var(--ide-accent-fg, #4ec9b0); }
|
|
3327
|
+
.mg-assoc-name { color: var(--ide-text, #d4d4d4); }
|
|
3328
|
+
.mg-assoc-dir { color: var(--ide-text-muted, #858585); overflow: hidden; text-overflow: ellipsis; }
|
|
3329
|
+
.mg-assoc-through { color: var(--ide-text-muted, #858585); font-style: italic; }
|
|
3330
|
+
.mg-assoc-more, .mg-assoc-empty { color: var(--ide-text-muted, #858585); margin-top: 3px; }
|
|
3331
|
+
|
|
3332
|
+
/* Explicit "open the schema" target in each model's header. */
|
|
3333
|
+
.mg-open-btn { cursor: pointer; }
|
|
3334
|
+
.mg-open-btn-bg { fill: transparent; }
|
|
3335
|
+
.mg-open-btn-bar { fill: var(--ide-text-muted, #858585); }
|
|
3336
|
+
.mg-open-btn:hover .mg-open-btn-bg { fill: color-mix(in srgb, var(--ide-fg, #d4d4d4) 16%, transparent); }
|
|
3337
|
+
.mg-open-btn:hover .mg-open-btn-bar { fill: var(--ide-fg, #d4d4d4); }
|
|
3338
|
+
.mg-node { cursor: zoom-in; }
|
|
3339
|
+
|
|
3340
|
+
/* Pico paints a magnifier as a background-image on every [type=search], at the
|
|
3341
|
+
left of the field, and our 8px padding put the text straight on top of it.
|
|
3342
|
+
Its selector is input:not([type=checkbox],...)[type=search] — specificity
|
|
3343
|
+
(0,2,1) — so a plain .class rule (0,1,0) loses and the icon stayed. Matching
|
|
3344
|
+
that specificity, and loading later, is what actually removes it. */
|
|
3345
|
+
input.ide-model-graph-search[type="search"] {
|
|
3346
|
+
background-image: none;
|
|
3347
|
+
padding-inline-start: 8px;
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
/* Model-graph search dropdown. Replaces a native <datalist>, which the browser
|
|
3351
|
+
renders in its own chrome — unstyleable, and unable to show the table name
|
|
3352
|
+
beside each model. */
|
|
3353
|
+
.mg-search-wrap { position: relative; display: inline-block; }
|
|
3354
|
+
.mg-search-list {
|
|
3355
|
+
position: absolute;
|
|
3356
|
+
top: calc(100% + 4px);
|
|
3357
|
+
right: 0;
|
|
3358
|
+
z-index: 40;
|
|
3359
|
+
min-width: 220px;
|
|
3360
|
+
max-height: 280px;
|
|
3361
|
+
overflow-y: auto;
|
|
3362
|
+
margin: 0;
|
|
3363
|
+
padding: 4px 0;
|
|
3364
|
+
list-style: none;
|
|
3365
|
+
background: var(--ide-panel-bg, #252526);
|
|
3366
|
+
border: 1px solid var(--ide-border, #3c3c3c);
|
|
3367
|
+
border-radius: 6px;
|
|
3368
|
+
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45);
|
|
3369
|
+
}
|
|
3370
|
+
.mg-search-option {
|
|
3371
|
+
display: flex;
|
|
3372
|
+
align-items: baseline;
|
|
3373
|
+
justify-content: space-between;
|
|
3374
|
+
gap: 10px;
|
|
3375
|
+
padding: 4px 10px;
|
|
3376
|
+
font-size: 11px;
|
|
3377
|
+
color: var(--ide-text, #d4d4d4);
|
|
3378
|
+
cursor: pointer;
|
|
3379
|
+
white-space: nowrap;
|
|
3380
|
+
}
|
|
3381
|
+
.mg-search-option-active { background: var(--ide-accent, #3794ff); color: #fff; }
|
|
3382
|
+
.mg-search-option-name { font-family: var(--ide-mono, monospace); }
|
|
3383
|
+
.mg-search-option-meta { color: var(--ide-text-muted, #858585); font-size: 10px; }
|
|
3384
|
+
.mg-search-option-active .mg-search-option-meta { color: rgba(255, 255, 255, 0.75); }
|
|
3385
|
+
|
|
3386
|
+
/* Inline route hints beside controller actions. Muted and italic so they read
|
|
3387
|
+
as annotation rather than code — they sit on the same line as the `def`. */
|
|
3388
|
+
.mbeditor-route-hint {
|
|
3389
|
+
color: var(--ide-text-muted, #858585) !important;
|
|
3390
|
+
font-style: italic;
|
|
3391
|
+
opacity: 0.85;
|
|
3392
|
+
}
|
|
3393
|
+
.mbeditor-route-hint-none {
|
|
3394
|
+
color: var(--ide-warning, #cca700) !important;
|
|
3395
|
+
font-style: italic;
|
|
3396
|
+
opacity: 0.8;
|
|
3397
|
+
}
|
|
@@ -1072,6 +1072,19 @@ module Mbeditor
|
|
|
1072
1072
|
}
|
|
1073
1073
|
end
|
|
1074
1074
|
|
|
1075
|
+
# GET /mbeditor/routes?path=app/controllers/users_controller.rb
|
|
1076
|
+
#
|
|
1077
|
+
# Which routes reach each action in a controller, for the inline hints the
|
|
1078
|
+
# editor draws beside every `def`. Returns {} for anything that is not a
|
|
1079
|
+
# controller rather than erroring — the client asks for every Ruby file it
|
|
1080
|
+
# opens and should not have to know the convention.
|
|
1081
|
+
def routes
|
|
1082
|
+
key = RouteService.controller_key(params[:path].to_s)
|
|
1083
|
+
return render json: { controller: nil, actions: {} } unless key
|
|
1084
|
+
|
|
1085
|
+
render json: { controller: key, actions: RouteService.for_controller(key) }
|
|
1086
|
+
end
|
|
1087
|
+
|
|
1075
1088
|
# GET /mbeditor/related_files?path=...
|
|
1076
1089
|
def related_files
|
|
1077
1090
|
path = resolve_path(params[:path])
|
|
@@ -1145,14 +1158,41 @@ module Mbeditor
|
|
|
1145
1158
|
# authenticate_with) so the host app can supply a collaboration display name.
|
|
1146
1159
|
# nil — including any failure or a blank result — falls through to the
|
|
1147
1160
|
# client-generated name.
|
|
1161
|
+
# Display name for the collaboration caret. An explicit user_name_callback
|
|
1162
|
+
# wins; otherwise fall back to current_user, when the host's auth library put
|
|
1163
|
+
# it within reach of an ActionController::Base subclass (Devise, Sorcery and
|
|
1164
|
+
# friends do; a current_user hand-written on the host's own
|
|
1165
|
+
# ApplicationController does not — see default_user_name).
|
|
1148
1166
|
def resolved_user_name
|
|
1149
1167
|
cb = Mbeditor.configuration.user_name_callback
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
name = instance_exec(&cb)
|
|
1168
|
+
name = cb ? instance_exec(&cb) : default_user_name
|
|
1153
1169
|
name = name.to_s.strip
|
|
1154
1170
|
name.empty? ? nil : name
|
|
1155
|
-
rescue StandardError
|
|
1171
|
+
rescue StandardError => e
|
|
1172
|
+
# Was a bare `nil`, which made a broken callback indistinguishable from an
|
|
1173
|
+
# unconfigured one: you got the generated name and no clue why. The usual
|
|
1174
|
+
# cause is a NameError on current_user, and silently swallowing it sent
|
|
1175
|
+
# people looking in the wrong place.
|
|
1176
|
+
Rails.logger.warn("[mbeditor] user name lookup failed: #{e.class}: #{e.message}")
|
|
1177
|
+
nil
|
|
1178
|
+
end
|
|
1179
|
+
|
|
1180
|
+
# Read the display name straight off current_user, trying each configured
|
|
1181
|
+
# attribute in turn. This is what makes an authenticated editor show real
|
|
1182
|
+
# names with no extra wiring; user_name_methods exists so an app whose column
|
|
1183
|
+
# isn't in the default list can name it instead of writing a callback.
|
|
1184
|
+
def default_user_name
|
|
1185
|
+
return nil unless respond_to?(:current_user, true)
|
|
1186
|
+
|
|
1187
|
+
user = current_user
|
|
1188
|
+
return nil unless user
|
|
1189
|
+
|
|
1190
|
+
Array(Mbeditor.configuration.user_name_methods).each do |m|
|
|
1191
|
+
next unless user.respond_to?(m)
|
|
1192
|
+
|
|
1193
|
+
value = user.public_send(m).to_s.strip
|
|
1194
|
+
return value unless value.empty?
|
|
1195
|
+
end
|
|
1156
1196
|
nil
|
|
1157
1197
|
end
|
|
1158
1198
|
|
|
@@ -22,7 +22,16 @@ module Mbeditor
|
|
|
22
22
|
SolidQueue SolidCache SolidCable
|
|
23
23
|
].freeze
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
# Cap on models drawn. 300 was chosen before the layout could handle that
|
|
26
|
+
# many; a schema slightly over it silently lost models and only said
|
|
27
|
+
# "(truncated)". Raised, and configurable for anything larger — the cost of
|
|
28
|
+
# a bigger graph is now the browser's rendering, not the layout.
|
|
29
|
+
DEFAULT_MAX_MODELS = 1000
|
|
30
|
+
|
|
31
|
+
def self.max_models
|
|
32
|
+
value = Mbeditor.configuration.model_graph_max_models.to_i
|
|
33
|
+
value.positive? ? value : DEFAULT_MAX_MODELS
|
|
34
|
+
end
|
|
26
35
|
|
|
27
36
|
# Only the first few columns travel: the diagram box shows that many and the
|
|
28
37
|
# full list is a click away in the schema modal, which fetches its own data
|
|
@@ -86,7 +95,8 @@ module Mbeditor
|
|
|
86
95
|
classes = model_classes
|
|
87
96
|
return unavailable("No ActiveRecord models found in this application.") if classes.empty?
|
|
88
97
|
|
|
89
|
-
|
|
98
|
+
limit = max_models
|
|
99
|
+
models = classes.first(limit).map { |klass| describe_model(klass, root) }
|
|
90
100
|
known = models.map { |m| m[:name] }.to_set
|
|
91
101
|
|
|
92
102
|
{
|
|
@@ -95,7 +105,7 @@ module Mbeditor
|
|
|
95
105
|
# Edges to classes outside the graph (a gem's model, or a typo in
|
|
96
106
|
# class_name:) would render as arrows into nowhere.
|
|
97
107
|
edges: models.flat_map { |m| m.delete(:edges) }.select { |e| known.include?(e[:to]) }.uniq,
|
|
98
|
-
truncated: classes.length >
|
|
108
|
+
truncated: classes.length > limit,
|
|
99
109
|
generatedAt: Time.now.utc.iso8601
|
|
100
110
|
}
|
|
101
111
|
rescue StandardError => e
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mbeditor
|
|
4
|
+
# Which routes reach a controller's actions, for the inline hints shown beside
|
|
5
|
+
# each `def` in a controller file.
|
|
6
|
+
#
|
|
7
|
+
# Read from the host app's own route set rather than by parsing config/routes.rb.
|
|
8
|
+
# mbeditor runs inside the host app, so the routes are already built — and they
|
|
9
|
+
# are the only source that accounts for `resources` expansion, `member`/
|
|
10
|
+
# `collection` blocks, scopes, constraints, mounted engines and anything a
|
|
11
|
+
# routes file does in plain Ruby. Parsing the file would get all of that wrong.
|
|
12
|
+
module RouteService
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# Actions with no route are worth calling out, but only for controllers Rails
|
|
16
|
+
# would actually dispatch to. These are the inherited ones every controller
|
|
17
|
+
# has and nobody routes.
|
|
18
|
+
NON_ACTION_METHODS = %w[
|
|
19
|
+
new inspect method_of to_s hash class dup freeze
|
|
20
|
+
].freeze
|
|
21
|
+
|
|
22
|
+
# "app/controllers/admin/users_controller.rb" -> "admin/users", which is the
|
|
23
|
+
# key Rails stores in a route's defaults.
|
|
24
|
+
def controller_key(relative_path)
|
|
25
|
+
path = relative_path.to_s.sub(%r{\A/+}, "")
|
|
26
|
+
match = path.match(%r{\Aapp/controllers/(.+)_controller\.rb\z})
|
|
27
|
+
return nil unless match
|
|
28
|
+
|
|
29
|
+
match[1]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# => { "show" => [{ verb:, path:, name: }], ... }
|
|
33
|
+
def for_controller(key)
|
|
34
|
+
return {} if key.nil? || key.empty?
|
|
35
|
+
return {} unless defined?(Rails) && Rails.respond_to?(:application) && Rails.application
|
|
36
|
+
|
|
37
|
+
routes_for(key)
|
|
38
|
+
rescue StandardError
|
|
39
|
+
# A broken route set must not take the editor down with it — the file still
|
|
40
|
+
# opens, just without hints.
|
|
41
|
+
{}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def routes_for(key)
|
|
45
|
+
out = {}
|
|
46
|
+
Rails.application.routes.routes.each do |route|
|
|
47
|
+
defaults = route.defaults
|
|
48
|
+
next unless defaults[:controller] == key
|
|
49
|
+
|
|
50
|
+
action = defaults[:action].to_s
|
|
51
|
+
next if action.empty?
|
|
52
|
+
|
|
53
|
+
(out[action] ||= []) << {
|
|
54
|
+
verb: verb_for(route),
|
|
55
|
+
path: path_for(route),
|
|
56
|
+
name: route.name
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
out
|
|
60
|
+
end
|
|
61
|
+
private_class_method :routes_for
|
|
62
|
+
|
|
63
|
+
# Rails has moved this between a String and a Regexp across versions; both
|
|
64
|
+
# stringify usefully, but a Regexp needs its slashes stripped.
|
|
65
|
+
def verb_for(route)
|
|
66
|
+
verb = route.verb
|
|
67
|
+
verb = verb.source.gsub(%r{[$^]}, "") if verb.is_a?(Regexp)
|
|
68
|
+
verb = verb.to_s
|
|
69
|
+
verb.empty? ? "ANY" : verb
|
|
70
|
+
end
|
|
71
|
+
private_class_method :verb_for
|
|
72
|
+
|
|
73
|
+
# "(.:format)" is noise in a hint that has to fit on one line.
|
|
74
|
+
def path_for(route)
|
|
75
|
+
route.path.spec.to_s.sub(/\(\.:format\)\z/, "")
|
|
76
|
+
end
|
|
77
|
+
private_class_method :path_for
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -5,14 +5,14 @@ module Mbeditor
|
|
|
5
5
|
attr_accessor :allowed_environments, :workspace_root, :excluded_paths, :rubocop_command, :rubocop_server,
|
|
6
6
|
:redmine_enabled, :redmine_url, :redmine_api_key, :redmine_ticket_source,
|
|
7
7
|
:test_framework, :test_command, :test_timeout,
|
|
8
|
-
:authenticate_with, :authentication_cache_ttl, :user_name_callback,
|
|
8
|
+
:authenticate_with, :authentication_cache_ttl, :user_name_callback, :user_name_methods,
|
|
9
9
|
:lint_timeout, :base_branch_candidates, :git_timeout, :search_timeout,
|
|
10
10
|
:ruby_def_include_dirs, :related_files_custom_paths,
|
|
11
11
|
:mount_path, :resilient_routing, :js_global_identifiers,
|
|
12
12
|
:js_program, :js_program_exclude,
|
|
13
13
|
:js_syntax_check, :babel_standalone_path,
|
|
14
14
|
:ruby_lsp, :ruby_lsp_command, :ruby_lsp_timeout,
|
|
15
|
-
:exception_capture,
|
|
15
|
+
:exception_capture, :model_graph_max_models,
|
|
16
16
|
:search_respect_gitignore, :ripgrep_command
|
|
17
17
|
|
|
18
18
|
def initialize
|
|
@@ -58,7 +58,11 @@ module Mbeditor
|
|
|
58
58
|
@ruby_def_include_dirs = %w[app/models app/controllers app/helpers app/concerns]
|
|
59
59
|
@related_files_custom_paths = []
|
|
60
60
|
@authentication_cache_ttl = 0
|
|
61
|
-
@user_name_callback = nil # proc resolved in controller context (instance_exec) → collaboration display name; nil falls through to the client-generated name
|
|
61
|
+
@user_name_callback = nil # proc resolved in controller context (instance_exec) → collaboration display name; nil falls through to current_user, then to the client-generated name
|
|
62
|
+
# Attributes tried on current_user, in order, when no user_name_callback
|
|
63
|
+
# is set. First non-blank one wins. Name your own column here rather than
|
|
64
|
+
# writing a callback for the common case.
|
|
65
|
+
@user_name_methods = %w[name full_name display_name username login email]
|
|
62
66
|
@js_global_identifiers = [] # extra ambient JS globals for the editor (runtime-only names invisible to static scan, e.g. %w[Routes I18n])
|
|
63
67
|
# Load the workspace's own JS source into Monaco's TypeScript program, so
|
|
64
68
|
# cross-file references get real inferred types instead of ambient `any`.
|
|
@@ -80,6 +84,9 @@ module Mbeditor
|
|
|
80
84
|
# same exposure the log panel already has, since it tails the dev log.
|
|
81
85
|
# Set to false to record nothing.
|
|
82
86
|
@exception_capture = :auto
|
|
87
|
+
# Models drawn in the model graph before it reports itself truncated.
|
|
88
|
+
# nil uses ModelGraphService::DEFAULT_MAX_MODELS.
|
|
89
|
+
@model_graph_max_models = nil
|
|
83
90
|
@mount_path = nil # explicit URL prefix override; nil falls through to detection/"/mbeditor"
|
|
84
91
|
@resilient_routing = true # serve /mbeditor from middleware so the editor survives a broken host routes.rb; false is the escape hatch
|
|
85
92
|
end
|
data/lib/mbeditor/engine.rb
CHANGED
|
@@ -54,9 +54,23 @@ module Mbeditor
|
|
|
54
54
|
# Insert before CheckPending so our middleware wraps it and can rescue
|
|
55
55
|
# the error it raises. Falls back silently if CheckPending is absent
|
|
56
56
|
# (e.g. host app does not use ActiveRecord).
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
|
|
57
|
+
#
|
|
58
|
+
# The constant being defined is not enough: Rails only puts CheckPending
|
|
59
|
+
# in the stack when config.active_record.migration_error is :page_load, so
|
|
60
|
+
# an app that loads ActiveRecord with any other setting has the constant
|
|
61
|
+
# but no middleware — and insert_before then raises "No such middleware",
|
|
62
|
+
# taking the host app's boot down with it. app.middleware is a
|
|
63
|
+
# MiddlewareStackProxy here and cannot be inspected, so the only way to
|
|
64
|
+
# know is to try it. Losing this middleware costs a friendlier pending-
|
|
65
|
+
# migration page, which is never worth failing to boot over.
|
|
66
|
+
# Rescuing the call is not an option: app.middleware is a
|
|
67
|
+
# MiddlewareStackProxy, so insert_before only records the operation and the
|
|
68
|
+
# "No such middleware" error surfaces later, during merge_into. Test the
|
|
69
|
+
# same condition Rails uses to add CheckPending in the first place.
|
|
70
|
+
migration_error = app.config.respond_to?(:active_record) &&
|
|
71
|
+
app.config.active_record[:migration_error]
|
|
72
|
+
|
|
73
|
+
if defined?(ActiveRecord::Migration::CheckPending) && migration_error == :page_load
|
|
60
74
|
app.middleware.insert_before ActiveRecord::Migration::CheckPending,
|
|
61
75
|
Mbeditor::Rack::HandlePendingMigrations
|
|
62
76
|
end
|
data/lib/mbeditor/route_map.rb
CHANGED
|
@@ -44,6 +44,7 @@ module Mbeditor
|
|
|
44
44
|
get 'client_config', to: 'editors#client_config'
|
|
45
45
|
get 'related_files', to: 'editors#related_files'
|
|
46
46
|
get 'model_schema', to: 'editors#model_schema'
|
|
47
|
+
get 'routes', to: 'editors#routes'
|
|
47
48
|
get 'changelog', to: 'editors#changelog'
|
|
48
49
|
get 'git_info', to: 'editors#git_info'
|
|
49
50
|
get 'git_status', to: 'editors#git_status'
|
data/lib/mbeditor/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mbeditor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.12.
|
|
4
|
+
version: 0.12.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oliver Noonan
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-08-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -133,6 +133,7 @@ files:
|
|
|
133
133
|
- app/services/mbeditor/rails_related_files_service.rb
|
|
134
134
|
- app/services/mbeditor/redmine_service.rb
|
|
135
135
|
- app/services/mbeditor/ri_definition_service.rb
|
|
136
|
+
- app/services/mbeditor/route_service.rb
|
|
136
137
|
- app/services/mbeditor/ruby_definition_service.rb
|
|
137
138
|
- app/services/mbeditor/safe_path.rb
|
|
138
139
|
- app/services/mbeditor/schema_service.rb
|