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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +1285 -0
- data/app/assets/javascripts/controllers/dashboard_controller.js +96 -0
- data/app/assets/javascripts/controllers/modal_controller.js +217 -0
- data/app/assets/javascripts/controllers/navigation_controller.js +117 -0
- data/app/assets/javascripts/controllers/notification_controller.js +85 -0
- data/app/assets/javascripts/controllers/search_controller.js +189 -0
- data/app/assets/javascripts/controllers/table_controller.js +272 -0
- data/app/assets/javascripts/solidcrud/application.js +9475 -0
- data/app/assets/stylesheets/solidcrud/_components.scss +267 -0
- data/app/assets/stylesheets/solidcrud/_forms.scss +69 -0
- data/app/assets/stylesheets/solidcrud/_layout.scss +149 -0
- data/app/assets/stylesheets/solidcrud/_tables.scss +90 -0
- data/app/assets/stylesheets/solidcrud/_variables.scss +21 -0
- data/app/assets/stylesheets/solidcrud/application.css +10 -0
- data/app/assets/stylesheets/solidcrud/application.css.map +1 -0
- data/app/assets/stylesheets/solidcrud/application.scss +10 -0
- data/app/assets/stylesheets/solidcrud/temp.css.map +1 -0
- data/app/assets/stylesheets/solidcrud/temp2.css.map +1 -0
- data/app/assets/stylesheets/solidcrud/webfonts/fa-brands-400.ttf +0 -0
- data/app/assets/stylesheets/solidcrud/webfonts/fa-brands-400.woff2 +0 -0
- data/app/assets/stylesheets/solidcrud/webfonts/fa-regular-400.ttf +0 -0
- data/app/assets/stylesheets/solidcrud/webfonts/fa-regular-400.woff2 +0 -0
- data/app/assets/stylesheets/solidcrud/webfonts/fa-solid-900.ttf +0 -0
- data/app/assets/stylesheets/solidcrud/webfonts/fa-solid-900.woff2 +0 -0
- data/app/assets/stylesheets/solidcrud/webfonts/fa-v4compatibility.ttf +0 -0
- data/app/assets/stylesheets/solidcrud/webfonts/fa-v4compatibility.woff2 +0 -0
- data/app/assets/stylesheets/webfonts/fa-brands-400.ttf +0 -0
- data/app/assets/stylesheets/webfonts/fa-brands-400.woff2 +0 -0
- data/app/assets/stylesheets/webfonts/fa-regular-400.ttf +0 -0
- data/app/assets/stylesheets/webfonts/fa-regular-400.woff2 +0 -0
- data/app/assets/stylesheets/webfonts/fa-solid-900.ttf +0 -0
- data/app/assets/stylesheets/webfonts/fa-solid-900.woff2 +0 -0
- data/app/assets/stylesheets/webfonts/fa-v4compatibility.ttf +0 -0
- data/app/assets/stylesheets/webfonts/fa-v4compatibility.woff2 +0 -0
- data/app/controllers/solidcrud/admin_controller.rb +215 -0
- data/app/controllers/solidcrud/application_controller.rb +19 -0
- data/app/controllers/solidcrud/assets_controller.rb +59 -0
- data/app/controllers/solidcrud/sessions_controller.rb +84 -0
- data/app/helpers/solidcrud/application_helper.rb +153 -0
- data/app/javascript/solidcrud/application.js +14 -0
- data/app/javascript/solidcrud/controllers/crud_controller.js +64 -0
- data/app/javascript/solidcrud/controllers/index.js +33 -0
- data/app/views/layouts/solidcrud/application.html.erb +70 -0
- data/app/views/solidcrud/admin/edit.html.erb +294 -0
- data/app/views/solidcrud/admin/index.html.erb +128 -0
- data/app/views/solidcrud/admin/model.html.erb +353 -0
- data/app/views/solidcrud/admin/new.html.erb +275 -0
- data/app/views/solidcrud/admin/shared/_dashboard_stats.html.erb +49 -0
- data/app/views/solidcrud/admin/shared/_edit_form_sidebar.html.erb +9 -0
- data/app/views/solidcrud/admin/shared/_flash_messages.html.erb +27 -0
- data/app/views/solidcrud/admin/shared/_full_sidebar.html.erb +56 -0
- data/app/views/solidcrud/admin/shared/_modal.html.erb +45 -0
- data/app/views/solidcrud/admin/shared/_new_form_sidebar.html.erb +6 -0
- data/app/views/solidcrud/admin/shared/_record_row.html.erb +35 -0
- data/app/views/solidcrud/admin/shared/_records_table.html.erb +85 -0
- data/app/views/solidcrud/sessions/new.html.erb +262 -0
- data/config/routes.rb +24 -0
- data/lib/generators/solidcrud/install/install_generator.rb +21 -0
- data/lib/generators/solidcrud/install/templates/INSTALL.md +80 -0
- data/lib/generators/solidcrud/install/templates/solidcrud.rb +31 -0
- data/lib/generators/solidcrud/install_generator.rb +17 -0
- data/lib/generators/solidcrud/templates/solidcrud.rb +4 -0
- data/lib/solidcrud/authentication.rb +143 -0
- data/lib/solidcrud/configuration.rb +64 -0
- data/lib/solidcrud/engine.rb +49 -0
- data/lib/solidcrud/version.rb +5 -0
- data/lib/solidcrud.rb +10 -0
- 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
|
+
}
|