solidcrud 0.1.0

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.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +1285 -0
  4. data/app/assets/javascripts/controllers/dashboard_controller.js +96 -0
  5. data/app/assets/javascripts/controllers/modal_controller.js +217 -0
  6. data/app/assets/javascripts/controllers/navigation_controller.js +117 -0
  7. data/app/assets/javascripts/controllers/notification_controller.js +85 -0
  8. data/app/assets/javascripts/controllers/search_controller.js +189 -0
  9. data/app/assets/javascripts/controllers/table_controller.js +272 -0
  10. data/app/assets/javascripts/solidcrud/application.js +9475 -0
  11. data/app/assets/stylesheets/solidcrud/_components.scss +267 -0
  12. data/app/assets/stylesheets/solidcrud/_forms.scss +69 -0
  13. data/app/assets/stylesheets/solidcrud/_layout.scss +149 -0
  14. data/app/assets/stylesheets/solidcrud/_tables.scss +90 -0
  15. data/app/assets/stylesheets/solidcrud/_variables.scss +21 -0
  16. data/app/assets/stylesheets/solidcrud/application.css +10 -0
  17. data/app/assets/stylesheets/solidcrud/application.css.map +1 -0
  18. data/app/assets/stylesheets/solidcrud/application.scss +10 -0
  19. data/app/assets/stylesheets/solidcrud/temp.css.map +1 -0
  20. data/app/assets/stylesheets/solidcrud/temp2.css.map +1 -0
  21. data/app/assets/stylesheets/solidcrud/webfonts/fa-brands-400.ttf +0 -0
  22. data/app/assets/stylesheets/solidcrud/webfonts/fa-brands-400.woff2 +0 -0
  23. data/app/assets/stylesheets/solidcrud/webfonts/fa-regular-400.ttf +0 -0
  24. data/app/assets/stylesheets/solidcrud/webfonts/fa-regular-400.woff2 +0 -0
  25. data/app/assets/stylesheets/solidcrud/webfonts/fa-solid-900.ttf +0 -0
  26. data/app/assets/stylesheets/solidcrud/webfonts/fa-solid-900.woff2 +0 -0
  27. data/app/assets/stylesheets/solidcrud/webfonts/fa-v4compatibility.ttf +0 -0
  28. data/app/assets/stylesheets/solidcrud/webfonts/fa-v4compatibility.woff2 +0 -0
  29. data/app/assets/stylesheets/webfonts/fa-brands-400.ttf +0 -0
  30. data/app/assets/stylesheets/webfonts/fa-brands-400.woff2 +0 -0
  31. data/app/assets/stylesheets/webfonts/fa-regular-400.ttf +0 -0
  32. data/app/assets/stylesheets/webfonts/fa-regular-400.woff2 +0 -0
  33. data/app/assets/stylesheets/webfonts/fa-solid-900.ttf +0 -0
  34. data/app/assets/stylesheets/webfonts/fa-solid-900.woff2 +0 -0
  35. data/app/assets/stylesheets/webfonts/fa-v4compatibility.ttf +0 -0
  36. data/app/assets/stylesheets/webfonts/fa-v4compatibility.woff2 +0 -0
  37. data/app/controllers/solidcrud/admin_controller.rb +215 -0
  38. data/app/controllers/solidcrud/application_controller.rb +19 -0
  39. data/app/controllers/solidcrud/assets_controller.rb +59 -0
  40. data/app/controllers/solidcrud/sessions_controller.rb +84 -0
  41. data/app/helpers/solidcrud/application_helper.rb +153 -0
  42. data/app/javascript/solidcrud/application.js +14 -0
  43. data/app/javascript/solidcrud/controllers/crud_controller.js +64 -0
  44. data/app/javascript/solidcrud/controllers/index.js +33 -0
  45. data/app/views/layouts/solidcrud/application.html.erb +70 -0
  46. data/app/views/solidcrud/admin/edit.html.erb +294 -0
  47. data/app/views/solidcrud/admin/index.html.erb +128 -0
  48. data/app/views/solidcrud/admin/model.html.erb +353 -0
  49. data/app/views/solidcrud/admin/new.html.erb +275 -0
  50. data/app/views/solidcrud/admin/shared/_dashboard_stats.html.erb +49 -0
  51. data/app/views/solidcrud/admin/shared/_edit_form_sidebar.html.erb +9 -0
  52. data/app/views/solidcrud/admin/shared/_flash_messages.html.erb +27 -0
  53. data/app/views/solidcrud/admin/shared/_full_sidebar.html.erb +56 -0
  54. data/app/views/solidcrud/admin/shared/_modal.html.erb +45 -0
  55. data/app/views/solidcrud/admin/shared/_new_form_sidebar.html.erb +6 -0
  56. data/app/views/solidcrud/admin/shared/_record_row.html.erb +35 -0
  57. data/app/views/solidcrud/admin/shared/_records_table.html.erb +85 -0
  58. data/app/views/solidcrud/sessions/new.html.erb +262 -0
  59. data/config/routes.rb +24 -0
  60. data/lib/generators/solidcrud/install/install_generator.rb +21 -0
  61. data/lib/generators/solidcrud/install/templates/INSTALL.md +80 -0
  62. data/lib/generators/solidcrud/install/templates/solidcrud.rb +31 -0
  63. data/lib/generators/solidcrud/install_generator.rb +17 -0
  64. data/lib/generators/solidcrud/templates/solidcrud.rb +4 -0
  65. data/lib/solidcrud/authentication.rb +143 -0
  66. data/lib/solidcrud/configuration.rb +64 -0
  67. data/lib/solidcrud/engine.rb +49 -0
  68. data/lib/solidcrud/version.rb +5 -0
  69. data/lib/solidcrud.rb +10 -0
  70. metadata +177 -0
@@ -0,0 +1,215 @@
1
+ module Solidcrud
2
+ class AdminController < ApplicationController
3
+ before_action :set_models
4
+
5
+ def index
6
+ @models ||= []
7
+ respond_to do |format|
8
+ format.html
9
+ format.turbo_stream
10
+ end
11
+ end
12
+
13
+ def dashboard_stats
14
+ @models_count = @models&.count || 0
15
+ @rails_version = Rails.version
16
+ @database_adapter = ActiveRecord::Base.connection.adapter_name
17
+
18
+ respond_to do |format|
19
+ format.turbo_stream do
20
+ render turbo_stream: turbo_stream.replace("dashboard-stats",
21
+ partial: "solidcrud/admin/shared/dashboard_stats",
22
+ locals: { models_count: @models_count, rails_version: @rails_version, database_adapter: @database_adapter }
23
+ )
24
+ end
25
+ end
26
+ end
27
+
28
+ def model
29
+ @model_name = params[:model]
30
+ @model_klass = model_class(@model_name)
31
+
32
+ if @model_klass.nil?
33
+ raise ActiveRecord::RecordNotFound, "Model '#{@model_name}' not found"
34
+ end
35
+
36
+ @records = @model_klass.all
37
+
38
+ # Search functionality
39
+ if params[:search].present?
40
+ search_term = "%#{params[:search]}%"
41
+ searchable_columns = @model_klass.column_names.select do |col|
42
+ column_type = @model_klass.columns_hash[col].type
43
+ [:string, :text].include?(column_type)
44
+ end
45
+
46
+ if searchable_columns.any?
47
+ # Use ILIKE for PostgreSQL, LIKE for others (case-insensitive search)
48
+ operator = ActiveRecord::Base.connection.adapter_name.downcase.include?('postgresql') ? 'ILIKE' : 'LIKE'
49
+ search_conditions = searchable_columns.map { |col| "LOWER(#{col}) LIKE LOWER(?)" }.join(' OR ')
50
+ search_params = [search_term] * searchable_columns.length
51
+ @records = @records.where(search_conditions, *search_params)
52
+ end
53
+ end
54
+
55
+ # Sort functionality
56
+ if params[:sort].present? && @model_klass.column_names.include?(params[:sort])
57
+ direction = params[:direction] == 'desc' ? 'DESC' : 'ASC'
58
+ @records = @records.order("#{params[:sort]} #{direction}")
59
+ else
60
+ @records = @records.order(id: :desc)
61
+ end
62
+
63
+ # Pagination
64
+ @page = (params[:page] || 1).to_i
65
+ @per_page = 25
66
+ @total_count = @records.count
67
+ @records = @records.offset((@page - 1) * @per_page).limit(@per_page)
68
+
69
+ @total_pages = (@total_count.to_f / @per_page).ceil
70
+
71
+ respond_to do |format|
72
+ format.html
73
+ format.turbo_stream
74
+ end
75
+ end
76
+
77
+ def new
78
+ @model_name = params[:model]
79
+ @model_klass = model_class(@model_name)
80
+ @record = @model_klass.new
81
+
82
+ respond_to do |format|
83
+ format.html
84
+ format.turbo_stream
85
+ end
86
+ end
87
+
88
+ def create
89
+ @model_name = params[:model]
90
+ @model_klass = model_class(@model_name)
91
+ @record = @model_klass.new(record_params)
92
+
93
+ respond_to do |format|
94
+ if @record.save
95
+ format.html { redirect_to solidcrud.admin_model_path(@model_name), notice: 'Created!' }
96
+ format.turbo_stream do
97
+ render turbo_stream: [
98
+ turbo_stream.replace("flash-messages", partial: "solidcrud/admin/shared/flash_messages", locals: { notice: 'Created!' }),
99
+ turbo_stream.replace("records-table", partial: "solidcrud/admin/shared/records_table", locals: { records: @model_klass.all.order(id: :desc).limit(25), model_name: @model_name, model_klass: @model_klass }),
100
+ turbo_stream.append("notification-container",
101
+ '<div class="notification-trigger" data-type="success" data-message="Record created successfully!" data-auto-dismiss="true" style="display: none;"></div>')
102
+ ]
103
+ end
104
+ else
105
+ format.html { render :new }
106
+ format.turbo_stream { render turbo_stream: turbo_stream.replace("new-form", partial: "solidcrud/admin/shared/form", locals: { record: @record, model_name: @model_name, model_klass: @model_klass }) }
107
+ end
108
+ end
109
+ end
110
+
111
+ def edit
112
+ @model_name = params[:model]
113
+ @model_klass = model_class(@model_name)
114
+ @record = @model_klass.find(params[:id])
115
+
116
+ respond_to do |format|
117
+ format.html
118
+ format.turbo_stream
119
+ end
120
+ end
121
+
122
+ def update
123
+ @model_name = params[:model]
124
+ @model_klass = model_class(@model_name)
125
+ @record = @model_klass.find(params[:id])
126
+
127
+ respond_to do |format|
128
+ if @record.update(record_params)
129
+ format.html { redirect_to solidcrud.admin_model_path(@model_name), notice: 'Updated!' }
130
+ format.turbo_stream do
131
+ render turbo_stream: [
132
+ turbo_stream.replace("flash-messages", partial: "solidcrud/admin/shared/flash_messages", locals: { notice: 'Updated!' }),
133
+ turbo_stream.replace("record-#{@record.id}", partial: "solidcrud/admin/shared/record_row", locals: { record: @record, model_name: @model_name, model_klass: @model_klass }),
134
+ turbo_stream.append("notification-container",
135
+ '<div class="notification-trigger" data-type="success" data-message="Record updated successfully!" data-auto-dismiss="true" style="display: none;"></div>')
136
+ ]
137
+ end
138
+ else
139
+ format.html { render :edit }
140
+ format.turbo_stream { render turbo_stream: turbo_stream.replace("edit-form", partial: "solidcrud/admin/shared/form", locals: { record: @record, model_name: @model_name, model_klass: @model_klass }) }
141
+ end
142
+ end
143
+ end
144
+
145
+ def destroy
146
+ @model_name = params[:model]
147
+ @model_klass = model_class(@model_name)
148
+ @record = @model_klass.find(params[:id])
149
+ @record.destroy
150
+
151
+ respond_to do |format|
152
+ format.html { redirect_to solidcrud.admin_model_path(@model_name), notice: 'Deleted!' }
153
+ format.turbo_stream do
154
+ render turbo_stream: [
155
+ turbo_stream.remove("record-#{params[:id]}"),
156
+ turbo_stream.replace("flash-messages", partial: "solidcrud/admin/shared/flash_messages", locals: { notice: 'Deleted!' }),
157
+ turbo_stream.append("notification-container",
158
+ '<div class="notification-trigger" data-type="success" data-message="Record deleted successfully!" data-auto-dismiss="true" style="display: none;"></div>')
159
+ ]
160
+ end
161
+ end
162
+ end
163
+
164
+ private
165
+
166
+ def set_models
167
+ # Force load all models from app/models directory
168
+ begin
169
+ Dir[Rails.root.join('app/models/**/*.rb')].each do |file|
170
+ require_dependency file
171
+ end
172
+ rescue => e
173
+ Rails.logger.error "Error loading models: #{e.message}"
174
+ end
175
+
176
+ exclude = Solidcrud.config&.models_exclude || []
177
+ @models = ActiveRecord::Base.descendants.reject { |m| m.abstract_class? || exclude.include?(m.name) }
178
+ rescue => e
179
+ Rails.logger.error "Error setting models: #{e.message}"
180
+ @models = []
181
+ end
182
+
183
+ def model_class(name)
184
+ return nil if name.blank?
185
+
186
+ # Try exact match first
187
+ model = @models.find { |m| m.name == name }
188
+ return model if model
189
+
190
+ # Try case-insensitive match
191
+ model = @models.find { |m| m.name.downcase == name.downcase }
192
+ return model if model
193
+
194
+ # Try singular/plural variations
195
+ singular_name = name.singularize
196
+ plural_name = name.pluralize
197
+
198
+ model = @models.find { |m| m.name == singular_name || m.name == plural_name }
199
+ return model if model
200
+
201
+ # Try camelize/underscore variations
202
+ camelized = name.camelize
203
+ underscored = name.underscore
204
+
205
+ model = @models.find { |m| m.name == camelized || m.name == underscored }
206
+ return model if model
207
+
208
+ nil
209
+ end
210
+
211
+ def record_params
212
+ params.require(@model_name.underscore.to_sym).permit(@model_klass.column_names - ["id", "created_at", "updated_at"])
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,19 @@
1
+ module Solidcrud
2
+ class ApplicationController < ActionController::Base
3
+ include ActionController::Flash
4
+ include Solidcrud::Authentication
5
+
6
+ layout 'solidcrud/application'
7
+
8
+ # Override flash method to work around Rails 7.2 delegation issue
9
+ def flash
10
+ @_flash ||= super
11
+ rescue NoMethodError
12
+ # Fallback for Rails 7.2 compatibility
13
+ session['flash'] ||= ActionDispatch::Flash::FlashHash.new
14
+ end
15
+
16
+ # Include flash helper methods for Rails engine compatibility
17
+ helper_method :flash
18
+ end
19
+ end
@@ -0,0 +1,59 @@
1
+ module Solidcrud
2
+ class AssetsController < Solidcrud::ApplicationController
3
+ skip_before_action :verify_authenticity_token
4
+
5
+ def stylesheet
6
+ filename = params[:filename]
7
+ # Ensure we have the .css extension
8
+ filename = "#{filename}.css" unless filename.end_with?('.css')
9
+ asset_path = Solidcrud::Engine.root.join('app', 'assets', 'stylesheets', 'solidcrud', filename)
10
+
11
+ if File.exist?(asset_path)
12
+ render plain: File.read(asset_path), content_type: 'text/css'
13
+ else
14
+ head :not_found
15
+ end
16
+ end
17
+
18
+ def javascript
19
+ filename = params[:filename]
20
+ # Ensure we have the .js extension
21
+ filename = "#{filename}.js" unless filename.end_with?('.js')
22
+ asset_path = Solidcrud::Engine.root.join('app', 'assets', 'javascripts', 'solidcrud', filename)
23
+
24
+ if File.exist?(asset_path)
25
+ render plain: File.read(asset_path), content_type: 'application/javascript'
26
+ else
27
+ head :not_found
28
+ end
29
+ end
30
+
31
+ def webfont
32
+ filename = params[:filename]
33
+ # Add format extension if present
34
+ filename = "#{filename}.#{params[:format]}" if params[:format].present?
35
+
36
+ asset_path = Solidcrud::Engine.root.join('app', 'assets', 'stylesheets', 'solidcrud', 'webfonts', filename)
37
+
38
+ if File.exist?(asset_path)
39
+ # Determine content type based on file extension
40
+ content_type = case File.extname(filename).downcase
41
+ when '.woff2'
42
+ 'font/woff2'
43
+ when '.woff'
44
+ 'font/woff'
45
+ when '.ttf'
46
+ 'font/ttf'
47
+ when '.otf'
48
+ 'font/otf'
49
+ else
50
+ 'application/octet-stream'
51
+ end
52
+
53
+ send_file asset_path, type: content_type, disposition: 'inline'
54
+ else
55
+ head :not_found
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,84 @@
1
+ module Solidcrud
2
+ class SessionsController < ApplicationController
3
+ skip_before_action :authenticate_solidcrud_user!, only: [:new, :create]
4
+ skip_before_action :verify_authenticity_token, only: [:destroy]
5
+
6
+ def new
7
+ # If user is already signed in, redirect to admin
8
+ if solidcrud_user_signed_in?
9
+ redirect_to solidcrud.root_path
10
+ return
11
+ end
12
+
13
+ # Only show login form for certain auth types
14
+ case Solidcrud.config.auth_type
15
+ when :basic_auth, :custom
16
+ # Show login form for basic auth and custom auth
17
+ when :devise
18
+ # For Devise, redirect to Devise login if not signed in
19
+ unless devise_user_signed_in?
20
+ redirect_to new_session_path(Solidcrud.config.devise_scope), alert: "Please login first"
21
+ return
22
+ end
23
+ when :jwt
24
+ # JWT auth doesn't need a login form - tokens are passed in headers
25
+ redirect_to solidcrud.root_path, alert: "JWT authentication required. Please include Bearer token in Authorization header."
26
+ return
27
+ else
28
+ redirect_to solidcrud.root_path, alert: "Authentication not configured"
29
+ return
30
+ end
31
+ end
32
+
33
+ def create
34
+ case Solidcrud.config.auth_type
35
+ when :basic_auth
36
+ handle_basic_auth_login
37
+ when :custom
38
+ handle_custom_auth_login
39
+ when :devise
40
+ # Devise handles its own login
41
+ redirect_to solidcrud.root_path, alert: "Use Devise login instead"
42
+ when :jwt
43
+ redirect_to solidcrud.root_path, alert: "JWT authentication doesn't use form login"
44
+ else
45
+ redirect_to solidcrud.root_path, alert: "Authentication method not implemented"
46
+ end
47
+ end
48
+
49
+ def destroy
50
+ sign_out_solidcrud_user
51
+ redirect_to solidcrud.root_path, notice: "Successfully signed out"
52
+ end
53
+
54
+ private
55
+
56
+ def handle_basic_auth_login
57
+ username = params[:username]
58
+ password = params[:password]
59
+
60
+ if username == Solidcrud.config.basic_auth_username &&
61
+ password == Solidcrud.config.basic_auth_password
62
+ sign_in_solidcrud_user
63
+ redirect_to solidcrud.root_path, notice: "Successfully signed in"
64
+ else
65
+ flash.now[:alert] = "Invalid credentials"
66
+ render :new
67
+ end
68
+ end
69
+
70
+ def handle_custom_auth_login
71
+ if respond_to?(:custom_authenticate, true)
72
+ if custom_authenticate(params[:username], params[:password])
73
+ sign_in_solidcrud_user
74
+ redirect_to solidcrud.root_path, notice: "Successfully signed in"
75
+ else
76
+ flash.now[:alert] = "Invalid credentials"
77
+ render :new
78
+ end
79
+ else
80
+ redirect_to solidcrud.root_path, alert: "Custom authentication method not implemented"
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,153 @@
1
+ module Solidcrud
2
+ module ApplicationHelper
3
+ def column_icon(column_name)
4
+ case column_name.to_s
5
+ when 'id'
6
+ '<i class="fas fa-key"></i>'.html_safe
7
+ when /name|title/
8
+ '<i class="fas fa-tag"></i>'.html_safe
9
+ when /email/
10
+ '<i class="fas fa-envelope"></i>'.html_safe
11
+ when /phone/
12
+ '<i class="fas fa-phone"></i>'.html_safe
13
+ when /address/
14
+ '<i class="fas fa-map-marker-alt"></i>'.html_safe
15
+ when /date|time/
16
+ '<i class="fas fa-calendar"></i>'.html_safe
17
+ when /created_at|updated_at/
18
+ '<i class="fas fa-clock"></i>'.html_safe
19
+ when /active|enabled|visible/
20
+ '<i class="fas fa-check-circle"></i>'.html_safe
21
+ when /count|amount|price|quantity/
22
+ '<i class="fas fa-hashtag"></i>'.html_safe
23
+ when /url|link/
24
+ '<i class="fas fa-link"></i>'.html_safe
25
+ when /description|content|body|text/
26
+ '<i class="fas fa-file-alt"></i>'.html_safe
27
+ else
28
+ '<i class="fas fa-circle"></i>'.html_safe
29
+ end
30
+ end
31
+
32
+ def column_visible?(column_name, model_name)
33
+ # This will be handled by JavaScript, but we provide a default
34
+ # All columns are visible by default
35
+ true
36
+ end
37
+
38
+ def format_value(value, column_name, model_class)
39
+ return content_tag(:span, 'NULL', class: 'text-slate-400 italic font-medium') if value.nil?
40
+
41
+ column = model_class.columns_hash[column_name.to_s]
42
+
43
+ case column&.type
44
+ when :boolean
45
+ if value
46
+ content_tag(:span, class: 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800') do
47
+ '<i class="fas fa-check mr-1"></i> Yes'.html_safe
48
+ end
49
+ else
50
+ content_tag(:span, class: 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-slate-100 text-slate-800') do
51
+ '<i class="fas fa-times mr-1"></i> No'.html_safe
52
+ end
53
+ end
54
+ when :datetime, :timestamp
55
+ content_tag(:span, title: value.strftime('%Y-%m-%d %H:%M:%S %Z'), class: 'text-slate-900') do
56
+ time_ago_in_words(value) + ' ago'
57
+ end
58
+ when :date
59
+ value.strftime('%B %d, %Y')
60
+ when :text
61
+ truncate(value.to_s, length: 100)
62
+ when :integer, :decimal, :float
63
+ if column_name.to_s.include?('price') || column_name.to_s.include?('amount')
64
+ number_to_currency(value)
65
+ else
66
+ number_with_delimiter(value)
67
+ end
68
+ else
69
+ if value.to_s.length > 50
70
+ content_tag(:span, title: value.to_s, class: 'text-slate-900') do
71
+ truncate(value.to_s, length: 50)
72
+ end
73
+ else
74
+ h(value.to_s)
75
+ end
76
+ end
77
+ rescue
78
+ h(value.to_s)
79
+ end
80
+
81
+ def render_form_field(form, column_name, model_class)
82
+ column = model_class.columns_hash[column_name.to_s]
83
+
84
+ case column&.type
85
+ when :boolean
86
+ form.check_box column_name, class: 'w-4 h-4 text-blue-600 bg-slate-100 border-slate-300 rounded focus:ring-blue-500 focus:ring-2'
87
+ when :text
88
+ form.text_area column_name, class: 'w-full px-4 py-3 border border-slate-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-slate-50 focus:bg-white resize-vertical', rows: 4
89
+ when :date
90
+ form.date_field column_name, class: 'w-full px-4 py-3 border border-slate-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-slate-50 focus:bg-white'
91
+ when :datetime, :timestamp
92
+ form.datetime_local_field column_name, class: 'w-full px-4 py-3 border border-slate-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-slate-50 focus:bg-white'
93
+ when :integer
94
+ if column_name.to_s.ends_with?('_id') && column_name.to_s != 'id'
95
+ # Foreign key field
96
+ association_name = column_name.to_s.gsub('_id', '')
97
+ if model_class.reflect_on_association(association_name)
98
+ associated_model = model_class.reflect_on_association(association_name).klass
99
+ options = associated_model.all.map { |r| [display_name_for_model(r), r.id] }
100
+ form.select column_name, options_for_select(options, form.object.send(column_name)),
101
+ { prompt: "Select #{association_name.humanize}" },
102
+ { class: 'w-full px-4 py-3 border border-slate-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-slate-50 focus:bg-white' }
103
+ else
104
+ form.number_field column_name, class: 'w-full px-4 py-3 border border-slate-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-slate-50 focus:bg-white'
105
+ end
106
+ else
107
+ form.number_field column_name, class: 'w-full px-4 py-3 border border-slate-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-slate-50 focus:bg-white'
108
+ end
109
+ when :decimal, :float
110
+ form.number_field column_name, step: 0.01, class: 'w-full px-4 py-3 border border-slate-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-slate-50 focus:bg-white'
111
+ else
112
+ if column_name.to_s.include?('email')
113
+ form.email_field column_name, class: 'w-full px-4 py-3 border border-slate-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-slate-50 focus:bg-white'
114
+ elsif column_name.to_s.include?('phone')
115
+ form.telephone_field column_name, class: 'w-full px-4 py-3 border border-slate-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-slate-50 focus:bg-white'
116
+ elsif column_name.to_s.include?('url') || column_name.to_s.include?('link')
117
+ form.url_field column_name, class: 'w-full px-4 py-3 border border-slate-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-slate-50 focus:bg-white'
118
+ elsif column_name.to_s.include?('password')
119
+ form.password_field column_name, class: 'w-full px-4 py-3 border border-slate-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-slate-50 focus:bg-white'
120
+ else
121
+ form.text_field column_name, class: 'w-full px-4 py-3 border border-slate-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-slate-50 focus:bg-white'
122
+ end
123
+ end
124
+ end
125
+
126
+ def display_name_for_model(record)
127
+ [:name, :title, :label, :email, :username].each do |attr|
128
+ return record.send(attr) if record.respond_to?(attr) && record.send(attr).present?
129
+ end
130
+ "#{record.class.name} ##{record.id}"
131
+ rescue
132
+ "#{record.class.name} ##{record.id}"
133
+ end
134
+
135
+ def solidcrud_stylesheet_path(filename = 'application.css')
136
+ assets_enabled = Rails.application.config.respond_to?(:assets) && Rails.application.config.assets.enabled
137
+ if Rails.application.config.api_only || !assets_enabled
138
+ stylesheet_path(filename: filename)
139
+ else
140
+ "solidcrud/#{filename.sub('.css', '')}"
141
+ end
142
+ end
143
+
144
+ def solidcrud_javascript_path(filename = 'application.js')
145
+ assets_enabled = Rails.application.config.respond_to?(:assets) && Rails.application.config.assets.enabled
146
+ if Rails.application.config.api_only || !assets_enabled
147
+ javascript_path(filename: filename)
148
+ else
149
+ "solidcrud/#{filename.sub('.js', '')}"
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,14 @@
1
+ // SolidCRUD JavaScript application
2
+ // Import Turbo first for optimal Rails compatibility
3
+ import "@hotwired/turbo-rails"
4
+
5
+ // Import and start Stimulus
6
+ import { Application } from "@hotwired/stimulus"
7
+ const application = Application.start()
8
+ application.debug = false
9
+ window.Stimulus = application
10
+
11
+ // Import controllers (they will auto-register when Stimulus is available)
12
+ import "./controllers"
13
+
14
+ export { application }
@@ -0,0 +1,64 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Connects to data-controller="crud"
4
+ export default class extends Controller {
5
+ static targets = ["form", "submitButton", "deleteButton"]
6
+
7
+ connect() {
8
+ console.log("SolidCRUD controller connected")
9
+ }
10
+
11
+ // Handle form submission
12
+ submit(event) {
13
+ event.preventDefault()
14
+
15
+ // Disable submit button to prevent double submission
16
+ if (this.hasSubmitButtonTarget) {
17
+ this.submitButtonTarget.disabled = true
18
+ this.submitButtonTarget.textContent = "Saving..."
19
+ }
20
+
21
+ // Submit the form
22
+ this.formTarget.submit()
23
+ }
24
+
25
+ // Handle delete confirmation
26
+ delete(event) {
27
+ event.preventDefault()
28
+
29
+ const message = this.deleteButtonTarget.dataset.confirmMessage || "Are you sure you want to delete this item?"
30
+ const confirmed = confirm(message)
31
+
32
+ if (confirmed) {
33
+ // Create and submit a delete form
34
+ const form = document.createElement("form")
35
+ form.method = "POST"
36
+ form.action = this.deleteButtonTarget.href
37
+
38
+ const methodInput = document.createElement("input")
39
+ methodInput.type = "hidden"
40
+ methodInput.name = "_method"
41
+ methodInput.value = "DELETE"
42
+ form.appendChild(methodInput)
43
+
44
+ document.body.appendChild(form)
45
+ form.submit()
46
+ }
47
+ }
48
+
49
+ // Handle successful form submission (can be called via Turbo events)
50
+ success() {
51
+ if (this.hasSubmitButtonTarget) {
52
+ this.submitButtonTarget.disabled = false
53
+ this.submitButtonTarget.textContent = "Save"
54
+ }
55
+ }
56
+
57
+ // Handle form errors
58
+ error() {
59
+ if (this.hasSubmitButtonTarget) {
60
+ this.submitButtonTarget.disabled = false
61
+ this.submitButtonTarget.textContent = "Save"
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,33 @@
1
+ // Import controllers
2
+ import CrudController from "./crud_controller"
3
+ import DashboardController from "../../../assets/javascripts/controllers/dashboard_controller"
4
+ import ModalController from "../../../assets/javascripts/controllers/modal_controller"
5
+ import NavigationController from "../../../assets/javascripts/controllers/navigation_controller"
6
+ import NotificationController from "../../../assets/javascripts/controllers/notification_controller"
7
+ import SearchController from "../../../assets/javascripts/controllers/search_controller"
8
+ import TableController from "../../../assets/javascripts/controllers/table_controller"
9
+
10
+ // Export controllers for manual registration
11
+ export {
12
+ CrudController,
13
+ DashboardController,
14
+ ModalController,
15
+ NavigationController,
16
+ NotificationController,
17
+ SearchController,
18
+ TableController
19
+ }
20
+
21
+ // Auto-register controllers if application is available
22
+ if (typeof window !== 'undefined' && window.Stimulus) {
23
+ const stimulusApp = window.Stimulus;
24
+ if (stimulusApp && typeof stimulusApp.register === 'function') {
25
+ stimulusApp.register("crud", CrudController)
26
+ stimulusApp.register("dashboard", DashboardController)
27
+ stimulusApp.register("modal", ModalController)
28
+ stimulusApp.register("navigation", NavigationController)
29
+ stimulusApp.register("notification", NotificationController)
30
+ stimulusApp.register("search", SearchController)
31
+ stimulusApp.register("table", TableController)
32
+ }
33
+ }