ruflet_rails 0.0.10 → 0.0.11
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/lib/generators/ruflet/install/install_generator.rb +4 -8
- data/lib/generators/ruflet/scaffold/scaffold_generator.rb +10 -16
- data/lib/ruflet/rails/assets.rb +87 -0
- data/lib/ruflet/rails/configuration.rb +129 -0
- data/lib/ruflet/rails/install_support.rb +83 -126
- data/lib/ruflet/rails/native_app.rb +253 -0
- data/lib/ruflet/rails/protocol/endpoint.rb +3 -11
- data/lib/ruflet/rails/protocol/local_server.rb +27 -194
- data/lib/ruflet/rails/protocol/middleware.rb +74 -1
- data/lib/ruflet/rails/protocol/static_index_guard.rb +6 -0
- data/lib/ruflet/rails/protocol/web_app.rb +194 -0
- data/lib/ruflet/rails/protocol/web_app_endpoint.rb +44 -0
- data/lib/ruflet/rails/protocol/websocket_detection.rb +19 -0
- data/lib/ruflet/rails/protocol.rb +3 -0
- data/lib/ruflet/rails/railtie.rb +51 -39
- data/lib/ruflet/rails/resource_component.rb +188 -8
- data/lib/ruflet/rails/route_stack.rb +90 -0
- data/lib/ruflet/rails/view_helpers.rb +58 -0
- data/lib/ruflet/rails/webview_app.rb +54 -0
- data/lib/ruflet/rails.rb +133 -5
- data/lib/ruflet/version.rb +1 -1
- data/lib/ruflet_rails.rb +6 -1
- metadata +17 -8
- data/lib/ruflet/rails/resource_view.rb +0 -124
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ebf6ba0607ab65bf23420e9d03d39283aac94ad1ffd92bcd6e96c21fa7d79c9a
|
|
4
|
+
data.tar.gz: 74bba5b6bbf7976457a49556f5ee3c81c42f7afe30ede8979e5cf6f3f4e77354
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d1027674f71e87d89e24e2ca90bb9906fe60a67b7f23d93b30e90e053e7a710439fe8d22774976f9c83cdbf83ca82b28aa6610eeec6178d413430fa105ced16c
|
|
7
|
+
data.tar.gz: e55b5f9c8a3549fb37833f5cc88d9950cf35428a2bd688884205b6c83a1ff7d68c627a25469a5893d0b81dd445d5c697fe66b80dcac91c57ae129748da9f1e6b
|
|
@@ -25,15 +25,11 @@ module Ruflet
|
|
|
25
25
|
create_file target, Ruflet::Rails::InstallSupport.default_ruflet_yaml(app_name: app_name)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
def
|
|
29
|
-
target = File.join(destination_root,
|
|
30
|
-
return
|
|
31
|
-
|
|
32
|
-
route = Ruflet::Rails::InstallSupport.route_snippet(entrypoint: entrypoint_path)
|
|
33
|
-
source = File.read(target)
|
|
34
|
-
return if source.include?(route)
|
|
28
|
+
def create_ruflet_initializer
|
|
29
|
+
target = File.join(destination_root, Ruflet::Rails::InstallSupport.initializer_path)
|
|
30
|
+
return if File.exist?(target)
|
|
35
31
|
|
|
36
|
-
|
|
32
|
+
create_file target, Ruflet::Rails::InstallSupport.initializer_template(entrypoint: entrypoint_path)
|
|
37
33
|
end
|
|
38
34
|
|
|
39
35
|
def add_desktop_flag_to_binstubs
|
|
@@ -10,17 +10,7 @@ module Ruflet
|
|
|
10
10
|
argument :model_name, type: :string
|
|
11
11
|
argument :attributes, type: :array, default: [], banner: "field:type field:type"
|
|
12
12
|
|
|
13
|
-
desc "Generate a Rails-first Ruflet resource
|
|
14
|
-
|
|
15
|
-
def create_ruflet_resource_view
|
|
16
|
-
create_file(
|
|
17
|
-
File.join(destination_root, scaffold_view_path),
|
|
18
|
-
Ruflet::Rails::InstallSupport.scaffold_view_template(
|
|
19
|
-
model_name: model_name,
|
|
20
|
-
attributes: scaffold_attributes
|
|
21
|
-
)
|
|
22
|
-
)
|
|
23
|
-
end
|
|
13
|
+
desc "Generate a Rails-first Ruflet resource component for an existing model."
|
|
24
14
|
|
|
25
15
|
def create_ruflet_resource_component
|
|
26
16
|
create_file(
|
|
@@ -33,15 +23,19 @@ module Ruflet
|
|
|
33
23
|
end
|
|
34
24
|
|
|
35
25
|
def print_scaffold_status
|
|
36
|
-
say "Ruflet
|
|
37
|
-
say "
|
|
38
|
-
say "
|
|
26
|
+
say "Ruflet resource component generated at #{scaffold_component_path}"
|
|
27
|
+
say "Mount it in config/routes.rb:"
|
|
28
|
+
say " mount Ruflet::Rails.web_app(view: #{scaffold_component_class.inspect}), at: \"/#{scaffold_route_segment}\""
|
|
39
29
|
end
|
|
40
30
|
|
|
41
31
|
private
|
|
42
32
|
|
|
43
|
-
def
|
|
44
|
-
|
|
33
|
+
def scaffold_component_class
|
|
34
|
+
"#{model_name.to_s.camelize}Component"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def scaffold_route_segment
|
|
38
|
+
model_name.to_s.underscore.pluralize
|
|
45
39
|
end
|
|
46
40
|
|
|
47
41
|
def scaffold_component_path
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruflet
|
|
4
|
+
module Rails
|
|
5
|
+
# Resolve Rails assets to absolute URLs the Flutter client can load over
|
|
6
|
+
# HTTP, so server-driven UI can show app images:
|
|
7
|
+
#
|
|
8
|
+
# image(src: Ruflet::Rails.asset_url("logo.png"))
|
|
9
|
+
# image(src: Ruflet::Rails.image_url("brand/header.png"), fit: "cover")
|
|
10
|
+
#
|
|
11
|
+
# The *path* comes from the Rails asset pipeline — digested in production
|
|
12
|
+
# (/assets/logo-<digest>.png), plain otherwise — so it survives
|
|
13
|
+
# fingerprinting and CDNs. The *host* is resolved, in order, from:
|
|
14
|
+
#
|
|
15
|
+
# 1. an explicit `host:` argument
|
|
16
|
+
# 2. Ruflet::Rails.config.backend_url
|
|
17
|
+
# 3. the host the client connected on (the live WebSocket request)
|
|
18
|
+
#
|
|
19
|
+
# The client is a separate device (simulator, phone, browser), so a bare
|
|
20
|
+
# "/assets/..." path would not resolve — the URL must be absolute. If Rails
|
|
21
|
+
# already has an asset_host/CDN configured, the pipeline returns an absolute
|
|
22
|
+
# URL and it is used unchanged. A value that is already a full URL passes
|
|
23
|
+
# through untouched.
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# The base URL the Flutter client uses to reach this Rails app — the single
|
|
27
|
+
# source of truth for asset URLs, the build-time RUFLET_URL define and the
|
|
28
|
+
# desktop launcher. A Rails Ruflet app always needs one, so this always
|
|
29
|
+
# resolves to a usable value:
|
|
30
|
+
#
|
|
31
|
+
# 1. an explicit host: argument
|
|
32
|
+
# 2. Ruflet::Rails.config.backend_url (set it in config/initializers/ruflet.rb)
|
|
33
|
+
# 3. the host the client connected on (the live WebSocket request)
|
|
34
|
+
#
|
|
35
|
+
# Returns "" only when none of those are available (e.g. a build with no
|
|
36
|
+
# configured backend_url) — set config.backend_url to cover that case.
|
|
37
|
+
def backend_url(host: nil)
|
|
38
|
+
candidate = host || config.backend_url
|
|
39
|
+
candidate = request_base_url if candidate.to_s.strip.empty?
|
|
40
|
+
candidate.to_s.strip.sub(%r{/+\z}, "")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def asset_url(source, host: nil)
|
|
44
|
+
raw = source.to_s
|
|
45
|
+
return raw if absolute_url?(raw)
|
|
46
|
+
|
|
47
|
+
path = asset_pipeline_path(raw)
|
|
48
|
+
return path if absolute_url?(path)
|
|
49
|
+
|
|
50
|
+
base = backend_url(host: host)
|
|
51
|
+
base.empty? ? path : "#{base}#{path}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Readability alias for image sources — identical resolution.
|
|
55
|
+
def image_url(source, host: nil)
|
|
56
|
+
asset_url(source, host: host)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def asset_pipeline_path(source)
|
|
60
|
+
::ActionController::Base.helpers.asset_path(source)
|
|
61
|
+
rescue StandardError
|
|
62
|
+
source.start_with?("/") ? source : "/#{source}"
|
|
63
|
+
end
|
|
64
|
+
private_class_method :asset_pipeline_path
|
|
65
|
+
|
|
66
|
+
# Derive scheme://host from the live WebSocket request env so the URL points
|
|
67
|
+
# back at the exact host the client reached — the one address guaranteed to
|
|
68
|
+
# be reachable from that device.
|
|
69
|
+
def request_base_url
|
|
70
|
+
env = Protocol::Context.current_env
|
|
71
|
+
return nil unless env
|
|
72
|
+
|
|
73
|
+
host = env["HTTP_X_FORWARDED_HOST"] || env["HTTP_HOST"]
|
|
74
|
+
return nil if host.to_s.strip.empty?
|
|
75
|
+
|
|
76
|
+
scheme = (env["HTTP_X_FORWARDED_PROTO"] || env["rack.url_scheme"] || "http").to_s.split(",").first.to_s.strip
|
|
77
|
+
scheme = "http" if scheme.empty?
|
|
78
|
+
"#{scheme}://#{host}"
|
|
79
|
+
end
|
|
80
|
+
private_class_method :request_base_url
|
|
81
|
+
|
|
82
|
+
def absolute_url?(value)
|
|
83
|
+
!(value.to_s =~ %r{\A[a-z][a-z0-9+.-]*://}i).nil?
|
|
84
|
+
end
|
|
85
|
+
private_class_method :absolute_url?
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruflet
|
|
4
|
+
module Rails
|
|
5
|
+
# Central configuration for ruflet_rails.
|
|
6
|
+
#
|
|
7
|
+
# Mirrors the full ruflet.yaml schema so a Rails app needs only
|
|
8
|
+
# config/initializers/ruflet.rb — no ruflet.yaml on disk.
|
|
9
|
+
#
|
|
10
|
+
# Set in config/initializers/ruflet.rb:
|
|
11
|
+
#
|
|
12
|
+
# Ruflet::Rails.configure do |config|
|
|
13
|
+
# # Runtime / server
|
|
14
|
+
# config.app_file = Rails.root.join("app/views/ruflet/main.rb")
|
|
15
|
+
# config.ws_path = "/ws"
|
|
16
|
+
# config.backend_url = Rails.env.production? ? "https://example.com" : "http://localhost:3000"
|
|
17
|
+
#
|
|
18
|
+
# # Flutter web build output directory
|
|
19
|
+
# config.web_build_dir = Rails.root.join("public/app")
|
|
20
|
+
#
|
|
21
|
+
# # App metadata (ruflet.yaml → app:)
|
|
22
|
+
# config.app_name = "My App"
|
|
23
|
+
#
|
|
24
|
+
# # Services (ruflet.yaml → services:)
|
|
25
|
+
# config.services = []
|
|
26
|
+
#
|
|
27
|
+
# # Assets (ruflet.yaml → assets:)
|
|
28
|
+
# config.splash_screen = Rails.root.join("app/assets/images/splash.png")
|
|
29
|
+
# config.splash_dark = Rails.root.join("app/assets/images/splash_dark.png")
|
|
30
|
+
# config.icon_launcher = Rails.root.join("app/assets/images/icon.png")
|
|
31
|
+
# config.icon_android = Rails.root.join("app/assets/images/icon_android.png")
|
|
32
|
+
# config.icon_ios = Rails.root.join("app/assets/images/icon_ios.png")
|
|
33
|
+
# config.icon_web = Rails.root.join("app/assets/images/icon_web.png")
|
|
34
|
+
# config.icon_windows = Rails.root.join("app/assets/images/icon_windows.png")
|
|
35
|
+
# config.icon_macos = Rails.root.join("app/assets/images/icon_macos.png")
|
|
36
|
+
#
|
|
37
|
+
# # Build options (ruflet.yaml → build:)
|
|
38
|
+
# config.splash_color = "#FFFFFF"
|
|
39
|
+
# config.splash_dark_color = "#000000"
|
|
40
|
+
# config.icon_background = "#FFFFFF"
|
|
41
|
+
# config.theme_color = "#FFFFFF"
|
|
42
|
+
# end
|
|
43
|
+
class Configuration
|
|
44
|
+
# --- Runtime / server ---
|
|
45
|
+
|
|
46
|
+
# Absolute path to the Ruflet app entry-point.
|
|
47
|
+
attr_accessor :app_file
|
|
48
|
+
|
|
49
|
+
# URL path the WebSocket endpoint listens on. Defaults to "/ws".
|
|
50
|
+
attr_accessor :ws_path
|
|
51
|
+
|
|
52
|
+
# Backend base URL. Used as --dart-define=RUFLET_URL at build time
|
|
53
|
+
# and by the desktop launcher. Replaces ruflet.yaml → app.backend_url.
|
|
54
|
+
attr_accessor :backend_url
|
|
55
|
+
|
|
56
|
+
# Absolute directory containing the Flutter web build (index.html + assets).
|
|
57
|
+
attr_accessor :web_build_dir
|
|
58
|
+
|
|
59
|
+
# --- App metadata (ruflet.yaml → app:) ---
|
|
60
|
+
|
|
61
|
+
attr_accessor :app_name
|
|
62
|
+
|
|
63
|
+
# --- Services (ruflet.yaml → services:) ---
|
|
64
|
+
|
|
65
|
+
attr_accessor :services
|
|
66
|
+
|
|
67
|
+
# --- Assets (ruflet.yaml → assets:) ---
|
|
68
|
+
|
|
69
|
+
attr_accessor :splash_screen
|
|
70
|
+
attr_accessor :splash_dark
|
|
71
|
+
attr_accessor :icon_launcher
|
|
72
|
+
attr_accessor :icon_android
|
|
73
|
+
attr_accessor :icon_ios
|
|
74
|
+
attr_accessor :icon_web
|
|
75
|
+
attr_accessor :icon_windows
|
|
76
|
+
attr_accessor :icon_macos
|
|
77
|
+
|
|
78
|
+
# --- Build options (ruflet.yaml → build:) ---
|
|
79
|
+
|
|
80
|
+
attr_accessor :splash_color
|
|
81
|
+
attr_accessor :splash_dark_color
|
|
82
|
+
attr_accessor :icon_background
|
|
83
|
+
attr_accessor :theme_color
|
|
84
|
+
|
|
85
|
+
def initialize
|
|
86
|
+
@ws_path = "/ws"
|
|
87
|
+
@app_file = nil
|
|
88
|
+
@backend_url = nil
|
|
89
|
+
@web_build_dir = nil
|
|
90
|
+
@app_name = nil
|
|
91
|
+
@services = []
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Serialises config to the ruflet.yaml hash structure so the CLI can
|
|
95
|
+
# consume it without a yaml file on disk (written to a temp file by
|
|
96
|
+
# the Railtie's build task).
|
|
97
|
+
def to_ruflet_yaml_hash
|
|
98
|
+
hash = {}
|
|
99
|
+
|
|
100
|
+
app = {}
|
|
101
|
+
app["name"] = @app_name if @app_name
|
|
102
|
+
app["backend_url"] = @backend_url if @backend_url
|
|
103
|
+
hash["app"] = app unless app.empty?
|
|
104
|
+
|
|
105
|
+
hash["services"] = Array(@services)
|
|
106
|
+
|
|
107
|
+
assets = {}
|
|
108
|
+
assets["splash_screen"] = @splash_screen.to_s if @splash_screen
|
|
109
|
+
assets["splash_dark"] = @splash_dark.to_s if @splash_dark
|
|
110
|
+
assets["icon_launcher"] = @icon_launcher.to_s if @icon_launcher
|
|
111
|
+
assets["icon_android"] = @icon_android.to_s if @icon_android
|
|
112
|
+
assets["icon_ios"] = @icon_ios.to_s if @icon_ios
|
|
113
|
+
assets["icon_web"] = @icon_web.to_s if @icon_web
|
|
114
|
+
assets["icon_windows"] = @icon_windows.to_s if @icon_windows
|
|
115
|
+
assets["icon_macos"] = @icon_macos.to_s if @icon_macos
|
|
116
|
+
hash["assets"] = assets unless assets.empty?
|
|
117
|
+
|
|
118
|
+
build = {}
|
|
119
|
+
build["splash_color"] = @splash_color if @splash_color
|
|
120
|
+
build["splash_dark_color"] = @splash_dark_color if @splash_dark_color
|
|
121
|
+
build["icon_background"] = @icon_background if @icon_background
|
|
122
|
+
build["theme_color"] = @theme_color if @theme_color
|
|
123
|
+
hash["build"] = build unless build.empty?
|
|
124
|
+
|
|
125
|
+
hash
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -113,129 +113,25 @@ module Ruflet
|
|
|
113
113
|
File.join("app", "views", "ruflet", "components", names[:plural], "#{names[:singular]}_form.rb")
|
|
114
114
|
end
|
|
115
115
|
|
|
116
|
-
def scaffold_view_path(model_name)
|
|
117
|
-
names = model_names(model_name)
|
|
118
|
-
|
|
119
|
-
File.join("app", "views", "ruflet", "#{names[:plural]}_view.rb")
|
|
120
|
-
end
|
|
121
|
-
|
|
122
116
|
def scaffold_component_path(model_name)
|
|
123
117
|
names = model_names(model_name)
|
|
124
118
|
|
|
125
119
|
File.join("app", "views", "ruflet", "components", names[:plural], "#{names[:singular]}_component.rb")
|
|
126
120
|
end
|
|
127
121
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
component_class = "#{model_class}Component"
|
|
133
|
-
title = names[:title]
|
|
134
|
-
attrs = normalized_form_attributes(attributes)
|
|
135
|
-
resource_fields = scaffold_resource_fields(attrs)
|
|
136
|
-
display_fields = scaffold_display_fields(attrs)
|
|
137
|
-
display_value_cases = scaffold_display_value_cases(attrs)
|
|
138
|
-
|
|
139
|
-
template = <<~RUBY
|
|
140
|
-
# frozen_string_literal: true
|
|
141
|
-
|
|
142
|
-
require "ruflet_rails"
|
|
143
|
-
require_relative "components/#{names[:plural]}/#{names[:singular]}_component"
|
|
144
|
-
|
|
145
|
-
class #{view_class} < Ruflet::Rails::ResourceView
|
|
146
|
-
route #{("/" + names[:plural]).inspect}
|
|
147
|
-
|
|
148
|
-
def render
|
|
149
|
-
page.title = resource_title
|
|
150
|
-
render_index
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
private
|
|
154
|
-
|
|
155
|
-
def model_class
|
|
156
|
-
#{model_class}
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def resource_title
|
|
160
|
-
#{title.inspect}
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
def singular_title
|
|
164
|
-
model_class.model_name.human.titleize
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
def records
|
|
168
|
-
scope = model_class.respond_to?(:limit) ? model_class.limit(50) : model_class.all
|
|
169
|
-
scope.respond_to?(:limit) ? scope.limit(50) : scope.to_a.first(50)
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def render_index
|
|
173
|
-
page.views = []
|
|
174
|
-
page.add(component.render)
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
def render_show(record)
|
|
178
|
-
page.views = []
|
|
179
|
-
page.add(component.show(record))
|
|
180
|
-
page.update
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
def component
|
|
184
|
-
@component ||= #{component_class}.new(page, controller: self)
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
def show_record(record)
|
|
188
|
-
render_show(record)
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
def save_record(record, attributes, dialog)
|
|
192
|
-
if record.update(attributes)
|
|
193
|
-
close_dialog(dialog)
|
|
194
|
-
render_index
|
|
195
|
-
show_snackbar("\#{singular_title} saved")
|
|
196
|
-
else
|
|
197
|
-
show_errors(record)
|
|
198
|
-
end
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
def destroy_record(record, dialog)
|
|
202
|
-
record.destroy!
|
|
203
|
-
close_dialog(dialog)
|
|
204
|
-
render_index
|
|
205
|
-
show_snackbar("\#{singular_title} deleted")
|
|
206
|
-
rescue StandardError => e
|
|
207
|
-
show_snackbar(e.message)
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
def resource_fields
|
|
211
|
-
#{resource_fields}
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
def display_fields
|
|
215
|
-
#{display_fields}
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
def display_value(record, field)
|
|
219
|
-
case field
|
|
220
|
-
__DISPLAY_VALUE_CASES__
|
|
221
|
-
else
|
|
222
|
-
record.public_send(field).to_s
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
def primary_label(record)
|
|
227
|
-
field = display_fields.first
|
|
228
|
-
field ? display_value(record, field) : "##\#{record_id(record)}"
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
def secondary_label(record)
|
|
232
|
-
field = display_fields[1]
|
|
233
|
-
field ? display_value(record, field) : nil
|
|
234
|
-
end
|
|
122
|
+
# A `case` with no `when` clause is a syntax error, so models without
|
|
123
|
+
# date/time attributes get the plain fallback body instead.
|
|
124
|
+
def scaffold_display_value_body(display_value_cases, indent)
|
|
125
|
+
return "#{indent}record.public_send(field).to_s" if display_value_cases.to_s.strip.empty?
|
|
235
126
|
|
|
127
|
+
reindented_cases = display_value_cases.gsub(/^ /, indent)
|
|
128
|
+
<<~RUBY.chomp.gsub(/^/, indent).gsub(/^#{Regexp.escape(indent)}__CASES__$/, reindented_cases)
|
|
129
|
+
case field
|
|
130
|
+
__CASES__
|
|
131
|
+
else
|
|
132
|
+
record.public_send(field).to_s
|
|
236
133
|
end
|
|
237
134
|
RUBY
|
|
238
|
-
template.gsub(/^[ \t]*__DISPLAY_VALUE_CASES__$/, display_value_cases)
|
|
239
135
|
end
|
|
240
136
|
|
|
241
137
|
def scaffold_component_template(model_name:, attributes: [])
|
|
@@ -253,6 +149,15 @@ module Ruflet
|
|
|
253
149
|
require "date"
|
|
254
150
|
require "ruflet_rails"
|
|
255
151
|
|
|
152
|
+
# The model (#{model_class}) is inferred from the class name and the route
|
|
153
|
+
# ("/#{names[:plural]}") is declared in config/routes.rb. This file is YOURS:
|
|
154
|
+
# the whole CRUD UI (index table, detail screen, create/edit form) and the
|
|
155
|
+
# database calls (#{model_class}#update, #destroy!, .new) are explicit below
|
|
156
|
+
# so you can change the UI or logic however you like. The base class only
|
|
157
|
+
# provides reusable helpers: record loading, field inference, dialog
|
|
158
|
+
# open/close, the date/time picker value helpers, and refresh.
|
|
159
|
+
#
|
|
160
|
+
# The same component renders on web and on mobile/desktop.
|
|
256
161
|
class #{component_class} < Ruflet::Rails::ResourceComponent
|
|
257
162
|
def render
|
|
258
163
|
safe_area(
|
|
@@ -401,6 +306,7 @@ module Ruflet
|
|
|
401
306
|
width: dialog_width,
|
|
402
307
|
content: column(
|
|
403
308
|
spacing: 8,
|
|
309
|
+
horizontal_alignment: "stretch",
|
|
404
310
|
children: [
|
|
405
311
|
#{control_list}
|
|
406
312
|
]
|
|
@@ -409,7 +315,14 @@ module Ruflet
|
|
|
409
315
|
actions: [
|
|
410
316
|
text_button(content: text("Cancel"), on_click: ->(_event) { close_dialog(dialog) }),
|
|
411
317
|
filled_button(content: text("Save"), on_click: ->(_event) {
|
|
412
|
-
|
|
318
|
+
# Persist with the model (this is your code — change it freely).
|
|
319
|
+
if record.update(attributes.call)
|
|
320
|
+
close_dialog(dialog)
|
|
321
|
+
refresh
|
|
322
|
+
show_snackbar("\#{singular_title} saved")
|
|
323
|
+
else
|
|
324
|
+
show_errors(record)
|
|
325
|
+
end
|
|
413
326
|
})
|
|
414
327
|
],
|
|
415
328
|
actions_alignment: "end"
|
|
@@ -426,7 +339,13 @@ module Ruflet
|
|
|
426
339
|
content: text("Permanently remove \#{singular_title} #\#{record_id(record)}?", no_wrap: false),
|
|
427
340
|
actions: [
|
|
428
341
|
text_button(content: text("Cancel"), on_click: ->(_event) { close_dialog(dialog) }),
|
|
429
|
-
filled_button(content: text("Delete"), on_click: ->(_event) {
|
|
342
|
+
filled_button(content: text("Delete"), on_click: ->(_event) {
|
|
343
|
+
# Destroy with the model (this is your code — change it freely).
|
|
344
|
+
record.destroy!
|
|
345
|
+
close_dialog(dialog)
|
|
346
|
+
refresh
|
|
347
|
+
show_snackbar("\#{singular_title} deleted")
|
|
348
|
+
})
|
|
430
349
|
],
|
|
431
350
|
actions_alignment: "end"
|
|
432
351
|
)
|
|
@@ -512,7 +431,7 @@ module Ruflet
|
|
|
512
431
|
help_text: #{label.inspect},
|
|
513
432
|
on_change: ->(_event) do
|
|
514
433
|
close_dialogs(#{control})
|
|
515
|
-
page.update(#{display_control}, value: date_display_value(#{control}.
|
|
434
|
+
page.update(#{display_control}, value: date_display_value(#{control}.value))
|
|
516
435
|
end
|
|
517
436
|
)
|
|
518
437
|
#{control}_field = column(
|
|
@@ -539,7 +458,7 @@ module Ruflet
|
|
|
539
458
|
help_text: #{label.inspect},
|
|
540
459
|
on_change: ->(_event) do
|
|
541
460
|
close_dialogs(#{control})
|
|
542
|
-
page.update(#{display_control}, value: time_display_value(#{control}.
|
|
461
|
+
page.update(#{display_control}, value: time_display_value(#{control}.value))
|
|
543
462
|
end
|
|
544
463
|
)
|
|
545
464
|
#{control}_field = column(
|
|
@@ -569,7 +488,7 @@ module Ruflet
|
|
|
569
488
|
close_dialogs(#{control})
|
|
570
489
|
page.update(
|
|
571
490
|
#{display_control},
|
|
572
|
-
value: date_range_display_value(#{control}.
|
|
491
|
+
value: date_range_display_value(#{control}.start_value, #{control}.end_value)
|
|
573
492
|
)
|
|
574
493
|
end
|
|
575
494
|
)
|
|
@@ -603,13 +522,13 @@ module Ruflet
|
|
|
603
522
|
value =
|
|
604
523
|
case type
|
|
605
524
|
when "boolean"
|
|
606
|
-
"!!#{control}.
|
|
525
|
+
"!!#{control}.value"
|
|
607
526
|
when "date"
|
|
608
|
-
"#{control}.
|
|
527
|
+
"#{control}.value.to_s.split(\"T\", 2).first"
|
|
609
528
|
when "date_range", "daterange"
|
|
610
|
-
"Range.new(Date.parse(#{control}.
|
|
529
|
+
"Range.new(Date.parse(#{control}.start_value.to_s), Date.parse(#{control}.end_value.to_s))"
|
|
611
530
|
else
|
|
612
|
-
"#{control}.
|
|
531
|
+
"#{control}.value.to_s"
|
|
613
532
|
end
|
|
614
533
|
|
|
615
534
|
"#{name.inspect} => #{value}"
|
|
@@ -648,7 +567,7 @@ module Ruflet
|
|
|
648
567
|
spacing: 12,
|
|
649
568
|
children: [
|
|
650
569
|
text(title, size: 24, weight: "bold"),
|
|
651
|
-
column(spacing: 8, children: ruflet_form_controls(fields)),
|
|
570
|
+
column(spacing: 8, horizontal_alignment: "stretch", children: ruflet_form_controls(fields)),
|
|
652
571
|
row(
|
|
653
572
|
spacing: 8,
|
|
654
573
|
children: [
|
|
@@ -827,17 +746,55 @@ module Ruflet
|
|
|
827
746
|
value
|
|
828
747
|
end
|
|
829
748
|
|
|
830
|
-
def build_args_for_platform(platform)
|
|
749
|
+
def build_args_for_platform(platform, ruflet_url: nil)
|
|
831
750
|
normalized = normalize_build_platform(platform)
|
|
832
751
|
return [] if normalized.to_s.empty?
|
|
833
752
|
|
|
834
|
-
[normalized]
|
|
753
|
+
args = [normalized]
|
|
754
|
+
args += ["--dart-define", "RUFLET_URL=#{ruflet_url}"] if normalized == "web" && ruflet_url.to_s.strip != ""
|
|
755
|
+
args
|
|
835
756
|
end
|
|
836
757
|
|
|
837
758
|
def default_entrypoint_path
|
|
838
759
|
File.join("app", "views", "ruflet", "main.rb")
|
|
839
760
|
end
|
|
840
761
|
|
|
762
|
+
def initializer_template(entrypoint: default_entrypoint_path, ws_path: "/ws")
|
|
763
|
+
<<~RUBY
|
|
764
|
+
# frozen_string_literal: true
|
|
765
|
+
|
|
766
|
+
Ruflet::Rails.configure do |config|
|
|
767
|
+
# Ruflet app entry-point. Auto-mounts a WebSocket endpoint at ws_path —
|
|
768
|
+
# no explicit route needed in config/routes.rb.
|
|
769
|
+
config.app_file = Rails.root.join(#{entrypoint.inspect})
|
|
770
|
+
|
|
771
|
+
# URL path the WebSocket endpoint listens on (default: "/ws").
|
|
772
|
+
config.ws_path = #{ws_path.inspect}
|
|
773
|
+
|
|
774
|
+
# Base URL the Flutter client uses to reach this Rails app. Always
|
|
775
|
+
# required: it backs asset URLs (Ruflet::Rails.asset_url), the
|
|
776
|
+
# build-time RUFLET_URL define, and the desktop launcher. At runtime
|
|
777
|
+
# it can fall back to the connecting host, but a build has no request,
|
|
778
|
+
# so set it here. Point it at a LAN IP (not localhost) to test on a
|
|
779
|
+
# real device.
|
|
780
|
+
config.backend_url = ENV.fetch("RUFLET_BACKEND_URL") do
|
|
781
|
+
Rails.env.production? ? "https://example.com" : "http://localhost:3000"
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
# Directory the Flutter web build is served from. Defaults to
|
|
785
|
+
# Rails.root/build/web (where `rake ruflet:build[web]` outputs).
|
|
786
|
+
# Must stay OUTSIDE public/, or Rails would serve it statically and
|
|
787
|
+
# expose the app at a path no route declares.
|
|
788
|
+
# config.web_build_dir = Rails.root.join("build", "web")
|
|
789
|
+
end
|
|
790
|
+
RUBY
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
def initializer_path
|
|
794
|
+
File.join("config", "initializers", "ruflet.rb")
|
|
795
|
+
end
|
|
796
|
+
|
|
797
|
+
# Kept for backward compatibility with apps that use manual mount.
|
|
841
798
|
def route_snippet(entrypoint: default_entrypoint_path, mount_path: "/ws", helper: "app")
|
|
842
799
|
%(match "#{mount_path}", to: Ruflet::Rails.#{helper}(Rails.root.join("#{entrypoint}")), via: :all)
|
|
843
800
|
end
|