tramway 3.1.2.8 → 4.0.1.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/README.md +134 -18
- data/app/assets/builds/tailwind.css +2 -0
- data/app/assets/javascripts/tramway/tramway.js +202 -1
- data/app/assets/stylesheets/font-awesome.css +2337 -0
- data/app/assets/tailwind/application.css +29 -0
- data/app/components/tramway/chat_component.html.haml +1 -1
- data/app/components/tramway/containers/main_component.rb +1 -3
- data/app/components/tramway/containers/narrow_component.rb +5 -4
- data/app/components/tramway/form/builder.rb +1 -2
- data/app/components/tramway/grid/card_component.html.haml +2 -0
- data/app/components/tramway/grid/card_component.rb +37 -0
- data/app/components/tramway/grid_component.html.haml +18 -0
- data/app/components/tramway/grid_component.rb +76 -0
- data/app/components/tramway/native_text_component.rb +1 -2
- data/app/components/tramway/nav/item_component.rb +4 -2
- data/app/components/tramway/navbar_component.html.haml +131 -102
- data/app/components/tramway/navbar_component.rb +84 -2
- data/app/components/tramway/table/cell_component.rb +9 -1
- data/app/components/tramway/table/header_component.rb +23 -2
- data/app/components/tramway/table/row_component.rb +27 -10
- data/app/components/tramway/table_component.rb +28 -1
- data/app/controllers/tramway/entities_controller.rb +13 -6
- data/app/views/tramway/entities/_list.html.haml +2 -1
- data/app/views/tramway/layouts/application.html.haml +8 -6
- data/config/routes.rb +1 -2
- data/config/tailwind.config.js +57 -0
- data/lib/generators/tramway/install/font_awesome_helpers.rb +39 -0
- data/lib/generators/tramway/install/install_generator.rb +133 -66
- data/lib/generators/tramway/install/layout_helpers.rb +95 -0
- data/lib/tramway/configs/entities/page.rb +1 -0
- data/lib/tramway/engine.rb +12 -0
- data/lib/tramway/errors.rb +45 -0
- data/lib/tramway/forms/validations.rb +1 -2
- data/lib/tramway/helpers/navbar_helper.rb +3 -1
- data/lib/tramway/helpers/table_helper.rb +27 -0
- data/lib/tramway/helpers/views_helper.rb +26 -2
- data/lib/tramway/pg_searchable.rb +62 -0
- data/lib/tramway/searchable.rb +8 -26
- data/lib/tramway/version.rb +1 -1
- metadata +44 -4
|
@@ -2,12 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
require 'rails/generators'
|
|
4
4
|
require 'fileutils'
|
|
5
|
+
require_relative 'font_awesome_helpers'
|
|
6
|
+
require_relative 'layout_helpers'
|
|
5
7
|
|
|
6
8
|
module Tramway
|
|
7
9
|
module Generators
|
|
8
10
|
# nodoc
|
|
9
|
-
# rubocop:disable Metrics/ModuleLength
|
|
11
|
+
# rubocop:disable-next Metrics/ModuleLength
|
|
10
12
|
module InstallGeneratorHelpers
|
|
13
|
+
TAILWIND_SCROLLBAR_UTILITY = <<~CSS.chomp
|
|
14
|
+
@layer utilities {
|
|
15
|
+
.tramway-scrollbar {
|
|
16
|
+
scrollbar-width: thin;
|
|
17
|
+
scrollbar-color: rgba(82, 82, 91, 0.9) rgba(9, 9, 11, 0.92);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.tramway-scrollbar::-webkit-scrollbar {
|
|
21
|
+
width: 0.5rem;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.tramway-scrollbar::-webkit-scrollbar-track {
|
|
25
|
+
background: rgba(9, 9, 11, 0.92);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.tramway-scrollbar::-webkit-scrollbar-thumb {
|
|
29
|
+
border-radius: 9999px;
|
|
30
|
+
background: rgba(82, 82, 91, 0.9);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.tramway-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
34
|
+
background: rgba(113, 113, 122, 0.95);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.tramway-scrollbar::-webkit-scrollbar-corner {
|
|
38
|
+
background: transparent;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
CSS
|
|
42
|
+
|
|
43
|
+
TAILWIND_APPLICATION_STYLESHEET_CONTENT =
|
|
44
|
+
"@import \"tailwindcss\";\n\n#{TAILWIND_SCROLLBAR_UTILITY}\n".freeze
|
|
45
|
+
|
|
11
46
|
private
|
|
12
47
|
|
|
13
48
|
def gem_dependencies
|
|
@@ -16,7 +51,8 @@ module Tramway
|
|
|
16
51
|
{ name: 'kaminari', declaration: 'gem "kaminari"' },
|
|
17
52
|
{ name: 'view_component', declaration: 'gem "view_component"' },
|
|
18
53
|
{ name: 'dry-initializer', declaration: "gem 'dry-initializer'" },
|
|
19
|
-
{ name: 'dry-monads', declaration: "gem 'dry-monads'" }
|
|
54
|
+
{ name: 'dry-monads', declaration: "gem 'dry-monads'" },
|
|
55
|
+
{ name: 'pg_search', declaration: "gem 'pg_search'" }
|
|
20
56
|
]
|
|
21
57
|
end
|
|
22
58
|
|
|
@@ -78,12 +114,13 @@ module Tramway
|
|
|
78
114
|
|
|
79
115
|
def stimulus_controller_imports
|
|
80
116
|
[
|
|
81
|
-
'import { TramwaySelect, TableRowPreview, UiCheckbox, Tooltip } from "@tramway/tramway"'
|
|
117
|
+
'import { Navbar, TramwaySelect, TableRowPreview, UiCheckbox, Tooltip } from "@tramway/tramway"'
|
|
82
118
|
]
|
|
83
119
|
end
|
|
84
120
|
|
|
85
121
|
def stimulus_controller_registrations
|
|
86
122
|
[
|
|
123
|
+
"application.register('tramway-navbar', Navbar)",
|
|
87
124
|
"application.register('tramway-select', TramwaySelect)",
|
|
88
125
|
"application.register('table-row-preview', TableRowPreview)",
|
|
89
126
|
"application.register('ui--checkbox', UiCheckbox)",
|
|
@@ -95,28 +132,6 @@ module Tramway
|
|
|
95
132
|
@agents_file_path ||= File.join(destination_root, 'AGENTS.md')
|
|
96
133
|
end
|
|
97
134
|
|
|
98
|
-
def application_layout_haml_path
|
|
99
|
-
@application_layout_haml_path ||= File.join(destination_root, 'app/views/layouts/application.html.haml')
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def application_layout_erb_path
|
|
103
|
-
@application_layout_erb_path ||= File.join(destination_root, 'app/views/layouts/application.html.erb')
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def trix_haml_tags
|
|
107
|
-
" = stylesheet_link_tag \"trix\", \"data-turbo-track\": \"reload\"\n " \
|
|
108
|
-
"= javascript_include_tag \"trix\", \"data-turbo-track\": \"reload\", defer: true\n"
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def trix_erb_tags
|
|
112
|
-
" <%= stylesheet_link_tag \"trix\", \"data-turbo-track\": \"reload\" %>\n " \
|
|
113
|
-
"<%= javascript_include_tag \"trix\", \"data-turbo-track\": \"reload\", defer: true %>\n"
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def trix_already_present?(content)
|
|
117
|
-
content.include?('stylesheet_link_tag "trix"') || content.include?("stylesheet_link_tag 'trix'")
|
|
118
|
-
end
|
|
119
|
-
|
|
120
135
|
def codex_agents_instruction
|
|
121
136
|
'If you are Codex, use the tramway-skill skill for Tramway/Rails work in this project. ' \
|
|
122
137
|
'If tramway-skill is not installed, install it from https://github.com/Purple-Magic/tramway-skill/.'
|
|
@@ -134,15 +149,16 @@ module Tramway
|
|
|
134
149
|
end
|
|
135
150
|
|
|
136
151
|
def safelist_section(content)
|
|
137
|
-
|
|
138
|
-
|
|
152
|
+
open_index, close_index = safelist_bracket_range(content)
|
|
153
|
+
return nil unless open_index
|
|
154
|
+
|
|
155
|
+
content[(open_index + 1)...close_index]
|
|
139
156
|
end
|
|
140
157
|
|
|
141
158
|
def insert_entries(content, entries)
|
|
142
|
-
|
|
143
|
-
return content unless
|
|
159
|
+
_open_index, closing_index = safelist_bracket_range(content)
|
|
160
|
+
return content unless closing_index
|
|
144
161
|
|
|
145
|
-
closing_index = match.begin(0) + match[0].rindex(']')
|
|
146
162
|
indentation = closing_indentation(content, closing_index)
|
|
147
163
|
formatted_entries = entries.map { |entry| "#{indentation}'#{entry}',\n" }.join
|
|
148
164
|
|
|
@@ -154,6 +170,33 @@ module Tramway
|
|
|
154
170
|
content.dup.insert(closing_index, insertion)
|
|
155
171
|
end
|
|
156
172
|
|
|
173
|
+
# Finds the `[`/`]` pair that delimits the `safelist` array, ignoring brackets that
|
|
174
|
+
# appear inside quoted entries (e.g. `'md:transition-[width]'`) since a naive regex
|
|
175
|
+
# would otherwise stop at the first `]`, wherever it occurs.
|
|
176
|
+
def safelist_bracket_range(content)
|
|
177
|
+
start_index = content =~ /safelist\s*:\s*\[/
|
|
178
|
+
return [nil, nil] unless start_index
|
|
179
|
+
|
|
180
|
+
open_index = content.index('[', start_index)
|
|
181
|
+
close_index = matching_bracket_index(content, open_index)
|
|
182
|
+
|
|
183
|
+
[open_index, close_index]
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def matching_bracket_index(content, open_index)
|
|
187
|
+
depth = 0
|
|
188
|
+
|
|
189
|
+
content[open_index..].to_enum(:scan, /'[^']*'|"[^"]*"|[\[\]]/).each do
|
|
190
|
+
token = Regexp.last_match[0]
|
|
191
|
+
next unless ['[', ']'].include?(token)
|
|
192
|
+
|
|
193
|
+
depth += token == '[' ? 1 : -1
|
|
194
|
+
return open_index + Regexp.last_match.end(0) - 1 if depth.zero?
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
nil
|
|
198
|
+
end
|
|
199
|
+
|
|
157
200
|
def closing_indentation(content, index)
|
|
158
201
|
line_start = content.rindex("\n", index - 1) || 0
|
|
159
202
|
line = content[line_start..index]
|
|
@@ -171,30 +214,39 @@ module Tramway
|
|
|
171
214
|
content.end_with?("\n") ? "\n" : "\n\n"
|
|
172
215
|
end
|
|
173
216
|
|
|
174
|
-
# rubocop:disable Metrics/MethodLength
|
|
175
217
|
def append_missing_imports(content)
|
|
176
218
|
content = remove_legacy_stimulus_imports(content)
|
|
219
|
+
content = normalize_tramway_stimulus_imports(content)
|
|
177
220
|
missing_imports = stimulus_controller_imports.reject { |line| content.include?(line) }
|
|
178
221
|
return content if missing_imports.empty?
|
|
179
222
|
|
|
180
223
|
import_lines = content.each_line.with_index.filter_map do |line, index|
|
|
181
224
|
index if line.lstrip.start_with?('import ')
|
|
182
225
|
end
|
|
183
|
-
|
|
226
|
+
insert_missing_imports(content, missing_imports.join("\n"), import_lines)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def insert_missing_imports(content, imports, import_lines)
|
|
230
|
+
insertion = "#{imports}\n"
|
|
184
231
|
updated = content.dup
|
|
185
232
|
|
|
186
|
-
|
|
187
|
-
insertion_index = updated.lines[0..import_lines.max].join.length
|
|
188
|
-
updated.insert(insertion_index, insertion)
|
|
189
|
-
else
|
|
190
|
-
updated.prepend(insertion)
|
|
191
|
-
end
|
|
233
|
+
return updated.prepend(insertion) unless import_lines.any?
|
|
192
234
|
|
|
235
|
+
insertion_index = updated.lines[0..import_lines.max].join.length
|
|
236
|
+
updated.insert(insertion_index, insertion)
|
|
193
237
|
updated
|
|
194
238
|
end
|
|
195
|
-
|
|
239
|
+
|
|
240
|
+
def normalize_tramway_stimulus_imports(content)
|
|
241
|
+
tramway_import_lines = content.each_line.select { |line| tramway_stimulus_import_line?(line) }
|
|
242
|
+
return content unless tramway_import_lines.any?
|
|
243
|
+
return content if normalized_tramway_import_lines?(tramway_import_lines)
|
|
244
|
+
|
|
245
|
+
content.each_line.reject { |line| tramway_stimulus_import_line?(line) }.join
|
|
246
|
+
end
|
|
196
247
|
|
|
197
248
|
def append_missing_registrations(content)
|
|
249
|
+
content = normalize_tramway_stimulus_registrations(content)
|
|
198
250
|
missing_registrations = stimulus_controller_registrations.reject { |line| content.include?(line) }
|
|
199
251
|
return content if missing_registrations.empty?
|
|
200
252
|
|
|
@@ -221,6 +273,36 @@ module Tramway
|
|
|
221
273
|
content.each_line.reject { |line| legacy_imports.include?(line.strip) }.join
|
|
222
274
|
end
|
|
223
275
|
|
|
276
|
+
def tramway_stimulus_import_line?(line)
|
|
277
|
+
line.strip.match?(%r{^import \{.*\} from ["']@tramway/tramway["']$})
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def normalize_tramway_stimulus_registrations(content)
|
|
281
|
+
tramway_registration_lines = content.each_line.select { |line| tramway_stimulus_registration_line?(line) }
|
|
282
|
+
return content if tramway_registration_lines.sort == stimulus_controller_registrations.sort
|
|
283
|
+
return content unless tramway_registration_lines.any?
|
|
284
|
+
|
|
285
|
+
content.each_line.reject { |line| tramway_stimulus_registration_line?(line) }.join
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def tramway_stimulus_registration_line?(line)
|
|
289
|
+
tramway_registration_prefixes.any? { |prefix| line.strip.start_with?(prefix) }
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def normalized_tramway_import_lines?(tramway_import_lines)
|
|
293
|
+
tramway_import_lines.one? && tramway_import_lines.first.strip == stimulus_controller_imports.first
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def tramway_registration_prefixes
|
|
297
|
+
[
|
|
298
|
+
"application.register('tramway-navbar'",
|
|
299
|
+
"application.register('tramway-select'",
|
|
300
|
+
"application.register('table-row-preview'",
|
|
301
|
+
"application.register('ui--checkbox'",
|
|
302
|
+
"application.register('tramway-tooltip'"
|
|
303
|
+
]
|
|
304
|
+
end
|
|
305
|
+
|
|
224
306
|
def remove_legacy_importmap_pins(content)
|
|
225
307
|
content.each_line.reject { |line| legacy_importmap_pins.include?(line.strip) }.join
|
|
226
308
|
end
|
|
@@ -255,12 +337,12 @@ module Tramway
|
|
|
255
337
|
say_status(:warning, "Skipping AGENTS.md update: #{e.message}")
|
|
256
338
|
end
|
|
257
339
|
end
|
|
258
|
-
# rubocop:enable Metrics/ModuleLength
|
|
259
340
|
|
|
260
|
-
#
|
|
261
|
-
#
|
|
341
|
+
# Installs Tramway dependencies and app wiring for a host Rails app.
|
|
262
342
|
class InstallGenerator < Rails::Generators::Base
|
|
263
343
|
include InstallGeneratorHelpers
|
|
344
|
+
include InstallGeneratorFontAwesomeHelpers
|
|
345
|
+
include InstallGeneratorLayoutHelpers
|
|
264
346
|
|
|
265
347
|
desc 'Installs Tramway dependencies and Tailwind safelist configuration.'
|
|
266
348
|
|
|
@@ -310,15 +392,14 @@ module Tramway
|
|
|
310
392
|
path = tailwind_application_stylesheet_path
|
|
311
393
|
FileUtils.mkdir_p(File.dirname(path))
|
|
312
394
|
|
|
313
|
-
return create_file(path,
|
|
395
|
+
return create_file(path, TAILWIND_APPLICATION_STYLESHEET_CONTENT) unless File.exist?(path)
|
|
314
396
|
|
|
315
397
|
content = File.read(path)
|
|
316
|
-
return if content.include?(tailwind_css_import_line)
|
|
398
|
+
return if content.include?(tailwind_css_import_line) && content.include?(TAILWIND_SCROLLBAR_UTILITY)
|
|
317
399
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
end
|
|
400
|
+
updated = content.include?(tailwind_css_import_line) ? content : "#{content}\n#{tailwind_css_import_line}\n"
|
|
401
|
+
updated = "#{updated}\n#{TAILWIND_SCROLLBAR_UTILITY}\n" unless updated.include?(TAILWIND_SCROLLBAR_UTILITY)
|
|
402
|
+
File.write(path, updated)
|
|
322
403
|
end
|
|
323
404
|
|
|
324
405
|
def ensure_importmap_pin
|
|
@@ -348,30 +429,16 @@ module Tramway
|
|
|
348
429
|
|
|
349
430
|
def ensure_trix_in_application_layout
|
|
350
431
|
if File.exist?(application_layout_haml_path)
|
|
432
|
+
ensure_font_awesome_in_haml_layout
|
|
351
433
|
ensure_trix_in_haml_layout
|
|
352
434
|
elsif File.exist?(application_layout_erb_path)
|
|
435
|
+
ensure_font_awesome_in_erb_layout
|
|
353
436
|
ensure_trix_in_erb_layout
|
|
354
437
|
end
|
|
355
438
|
end
|
|
356
439
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
def ensure_trix_in_haml_layout
|
|
360
|
-
content = File.read(application_layout_haml_path)
|
|
361
|
-
return if trix_already_present?(content)
|
|
362
|
-
return unless content.match?(/^\s+%body/)
|
|
363
|
-
|
|
364
|
-
updated = content.sub(/^(\s+%body)/, "#{trix_haml_tags}\\1")
|
|
365
|
-
File.write(application_layout_haml_path, updated)
|
|
366
|
-
end
|
|
367
|
-
|
|
368
|
-
def ensure_trix_in_erb_layout
|
|
369
|
-
content = File.read(application_layout_erb_path)
|
|
370
|
-
return if trix_already_present?(content)
|
|
371
|
-
return unless content.include?('</head>')
|
|
372
|
-
|
|
373
|
-
updated = content.sub('</head>', "#{trix_erb_tags} </head>")
|
|
374
|
-
File.write(application_layout_erb_path, updated)
|
|
440
|
+
def ensure_navbar_sidebar_offset_in_application_layout
|
|
441
|
+
ensure_navbar_sidebar_offset
|
|
375
442
|
end
|
|
376
443
|
end
|
|
377
444
|
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tramway
|
|
4
|
+
module Generators
|
|
5
|
+
# Layout helpers for Tramway generator template updates.
|
|
6
|
+
module InstallGeneratorLayoutHelpers
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def application_layout_haml_path
|
|
10
|
+
@application_layout_haml_path ||= File.join(destination_root, 'app/views/layouts/application.html.haml')
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def application_layout_erb_path
|
|
14
|
+
@application_layout_erb_path ||= File.join(destination_root, 'app/views/layouts/application.html.erb')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def trix_haml_tags
|
|
18
|
+
" = stylesheet_link_tag \"trix\", \"data-turbo-track\": \"reload\"\n " \
|
|
19
|
+
"= javascript_include_tag \"trix\", \"data-turbo-track\": \"reload\", defer: true\n"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def trix_erb_tags
|
|
23
|
+
" <%= stylesheet_link_tag \"trix\", \"data-turbo-track\": \"reload\" %>\n " \
|
|
24
|
+
"<%= javascript_include_tag \"trix\", \"data-turbo-track\": \"reload\", defer: true %>\n"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def trix_already_present?(content)
|
|
28
|
+
content.include?('stylesheet_link_tag "trix"') || content.include?("stylesheet_link_tag 'trix'")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def font_awesome_already_present?(content)
|
|
32
|
+
content.include?('stylesheet_link_tag "font-awesome"') ||
|
|
33
|
+
content.include?("stylesheet_link_tag 'font-awesome'")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def navbar_sidebar_offset_class
|
|
37
|
+
'md:pl-72 transition-all duration-300 ease-in-out'
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def ensure_trix_in_haml_layout
|
|
41
|
+
content = File.read(application_layout_haml_path)
|
|
42
|
+
return if trix_already_present?(content)
|
|
43
|
+
return unless content.match?(/^\s+%body/)
|
|
44
|
+
|
|
45
|
+
updated = content.sub(/^(\s+%body)/, "#{trix_haml_tags}\\1")
|
|
46
|
+
File.write(application_layout_haml_path, updated)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def ensure_trix_in_erb_layout
|
|
50
|
+
content = File.read(application_layout_erb_path)
|
|
51
|
+
return if trix_already_present?(content)
|
|
52
|
+
return unless content.include?('</head>')
|
|
53
|
+
|
|
54
|
+
updated = content.sub('</head>', "#{trix_erb_tags} </head>")
|
|
55
|
+
File.write(application_layout_erb_path, updated)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def ensure_navbar_sidebar_offset
|
|
59
|
+
if File.exist?(application_layout_haml_path)
|
|
60
|
+
ensure_navbar_sidebar_offset_in_haml_layout
|
|
61
|
+
elsif File.exist?(application_layout_erb_path)
|
|
62
|
+
ensure_navbar_sidebar_offset_in_erb_layout
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def ensure_navbar_sidebar_offset_in_haml_layout
|
|
67
|
+
content = File.read(application_layout_haml_path)
|
|
68
|
+
return if content.include?(navbar_sidebar_offset_class)
|
|
69
|
+
return unless content.match?(/= tramway_main_container(?: class: '[^']*')? do/)
|
|
70
|
+
|
|
71
|
+
File.write(
|
|
72
|
+
application_layout_haml_path,
|
|
73
|
+
content.sub(
|
|
74
|
+
/= tramway_main_container(?: class: '[^']*')? do/,
|
|
75
|
+
"= tramway_main_container class: '#{navbar_sidebar_offset_class}' do"
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def ensure_navbar_sidebar_offset_in_erb_layout
|
|
81
|
+
content = File.read(application_layout_erb_path)
|
|
82
|
+
return if content.include?(navbar_sidebar_offset_class)
|
|
83
|
+
return unless content.match?(/<%= tramway_main_container(?: class: '[^']*')? do %>/)
|
|
84
|
+
|
|
85
|
+
File.write(
|
|
86
|
+
application_layout_erb_path,
|
|
87
|
+
content.sub(
|
|
88
|
+
/<%= tramway_main_container(?: class: '[^']*')? do %>/,
|
|
89
|
+
"<%= tramway_main_container class: '#{navbar_sidebar_offset_class}' do %>"
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
data/lib/tramway/engine.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'font-awesome-rails'
|
|
4
|
+
|
|
3
5
|
module Tramway
|
|
4
6
|
# Rails plugin is the Engine
|
|
5
7
|
#
|
|
@@ -17,8 +19,14 @@ module Tramway
|
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
initializer 'tramway.assets.precompile' do |app|
|
|
22
|
+
font_awesome_font_paths.each do |path|
|
|
23
|
+
path_string = path.to_s
|
|
24
|
+
app.config.assets.paths << path_string unless app.config.assets.paths.include?(path_string)
|
|
25
|
+
end
|
|
26
|
+
|
|
20
27
|
app.config.assets.precompile += %w[
|
|
21
28
|
tramway/tramway.js
|
|
29
|
+
font-awesome.css
|
|
22
30
|
]
|
|
23
31
|
end
|
|
24
32
|
|
|
@@ -88,6 +96,10 @@ module Tramway
|
|
|
88
96
|
end
|
|
89
97
|
end
|
|
90
98
|
|
|
99
|
+
def font_awesome_font_paths
|
|
100
|
+
[FontAwesome::Rails::Engine.root.join('app/assets/fonts')]
|
|
101
|
+
end
|
|
102
|
+
|
|
91
103
|
def configure_pagination
|
|
92
104
|
ActiveSupport.on_load(:action_controller) do
|
|
93
105
|
# Detecting tramway views path
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tramway
|
|
4
|
+
# Namespace for Tramway-specific error classes
|
|
5
|
+
module Errors
|
|
6
|
+
# Raised when a Tramway feature depends on infrastructure (e.g. a specific
|
|
7
|
+
# database adapter) that the current application does not provide, so the
|
|
8
|
+
# developer gets an actionable error instead of a raw, confusing failure
|
|
9
|
+
# deep inside the underlying gem.
|
|
10
|
+
class UnsupportedDatabaseAdapterError < StandardError
|
|
11
|
+
def initialize(feature:, model_class:, adapter:, required_adapter: 'PostgreSQL')
|
|
12
|
+
super(<<~MESSAGE)
|
|
13
|
+
Tramway #{feature} requires #{required_adapter}, but #{model_class} is connected \
|
|
14
|
+
through the "#{adapter}" adapter.
|
|
15
|
+
|
|
16
|
+
This feature relies on the `pg_search` gem, which needs PostgreSQL's full-text \
|
|
17
|
+
search functions (to_tsvector/to_tsquery) and does not work on other adapters.
|
|
18
|
+
|
|
19
|
+
To fix this:
|
|
20
|
+
- switch #{model_class}'s database connection to PostgreSQL, or
|
|
21
|
+
- define a custom `#{model_class}.search(query)` scope that works with your \
|
|
22
|
+
adapter; Tramway will use it instead of the built-in pg_search fallback.
|
|
23
|
+
MESSAGE
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Raised when a Tramway feature depends on a gem that could not be loaded or used, so the
|
|
28
|
+
# developer (or an AI agent) gets a precise, actionable fix instead of a raw NoMethodError
|
|
29
|
+
# or LoadError deep inside the missing gem's call chain.
|
|
30
|
+
class MissingGemError < StandardError
|
|
31
|
+
def initialize(feature:, model_class:, gem_name:, original_error:)
|
|
32
|
+
super(<<~MESSAGE)
|
|
33
|
+
Tramway #{feature} for #{model_class} could not use the `#{gem_name}` gem \
|
|
34
|
+
(#{original_error.class}: #{original_error.message}).
|
|
35
|
+
|
|
36
|
+
To fix this:
|
|
37
|
+
- add `gem "#{gem_name}"` to your Gemfile, run `bundle install`, and restart your \
|
|
38
|
+
server (or re-run `bin/rails g tramway:install`, which adds it automatically), or
|
|
39
|
+
- define a custom `#{model_class}.search(query)` scope that does not depend on \
|
|
40
|
+
`#{gem_name}`; Tramway will use it instead of the built-in fallback.
|
|
41
|
+
MESSAGE
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -16,11 +16,10 @@ module Tramway
|
|
|
16
16
|
def __initialize_validations(_subclass); end
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
# rubocop:disable Naming/PredicateMethod
|
|
19
|
+
# rubocop:disable-next Naming/PredicateMethod
|
|
20
20
|
def __apply_validations(_params)
|
|
21
21
|
valid?
|
|
22
22
|
end
|
|
23
|
-
# rubocop:enable Naming/PredicateMethod
|
|
24
23
|
end
|
|
25
24
|
end
|
|
26
25
|
end
|
|
@@ -6,11 +6,13 @@ module Tramway
|
|
|
6
6
|
module Helpers
|
|
7
7
|
# Provides navbar helpers for ActionView
|
|
8
8
|
module NavbarHelper
|
|
9
|
-
def tramway_navbar(with_entities: true, **options)
|
|
9
|
+
def tramway_navbar(with_entities: true, direction: :vertical, **options)
|
|
10
|
+
@tramway_navbar_direction = direction.presence&.to_sym || :vertical
|
|
10
11
|
initialize_navbar(with_entities:)
|
|
11
12
|
|
|
12
13
|
yield @navbar if block_given?
|
|
13
14
|
|
|
15
|
+
options[:direction] = direction if options[:direction].nil?
|
|
14
16
|
assign_navbar_items(options)
|
|
15
17
|
|
|
16
18
|
render_navbar_component(options)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tramway
|
|
4
|
+
module Helpers
|
|
5
|
+
# Provides size helpers for Tramway tables
|
|
6
|
+
module TableHelper
|
|
7
|
+
TABLE_SIZES = %i[small medium large].freeze
|
|
8
|
+
|
|
9
|
+
def tramway_table_size
|
|
10
|
+
context = tramway_table_context
|
|
11
|
+
return :medium unless context.respond_to?(:tramway_table_size)
|
|
12
|
+
|
|
13
|
+
TABLE_SIZES.include?(context.tramway_table_size) ? context.tramway_table_size : :medium
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def normalize_table_size(size)
|
|
17
|
+
TABLE_SIZES.include?(size) ? size : :medium
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def tramway_table_context
|
|
21
|
+
return __vc_original_view_context if respond_to?(:__vc_original_view_context)
|
|
22
|
+
|
|
23
|
+
view_context if respond_to?(:view_context)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'tramway/helpers/table_helper'
|
|
4
|
+
|
|
3
5
|
module Tramway
|
|
4
6
|
module Helpers
|
|
5
7
|
# Provides view-oriented helpers for ActionView
|
|
6
8
|
module ViewsHelper
|
|
7
9
|
include Tramway::Helpers::ComponentHelper
|
|
10
|
+
include Tramway::Helpers::TableHelper
|
|
8
11
|
|
|
9
12
|
FORM_SIZES = %i[small medium large].freeze
|
|
10
13
|
|
|
@@ -19,8 +22,16 @@ module Tramway
|
|
|
19
22
|
), &)
|
|
20
23
|
end
|
|
21
24
|
|
|
22
|
-
def tramway_table(**options, &)
|
|
23
|
-
component 'tramway/table', options:, &
|
|
25
|
+
def tramway_table(size: :medium, **options, &)
|
|
26
|
+
component 'tramway/table', size: normalize_table_size(size), options:, &
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def tramway_grid(rows:, columns:, outline: false, **options, &)
|
|
30
|
+
component 'tramway/grid', rows:, columns:, outline:, options:, &
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def tramway_card(size: [1, 1], **options, &)
|
|
34
|
+
component 'tramway/grid/card', size:, options:, &
|
|
24
35
|
end
|
|
25
36
|
|
|
26
37
|
def tramway_header(headers: nil, columns: nil, **options, &)
|
|
@@ -63,6 +74,8 @@ module Tramway
|
|
|
63
74
|
end
|
|
64
75
|
|
|
65
76
|
def tramway_main_container(**options, &)
|
|
77
|
+
options[:id] ||= 'tramway-main-container'
|
|
78
|
+
options[:class] = tramway_main_container_classes(options[:class])
|
|
66
79
|
component 'tramway/containers/main', options:, &
|
|
67
80
|
end
|
|
68
81
|
|
|
@@ -102,6 +115,17 @@ module Tramway
|
|
|
102
115
|
def normalize_form_size(size)
|
|
103
116
|
FORM_SIZES.include?(size) ? size : :medium
|
|
104
117
|
end
|
|
118
|
+
|
|
119
|
+
def tramway_main_container_classes(value)
|
|
120
|
+
classes = Array(value).flat_map { _1.to_s.split }
|
|
121
|
+
|
|
122
|
+
return classes.join(' ') if @tramway_navbar_direction.nil?
|
|
123
|
+
|
|
124
|
+
classes = classes.reject { _1.start_with?('md:pl-') || _1.start_with?('md:pr-') }
|
|
125
|
+
classes << 'md:pl-72' if @tramway_navbar_direction == :vertical
|
|
126
|
+
|
|
127
|
+
classes.join(' ')
|
|
128
|
+
end
|
|
105
129
|
end
|
|
106
130
|
end
|
|
107
131
|
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tramway/errors'
|
|
4
|
+
|
|
5
|
+
module Tramway
|
|
6
|
+
# Wires up a default, generic search across all string/text columns using the
|
|
7
|
+
# `pg_search` gem, so entities#index search works out of the box for any model
|
|
8
|
+
# that does not define its own `.search` scope.
|
|
9
|
+
#
|
|
10
|
+
# This is only supported on PostgreSQL, since pg_search depends on PostgreSQL's
|
|
11
|
+
# full-text search functions. On any other adapter it raises a clear,
|
|
12
|
+
# actionable error instead of failing with a confusing SQL error deep inside
|
|
13
|
+
# pg_search.
|
|
14
|
+
module PgSearchable
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def call(model_class, query)
|
|
18
|
+
ensure_postgres!(model_class)
|
|
19
|
+
ensure_pg_search_scope!(model_class)
|
|
20
|
+
|
|
21
|
+
model_class.tramway_pg_search(query)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def ensure_postgres!(model_class)
|
|
25
|
+
adapter = model_class.connection.adapter_name
|
|
26
|
+
|
|
27
|
+
return if adapter.casecmp('postgresql').zero?
|
|
28
|
+
|
|
29
|
+
raise Tramway::Errors::UnsupportedDatabaseAdapterError.new(
|
|
30
|
+
feature: 'the default entities#index search',
|
|
31
|
+
model_class:,
|
|
32
|
+
adapter:
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def ensure_pg_search_scope!(model_class)
|
|
37
|
+
return if model_class.respond_to?(:tramway_pg_search)
|
|
38
|
+
|
|
39
|
+
define_pg_search_scope!(model_class)
|
|
40
|
+
rescue LoadError, NameError => e
|
|
41
|
+
raise missing_pg_search_gem_error(model_class, e)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def define_pg_search_scope!(model_class)
|
|
45
|
+
require 'pg_search'
|
|
46
|
+
|
|
47
|
+
model_class.include(PgSearch::Model)
|
|
48
|
+
model_class.pg_search_scope :tramway_pg_search,
|
|
49
|
+
against: model_class.searchable_column_names,
|
|
50
|
+
using: { tsearch: { prefix: true } }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def missing_pg_search_gem_error(model_class, original_error)
|
|
54
|
+
Tramway::Errors::MissingGemError.new(
|
|
55
|
+
feature: 'the default entities#index search',
|
|
56
|
+
model_class:,
|
|
57
|
+
gem_name: 'pg_search',
|
|
58
|
+
original_error:
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|