ruflet_rails 0.0.13 → 0.0.14
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 +165 -62
- data/lib/generators/ruflet/install/install_generator.rb +3 -3
- data/lib/ruflet/rails/configuration.rb +13 -7
- data/lib/ruflet/rails/install_support.rb +19 -637
- data/lib/ruflet/rails/native_app/actions.rb +167 -0
- data/lib/ruflet/rails/native_app/customization.rb +118 -0
- data/lib/ruflet/rails/native_app/html_adapter.rb +248 -0
- data/lib/ruflet/rails/native_app/navigation.rb +645 -0
- data/lib/ruflet/rails/native_app/overlays.rb +222 -0
- data/lib/ruflet/rails/native_app/shell.rb +390 -0
- data/lib/ruflet/rails/native_app.rb +25 -222
- data/lib/ruflet/rails/protocol/web_app.rb +1 -1
- data/lib/ruflet/rails/railtie.rb +3 -20
- data/lib/ruflet/rails/view_helpers.rb +205 -0
- data/lib/ruflet/rails.rb +22 -55
- data/lib/ruflet/version.rb +1 -1
- data/lib/ruflet_rails.rb +0 -4
- metadata +14 -15
- data/lib/generators/ruflet/form/form_generator.rb +0 -55
- data/lib/generators/ruflet/scaffold/scaffold_generator.rb +0 -54
- data/lib/ruflet/rails/form_helpers.rb +0 -161
- data/lib/ruflet/rails/generator_hooks.rb +0 -25
- data/lib/ruflet/rails/resource_component.rb +0 -370
- data/lib/ruflet/rails/route_stack.rb +0 -90
- data/lib/ruflet/rails/webview_app.rb +0 -54
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "fileutils"
|
|
4
3
|
require "rbconfig"
|
|
5
|
-
require "active_support/core_ext/string/inflections"
|
|
6
4
|
|
|
7
5
|
module Ruflet
|
|
8
6
|
module Rails
|
|
@@ -13,9 +11,8 @@ module Ruflet
|
|
|
13
11
|
<<~RUBY
|
|
14
12
|
# frozen_string_literal: true
|
|
15
13
|
|
|
16
|
-
# The home screen for this Ruflet app. You own this file
|
|
17
|
-
#
|
|
18
|
-
# config/routes.rb (the install generator adds this line):
|
|
14
|
+
# The home screen for this Ruflet app. You own this file; nothing is
|
|
15
|
+
# auto-discovered. It is mounted explicitly in config/routes.rb:
|
|
19
16
|
# match "/ws", to: Ruflet::Rails.app(Rails.root.join("app/views/ruflet/main.rb")), via: :all
|
|
20
17
|
Ruflet.run do |page|
|
|
21
18
|
page.title = #{app_title.inspect}
|
|
@@ -39,637 +36,6 @@ module Ruflet
|
|
|
39
36
|
RUBY
|
|
40
37
|
end
|
|
41
38
|
|
|
42
|
-
def application_component_template
|
|
43
|
-
template = <<~RUBY
|
|
44
|
-
# frozen_string_literal: true
|
|
45
|
-
|
|
46
|
-
# ApplicationComponent is the base class for all Ruflet UI components in
|
|
47
|
-
# this Rails app. It explicitly includes Ruflet::UI::SharedControlForwarders
|
|
48
|
-
# so that every subclass has the full ruflet widget DSL available as
|
|
49
|
-
# instance methods (text, column, row, container, safe_area, filled_button,
|
|
50
|
-
# icon, data_table, alert_dialog, and every other ruflet widget).
|
|
51
|
-
# This is the same DSL that showcase uses — explicit, no Kernel magic.
|
|
52
|
-
class ApplicationComponent
|
|
53
|
-
include Ruflet::UI::SharedControlForwarders
|
|
54
|
-
|
|
55
|
-
attr_reader :page
|
|
56
|
-
|
|
57
|
-
def self.render(page, *args, **kwargs, &block)
|
|
58
|
-
new(page).render(*args, **kwargs, &block)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def initialize(page)
|
|
62
|
-
@page = page
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
private
|
|
66
|
-
|
|
67
|
-
# Widget builder calls on this component delegate to Ruflet::DSL,
|
|
68
|
-
# the same target used by the showcase App and by Kernel.
|
|
69
|
-
# Override in a subclass to scope builds to a local WidgetBuilder.
|
|
70
|
-
def control_delegate
|
|
71
|
-
Ruflet::DSL
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def platform
|
|
75
|
-
page.client_details["platform"].to_s
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def desktop?
|
|
79
|
-
%w[macos windows linux].include?(platform)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def web?
|
|
83
|
-
platform == "web"
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def mobile?
|
|
87
|
-
!desktop? && !web?
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def screen_width
|
|
91
|
-
page.client_details["width"].to_f
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
# Returns true when the client is narrower than 600 logical pixels
|
|
95
|
-
# (phones and small tablets), enabling compact list layouts.
|
|
96
|
-
def compact?
|
|
97
|
-
screen_width > 0 && screen_width < 600
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
RUBY
|
|
101
|
-
template.gsub(/^ /, " ")
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def application_component_path
|
|
105
|
-
File.join("app", "views", "ruflet", "components", "application_component.rb")
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def model_names(model_name)
|
|
109
|
-
raw = model_name.to_s.strip
|
|
110
|
-
class_name = raw.camelize
|
|
111
|
-
singular = raw.underscore.singularize
|
|
112
|
-
plural = singular.pluralize
|
|
113
|
-
{
|
|
114
|
-
class_name: class_name,
|
|
115
|
-
singular: singular,
|
|
116
|
-
plural: plural,
|
|
117
|
-
title: plural.humanize.titleize
|
|
118
|
-
}
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def form_view_path(model_name)
|
|
122
|
-
names = model_names(model_name)
|
|
123
|
-
|
|
124
|
-
File.join("app", "views", "ruflet", "components", names[:plural], "#{names[:singular]}_form.rb")
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def scaffold_component_path(model_name)
|
|
128
|
-
names = model_names(model_name)
|
|
129
|
-
|
|
130
|
-
File.join("app", "views", "ruflet", "components", names[:plural], "#{names[:singular]}_component.rb")
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
# A `case` with no `when` clause is a syntax error, so models without
|
|
134
|
-
# date/time attributes get the plain fallback body instead.
|
|
135
|
-
def scaffold_display_value_body(display_value_cases, indent)
|
|
136
|
-
return "#{indent}record.public_send(field).to_s" if display_value_cases.to_s.strip.empty?
|
|
137
|
-
|
|
138
|
-
reindented_cases = display_value_cases.gsub(/^ /, indent)
|
|
139
|
-
<<~RUBY.chomp.gsub(/^/, indent).gsub(/^#{Regexp.escape(indent)}__CASES__$/, reindented_cases)
|
|
140
|
-
case field
|
|
141
|
-
__CASES__
|
|
142
|
-
else
|
|
143
|
-
record.public_send(field).to_s
|
|
144
|
-
end
|
|
145
|
-
RUBY
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def scaffold_component_template(model_name:, attributes: [])
|
|
149
|
-
names = model_names(model_name)
|
|
150
|
-
model_class = names[:class_name]
|
|
151
|
-
component_class = "#{model_class}Component"
|
|
152
|
-
attrs = normalized_form_attributes(attributes)
|
|
153
|
-
control_locals = scaffold_control_locals(attrs)
|
|
154
|
-
control_list = scaffold_control_list(attrs)
|
|
155
|
-
attributes_hash = scaffold_attributes_hash(attrs)
|
|
156
|
-
|
|
157
|
-
<<~RUBY
|
|
158
|
-
# frozen_string_literal: true
|
|
159
|
-
|
|
160
|
-
require "date"
|
|
161
|
-
require "ruflet_rails"
|
|
162
|
-
|
|
163
|
-
# The model (#{model_class}) is inferred from the class name and the route
|
|
164
|
-
# ("/#{names[:plural]}") is declared in config/routes.rb. This file is YOURS:
|
|
165
|
-
# the whole CRUD UI (index table, detail screen, create/edit form) and the
|
|
166
|
-
# database calls (#{model_class}#update, #destroy!, .new) are explicit below
|
|
167
|
-
# so you can change the UI or logic however you like. The base class only
|
|
168
|
-
# provides reusable helpers: record loading, field inference, dialog
|
|
169
|
-
# open/close, the date/time picker value helpers, and refresh.
|
|
170
|
-
#
|
|
171
|
-
# The same component renders on web and on mobile/desktop.
|
|
172
|
-
class #{component_class} < Ruflet::Rails::ResourceComponent
|
|
173
|
-
def render
|
|
174
|
-
safe_area(
|
|
175
|
-
container(
|
|
176
|
-
expand: true,
|
|
177
|
-
padding: { left: 24, top: 16, right: 24, bottom: 24 },
|
|
178
|
-
content: column(
|
|
179
|
-
expand: true,
|
|
180
|
-
spacing: 16,
|
|
181
|
-
children: [
|
|
182
|
-
index_header,
|
|
183
|
-
compact? ? record_list(records) : record_table(records)
|
|
184
|
-
]
|
|
185
|
-
)
|
|
186
|
-
),
|
|
187
|
-
expand: true
|
|
188
|
-
)
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
def show(record)
|
|
192
|
-
safe_area(
|
|
193
|
-
container(
|
|
194
|
-
expand: true,
|
|
195
|
-
padding: { left: 24, top: 16, right: 24, bottom: 24 },
|
|
196
|
-
content: column(
|
|
197
|
-
expand: true,
|
|
198
|
-
spacing: 16,
|
|
199
|
-
children: [
|
|
200
|
-
show_header(record),
|
|
201
|
-
column(
|
|
202
|
-
spacing: 8,
|
|
203
|
-
children: resource_fields.map { |field| field_row(field.humanize, display_value(record, field)) }
|
|
204
|
-
)
|
|
205
|
-
]
|
|
206
|
-
)
|
|
207
|
-
),
|
|
208
|
-
expand: true
|
|
209
|
-
)
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
private
|
|
213
|
-
|
|
214
|
-
def show_header(record)
|
|
215
|
-
row(
|
|
216
|
-
alignment: "spaceBetween",
|
|
217
|
-
vertical_alignment: "center",
|
|
218
|
-
children: [
|
|
219
|
-
container(expand: true, content: text("\#{singular_title} ##\#{record_id(record)}", size: 24, weight: "bold")),
|
|
220
|
-
row(
|
|
221
|
-
tight: true,
|
|
222
|
-
spacing: 8,
|
|
223
|
-
children: [
|
|
224
|
-
outlined_button(content: text("Back"), on_click: ->(_event) { render_index }),
|
|
225
|
-
filled_button(content: text("Edit"), on_click: ->(_event) { open_form(record) })
|
|
226
|
-
]
|
|
227
|
-
)
|
|
228
|
-
]
|
|
229
|
-
)
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
def index_header
|
|
233
|
-
row(
|
|
234
|
-
alignment: "spaceBetween",
|
|
235
|
-
vertical_alignment: "center",
|
|
236
|
-
children: [
|
|
237
|
-
container(expand: true, content: text(resource_title, size: 24, weight: "bold")),
|
|
238
|
-
filled_button(content: text("New \#{singular_title}"), on_click: ->(_event) { open_form(model_class.new) })
|
|
239
|
-
]
|
|
240
|
-
)
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
def record_table(items)
|
|
244
|
-
row(
|
|
245
|
-
scroll: "auto",
|
|
246
|
-
children: [
|
|
247
|
-
data_table(
|
|
248
|
-
table_columns,
|
|
249
|
-
rows: items.map { |record| table_row(record) },
|
|
250
|
-
column_spacing: 24,
|
|
251
|
-
horizontal_margin: 12,
|
|
252
|
-
show_bottom_border: true
|
|
253
|
-
)
|
|
254
|
-
]
|
|
255
|
-
)
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
def table_columns
|
|
259
|
-
display_fields.map { |field| data_column(field.humanize) } + [
|
|
260
|
-
data_column("Actions"),
|
|
261
|
-
data_column(""),
|
|
262
|
-
data_column("")
|
|
263
|
-
]
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
def table_row(record)
|
|
267
|
-
data_row(
|
|
268
|
-
display_fields.map { |field| data_cell(display_value(record, field), on_tap: ->(_event) { open_show(record) }) } +
|
|
269
|
-
[
|
|
270
|
-
data_cell(icon("visibility", tooltip: "Show"), on_tap: ->(_event) { open_show(record) }),
|
|
271
|
-
data_cell(icon("edit", tooltip: "Edit"), on_tap: ->(_event) { open_form(record) }),
|
|
272
|
-
data_cell(icon("delete", tooltip: "Delete"), on_tap: ->(_event) { open_delete(record) })
|
|
273
|
-
]
|
|
274
|
-
)
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
def record_list(items)
|
|
278
|
-
column(spacing: 4, children: items.map { |record| record_tile(record) })
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
def record_tile(record)
|
|
282
|
-
list_tile(
|
|
283
|
-
title: text(primary_label(record)),
|
|
284
|
-
subtitle: secondary_label(record) ? text(secondary_label(record)) : nil,
|
|
285
|
-
trailing: row(
|
|
286
|
-
tight: true,
|
|
287
|
-
spacing: 0,
|
|
288
|
-
children: [
|
|
289
|
-
icon_button("edit", tooltip: "Edit", on_click: ->(_event) { open_form(record) }),
|
|
290
|
-
icon_button("delete", tooltip: "Delete", on_click: ->(_event) { open_delete(record) })
|
|
291
|
-
]
|
|
292
|
-
),
|
|
293
|
-
on_click: ->(_event) { open_show(record) }
|
|
294
|
-
)
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
def open_show(record)
|
|
298
|
-
show_record(record)
|
|
299
|
-
end
|
|
300
|
-
|
|
301
|
-
def open_form(record)
|
|
302
|
-
#{control_locals}
|
|
303
|
-
|
|
304
|
-
attributes = lambda do
|
|
305
|
-
{
|
|
306
|
-
#{attributes_hash}
|
|
307
|
-
}
|
|
308
|
-
end
|
|
309
|
-
|
|
310
|
-
dialog = nil
|
|
311
|
-
dialog = alert_dialog(
|
|
312
|
-
open: false,
|
|
313
|
-
modal: true,
|
|
314
|
-
scrollable: true,
|
|
315
|
-
title: text(record.persisted? ? "Edit \#{singular_title}" : "New \#{singular_title}"),
|
|
316
|
-
content: container(
|
|
317
|
-
width: dialog_width,
|
|
318
|
-
content: column(
|
|
319
|
-
spacing: 8,
|
|
320
|
-
horizontal_alignment: "stretch",
|
|
321
|
-
children: [
|
|
322
|
-
#{control_list}
|
|
323
|
-
]
|
|
324
|
-
)
|
|
325
|
-
),
|
|
326
|
-
actions: [
|
|
327
|
-
text_button(content: text("Cancel"), on_click: ->(_event) { close_dialog(dialog) }),
|
|
328
|
-
filled_button(content: text("Save"), on_click: ->(_event) {
|
|
329
|
-
# Persist with the model (this is your code — change it freely).
|
|
330
|
-
if record.update(attributes.call)
|
|
331
|
-
close_dialog(dialog)
|
|
332
|
-
refresh
|
|
333
|
-
show_snackbar("\#{singular_title} saved")
|
|
334
|
-
else
|
|
335
|
-
show_errors(record)
|
|
336
|
-
end
|
|
337
|
-
})
|
|
338
|
-
],
|
|
339
|
-
actions_alignment: "end"
|
|
340
|
-
)
|
|
341
|
-
open_dialog(dialog)
|
|
342
|
-
end
|
|
343
|
-
|
|
344
|
-
def open_delete(record)
|
|
345
|
-
dialog = nil
|
|
346
|
-
dialog = alert_dialog(
|
|
347
|
-
open: false,
|
|
348
|
-
modal: true,
|
|
349
|
-
title: text("Delete \#{singular_title}?"),
|
|
350
|
-
content: text("Permanently remove \#{singular_title} #\#{record_id(record)}?", no_wrap: false),
|
|
351
|
-
actions: [
|
|
352
|
-
text_button(content: text("Cancel"), on_click: ->(_event) { close_dialog(dialog) }),
|
|
353
|
-
filled_button(content: text("Delete"), on_click: ->(_event) {
|
|
354
|
-
# Destroy with the model (this is your code — change it freely).
|
|
355
|
-
record.destroy!
|
|
356
|
-
close_dialog(dialog)
|
|
357
|
-
refresh
|
|
358
|
-
show_snackbar("\#{singular_title} deleted")
|
|
359
|
-
})
|
|
360
|
-
],
|
|
361
|
-
actions_alignment: "end"
|
|
362
|
-
)
|
|
363
|
-
open_dialog(dialog)
|
|
364
|
-
end
|
|
365
|
-
|
|
366
|
-
def field_row(label, value)
|
|
367
|
-
row(
|
|
368
|
-
children: [
|
|
369
|
-
container(width: 140, content: text(label, weight: "bold")),
|
|
370
|
-
container(expand: true, content: text(value, no_wrap: false))
|
|
371
|
-
]
|
|
372
|
-
)
|
|
373
|
-
end
|
|
374
|
-
end
|
|
375
|
-
RUBY
|
|
376
|
-
end
|
|
377
|
-
|
|
378
|
-
def scaffold_control_locals(attrs)
|
|
379
|
-
attrs.map { |field| scaffold_control_local(field) }.join("\n ")
|
|
380
|
-
end
|
|
381
|
-
|
|
382
|
-
def scaffold_resource_fields(attrs)
|
|
383
|
-
attrs.map { |field| field[:name] }.inspect
|
|
384
|
-
end
|
|
385
|
-
|
|
386
|
-
def scaffold_display_fields(attrs)
|
|
387
|
-
fields = attrs.reject { |field| field[:type].to_s == "text" }
|
|
388
|
-
fields = attrs if fields.empty?
|
|
389
|
-
fields.first(3).map { |field| field[:name] }.inspect
|
|
390
|
-
end
|
|
391
|
-
|
|
392
|
-
def scaffold_display_value_cases(attrs)
|
|
393
|
-
attrs.filter_map do |field|
|
|
394
|
-
next unless %w[date datetime timestamp time date_range daterange].include?(field[:type].to_s)
|
|
395
|
-
|
|
396
|
-
name = field[:name]
|
|
397
|
-
formatter =
|
|
398
|
-
case field[:type].to_s
|
|
399
|
-
when "time"
|
|
400
|
-
"value.respond_to?(:strftime) ? value.strftime(\"%H:%M\") : value.to_s"
|
|
401
|
-
when "date_range", "daterange"
|
|
402
|
-
"value.respond_to?(:begin) && value.respond_to?(:end) ? \"\#{value.begin} - \#{value.end}\" : value.to_s"
|
|
403
|
-
when "date"
|
|
404
|
-
"value.respond_to?(:to_date) ? value.to_date.iso8601 : value.to_s"
|
|
405
|
-
else
|
|
406
|
-
"value.respond_to?(:iso8601) ? value.iso8601 : value.to_s"
|
|
407
|
-
end
|
|
408
|
-
[
|
|
409
|
-
" when #{name.inspect}",
|
|
410
|
-
" value = record.public_send(#{name.inspect})",
|
|
411
|
-
" #{formatter}"
|
|
412
|
-
].join("\n")
|
|
413
|
-
end.join("\n")
|
|
414
|
-
end
|
|
415
|
-
|
|
416
|
-
def scaffold_control_list(attrs)
|
|
417
|
-
attrs.map { |field| scaffold_control_view_name(field) }.join(",\n ")
|
|
418
|
-
end
|
|
419
|
-
|
|
420
|
-
def scaffold_attributes_hash(attrs)
|
|
421
|
-
attrs.map { |field| scaffold_attribute_pair(field) }.join(",\n ")
|
|
422
|
-
end
|
|
423
|
-
|
|
424
|
-
def scaffold_control_local(field)
|
|
425
|
-
name = field[:name]
|
|
426
|
-
type = field[:type].to_s
|
|
427
|
-
control = scaffold_control_name(field)
|
|
428
|
-
label = name.humanize
|
|
429
|
-
value = "record.public_send(#{name.inspect})"
|
|
430
|
-
|
|
431
|
-
case type
|
|
432
|
-
when "boolean"
|
|
433
|
-
"#{control} = checkbox(label: #{label.inspect}, value: !!#{value})"
|
|
434
|
-
when "date", "datetime", "timestamp"
|
|
435
|
-
display_control = "#{control}_display"
|
|
436
|
-
picker_value_helper = type == "date" ? "date_picker_value" : "datetime_picker_value"
|
|
437
|
-
<<~RUBY.chomp
|
|
438
|
-
#{control}_value = #{picker_value_helper}(#{value})
|
|
439
|
-
#{display_control} = text(date_display_value(#{control}_value))
|
|
440
|
-
#{control} = date_picker(
|
|
441
|
-
value: #{control}_value,
|
|
442
|
-
help_text: #{label.inspect},
|
|
443
|
-
on_change: ->(_event) do
|
|
444
|
-
close_dialogs(#{control})
|
|
445
|
-
page.update(#{display_control}, value: date_display_value(#{control}.value))
|
|
446
|
-
end
|
|
447
|
-
)
|
|
448
|
-
#{control}_field = column(
|
|
449
|
-
spacing: 6,
|
|
450
|
-
children: [
|
|
451
|
-
text(#{label.inspect}),
|
|
452
|
-
row(
|
|
453
|
-
spacing: 8,
|
|
454
|
-
children: [
|
|
455
|
-
container(expand: true, content: #{display_control}),
|
|
456
|
-
outlined_button(content: text("Choose #{label}"), on_click: ->(_event) { open_dialog(#{control}) })
|
|
457
|
-
]
|
|
458
|
-
)
|
|
459
|
-
]
|
|
460
|
-
)
|
|
461
|
-
RUBY
|
|
462
|
-
when "time"
|
|
463
|
-
display_control = "#{control}_display"
|
|
464
|
-
<<~RUBY.chomp
|
|
465
|
-
#{control}_value = time_picker_value(#{value})
|
|
466
|
-
#{display_control} = text(time_display_value(#{control}_value))
|
|
467
|
-
#{control} = time_picker(
|
|
468
|
-
value: #{control}_value,
|
|
469
|
-
help_text: #{label.inspect},
|
|
470
|
-
on_change: ->(_event) do
|
|
471
|
-
close_dialogs(#{control})
|
|
472
|
-
page.update(#{display_control}, value: time_display_value(#{control}.value))
|
|
473
|
-
end
|
|
474
|
-
)
|
|
475
|
-
#{control}_field = column(
|
|
476
|
-
spacing: 6,
|
|
477
|
-
children: [
|
|
478
|
-
text(#{label.inspect}),
|
|
479
|
-
row(
|
|
480
|
-
spacing: 8,
|
|
481
|
-
children: [
|
|
482
|
-
container(expand: true, content: #{display_control}),
|
|
483
|
-
outlined_button(content: text("Choose #{label}"), on_click: ->(_event) { open_dialog(#{control}) })
|
|
484
|
-
]
|
|
485
|
-
)
|
|
486
|
-
]
|
|
487
|
-
)
|
|
488
|
-
RUBY
|
|
489
|
-
when "date_range", "daterange"
|
|
490
|
-
display_control = "#{control}_display"
|
|
491
|
-
<<~RUBY.chomp
|
|
492
|
-
#{control}_start_value, #{control}_end_value = date_range_picker_values(#{value})
|
|
493
|
-
#{display_control} = text(date_range_display_value(#{control}_start_value, #{control}_end_value))
|
|
494
|
-
#{control} = date_range_picker(
|
|
495
|
-
start_value: #{control}_start_value,
|
|
496
|
-
end_value: #{control}_end_value,
|
|
497
|
-
help_text: #{label.inspect},
|
|
498
|
-
on_change: ->(_event) do
|
|
499
|
-
close_dialogs(#{control})
|
|
500
|
-
page.update(
|
|
501
|
-
#{display_control},
|
|
502
|
-
value: date_range_display_value(#{control}.start_value, #{control}.end_value)
|
|
503
|
-
)
|
|
504
|
-
end
|
|
505
|
-
)
|
|
506
|
-
#{control}_field = column(
|
|
507
|
-
spacing: 6,
|
|
508
|
-
children: [
|
|
509
|
-
text(#{label.inspect}),
|
|
510
|
-
row(
|
|
511
|
-
spacing: 8,
|
|
512
|
-
children: [
|
|
513
|
-
container(expand: true, content: #{display_control}),
|
|
514
|
-
outlined_button(content: text("Choose #{label}"), on_click: ->(_event) { open_dialog(#{control}) })
|
|
515
|
-
]
|
|
516
|
-
)
|
|
517
|
-
]
|
|
518
|
-
)
|
|
519
|
-
RUBY
|
|
520
|
-
when "text"
|
|
521
|
-
"#{control} = text_field(value: #{value}.to_s, label: #{label.inspect}, multiline: true, min_lines: 3)"
|
|
522
|
-
when "integer", "float", "decimal"
|
|
523
|
-
"#{control} = text_field(value: #{value}.to_s, label: #{label.inspect}, keyboard_type: \"number\")"
|
|
524
|
-
else
|
|
525
|
-
"#{control} = text_field(value: #{value}.to_s, label: #{label.inspect})"
|
|
526
|
-
end
|
|
527
|
-
end
|
|
528
|
-
|
|
529
|
-
def scaffold_attribute_pair(field)
|
|
530
|
-
name = field[:name]
|
|
531
|
-
type = field[:type].to_s
|
|
532
|
-
control = scaffold_control_name(field)
|
|
533
|
-
value =
|
|
534
|
-
case type
|
|
535
|
-
when "boolean"
|
|
536
|
-
"!!#{control}.value"
|
|
537
|
-
when "date"
|
|
538
|
-
"#{control}.value.to_s.split(\"T\", 2).first"
|
|
539
|
-
when "date_range", "daterange"
|
|
540
|
-
"Range.new(Date.parse(#{control}.start_value.to_s), Date.parse(#{control}.end_value.to_s))"
|
|
541
|
-
else
|
|
542
|
-
"#{control}.value.to_s"
|
|
543
|
-
end
|
|
544
|
-
|
|
545
|
-
"#{name.inspect} => #{value}"
|
|
546
|
-
end
|
|
547
|
-
|
|
548
|
-
def scaffold_control_name(field)
|
|
549
|
-
"#{field[:name].gsub(/[^a-zA-Z0-9_]/, '_')}_control"
|
|
550
|
-
end
|
|
551
|
-
|
|
552
|
-
def scaffold_control_view_name(field)
|
|
553
|
-
control = scaffold_control_name(field)
|
|
554
|
-
%w[date datetime timestamp time date_range daterange].include?(field[:type].to_s) ? "#{control}_field" : control
|
|
555
|
-
end
|
|
556
|
-
|
|
557
|
-
def form_view_template(model_name:, attributes:)
|
|
558
|
-
names = model_names(model_name)
|
|
559
|
-
attrs = normalized_form_attributes(attributes)
|
|
560
|
-
fields_literal = attrs.map { |field| form_field_literal(field) }.join(", ")
|
|
561
|
-
model_class = names[:class_name]
|
|
562
|
-
singular_title = names[:singular].humanize.titleize
|
|
563
|
-
|
|
564
|
-
<<~RUBY
|
|
565
|
-
# frozen_string_literal: true
|
|
566
|
-
|
|
567
|
-
require "ruflet_rails"
|
|
568
|
-
|
|
569
|
-
class #{model_class}Form < ApplicationComponent
|
|
570
|
-
include Ruflet::Rails::FormHelpers
|
|
571
|
-
|
|
572
|
-
def render(record:, title: nil, on_save: nil, on_cancel: nil)
|
|
573
|
-
title ||= record.persisted? ? "Edit #{singular_title}" : "New #{singular_title}"
|
|
574
|
-
fields = ruflet_form_bindings(record, form_fields)
|
|
575
|
-
|
|
576
|
-
column(
|
|
577
|
-
expand: true,
|
|
578
|
-
spacing: 12,
|
|
579
|
-
children: [
|
|
580
|
-
text(title, size: 24, weight: "bold"),
|
|
581
|
-
column(spacing: 8, horizontal_alignment: "stretch", children: ruflet_form_controls(fields)),
|
|
582
|
-
row(
|
|
583
|
-
spacing: 8,
|
|
584
|
-
children: [
|
|
585
|
-
outlined_button(
|
|
586
|
-
content: text("Cancel"),
|
|
587
|
-
on_click: ->(_e) { on_cancel ? on_cancel.call(page, record) : nil }
|
|
588
|
-
),
|
|
589
|
-
filled_button(
|
|
590
|
-
content: text(record.persisted? ? "Update #{singular_title}" : "Create #{singular_title}"),
|
|
591
|
-
on_click: ->(_e) { save(record, fields, on_save: on_save) }
|
|
592
|
-
)
|
|
593
|
-
]
|
|
594
|
-
)
|
|
595
|
-
]
|
|
596
|
-
)
|
|
597
|
-
end
|
|
598
|
-
|
|
599
|
-
def save(record, fields, on_save: nil)
|
|
600
|
-
if record.update(ruflet_form_attributes(fields, form_fields))
|
|
601
|
-
on_save ? on_save.call(page, record) : record
|
|
602
|
-
else
|
|
603
|
-
show_errors(record)
|
|
604
|
-
false
|
|
605
|
-
end
|
|
606
|
-
end
|
|
607
|
-
|
|
608
|
-
def form_fields
|
|
609
|
-
[#{fields_literal}]
|
|
610
|
-
end
|
|
611
|
-
|
|
612
|
-
def show_errors(record)
|
|
613
|
-
show_snackbar(error_message(record))
|
|
614
|
-
end
|
|
615
|
-
|
|
616
|
-
def show_snackbar(message)
|
|
617
|
-
page.snackbar = snackbar(text(message), open: true)
|
|
618
|
-
end
|
|
619
|
-
|
|
620
|
-
def error_message(record)
|
|
621
|
-
messages = record.errors.full_messages
|
|
622
|
-
messages.respond_to?(:to_sentence) ? messages.to_sentence : messages.join(", ")
|
|
623
|
-
end
|
|
624
|
-
end
|
|
625
|
-
RUBY
|
|
626
|
-
end
|
|
627
|
-
|
|
628
|
-
def normalized_form_attributes(attributes)
|
|
629
|
-
attrs = Array(attributes).map { |field| normalize_form_attribute(field) }.reject { |field| field[:name].empty? }
|
|
630
|
-
attrs.empty? ? [{ name: "name", type: "string" }] : attrs
|
|
631
|
-
end
|
|
632
|
-
|
|
633
|
-
def attributes_from_model(model_class)
|
|
634
|
-
return [] unless model_class.respond_to?(:columns)
|
|
635
|
-
|
|
636
|
-
model_class.columns.reject { |column|
|
|
637
|
-
%w[id created_at updated_at].include?(column.name)
|
|
638
|
-
}.map { |column| "#{column.name}:#{column.type}" }
|
|
639
|
-
end
|
|
640
|
-
|
|
641
|
-
def form_field_literal(field)
|
|
642
|
-
parts = [
|
|
643
|
-
"name: #{field[:name].inspect}",
|
|
644
|
-
"type: #{field[:type].inspect}"
|
|
645
|
-
]
|
|
646
|
-
parts << "class_name: #{field[:class_name].inspect}" if field[:class_name]
|
|
647
|
-
"{ #{parts.join(', ')} }"
|
|
648
|
-
end
|
|
649
|
-
|
|
650
|
-
def normalize_form_attribute(value)
|
|
651
|
-
raw = value.to_s.strip
|
|
652
|
-
name, type = raw.split(":", 2)
|
|
653
|
-
name = name.to_s.underscore.gsub(/[^a-z0-9_]/, "")
|
|
654
|
-
type = type.to_s.strip
|
|
655
|
-
type = "string" if type.empty?
|
|
656
|
-
name = "#{name}_id" if %w[references belongs_to association].include?(type) && !name.end_with?("_id")
|
|
657
|
-
association = association_class_name_for(name, type)
|
|
658
|
-
{
|
|
659
|
-
name: name,
|
|
660
|
-
type: association ? "association" : type
|
|
661
|
-
}.tap do |field|
|
|
662
|
-
field[:class_name] = association if association
|
|
663
|
-
end
|
|
664
|
-
end
|
|
665
|
-
|
|
666
|
-
def association_class_name_for(name, type)
|
|
667
|
-
return name.sub(/_id\z/, "").camelize if name.end_with?("_id")
|
|
668
|
-
return name.camelize if %w[references belongs_to association].include?(type)
|
|
669
|
-
|
|
670
|
-
nil
|
|
671
|
-
end
|
|
672
|
-
|
|
673
39
|
def default_ruflet_yaml(app_name:)
|
|
674
40
|
<<~YAML
|
|
675
41
|
app:
|
|
@@ -684,6 +50,23 @@ module Ruflet
|
|
|
684
50
|
YAML
|
|
685
51
|
end
|
|
686
52
|
|
|
53
|
+
def default_initializer(app_name:)
|
|
54
|
+
<<~RUBY
|
|
55
|
+
# frozen_string_literal: true
|
|
56
|
+
|
|
57
|
+
Ruflet::Rails.configure do |config|
|
|
58
|
+
config.app_name = #{app_name.inspect}
|
|
59
|
+
config.backend_url = ENV.fetch("RUFLET_BACKEND_URL", #{default_backend_url.inspect})
|
|
60
|
+
|
|
61
|
+
config.services = []
|
|
62
|
+
|
|
63
|
+
# Build assets. Replace these with your app artwork when ready.
|
|
64
|
+
config.splash_screen = Rails.root.join("app/assets/images/splash.png")
|
|
65
|
+
config.icon_launcher = Rails.root.join("app/assets/images/icon.png")
|
|
66
|
+
end
|
|
67
|
+
RUBY
|
|
68
|
+
end
|
|
69
|
+
|
|
687
70
|
def ruby_desktop_flag_bootstrap
|
|
688
71
|
<<~RUBY
|
|
689
72
|
# ruflet_rails desktop flag
|
|
@@ -756,7 +139,6 @@ module Ruflet
|
|
|
756
139
|
File.join("app", "views", "ruflet", "main.rb")
|
|
757
140
|
end
|
|
758
141
|
|
|
759
|
-
# Kept for backward compatibility with apps that use manual mount.
|
|
760
142
|
def route_snippet(entrypoint: default_entrypoint_path, mount_path: "/ws", helper: "app")
|
|
761
143
|
%(match "#{mount_path}", to: Ruflet::Rails.#{helper}(Rails.root.join("#{entrypoint}")), via: :all)
|
|
762
144
|
end
|