fluxbit_view_components 0.3.0 → 0.4.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 (152) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -0
  3. data/app/assets/javascripts/fluxbit_view_components/assigner_controller.js +49 -0
  4. data/app/assets/javascripts/fluxbit_view_components/auto_submit_controller.js +39 -0
  5. data/app/assets/javascripts/fluxbit_view_components/drawer_controller.js +135 -0
  6. data/app/assets/javascripts/fluxbit_view_components/index.js +56 -0
  7. data/app/assets/javascripts/fluxbit_view_components/method_link_controller.js +143 -0
  8. data/app/assets/javascripts/fluxbit_view_components/modal_controller.js +118 -0
  9. data/app/assets/javascripts/fluxbit_view_components/password_controller.js +170 -0
  10. data/app/assets/javascripts/fluxbit_view_components/progress_controller.js +374 -0
  11. data/app/assets/javascripts/fluxbit_view_components/row_click_controller.js +32 -0
  12. data/app/assets/javascripts/fluxbit_view_components/select_all_controller.js +122 -0
  13. data/app/assets/javascripts/fluxbit_view_components/spinner_percent_controller.js +174 -0
  14. data/app/assets/javascripts/fluxbit_view_components/theme_button_controller.js +90 -0
  15. data/app/assets/javascripts/fluxbit_view_components.js +1175 -0
  16. data/app/components/fluxbit/accordion_component.rb +125 -0
  17. data/app/components/fluxbit/alert_component.rb +8 -8
  18. data/app/components/fluxbit/avatar_component.rb +11 -12
  19. data/app/components/fluxbit/avatar_group_component.rb +1 -1
  20. data/app/components/fluxbit/badge_component.rb +8 -7
  21. data/app/components/fluxbit/banner_component.rb +139 -0
  22. data/app/components/fluxbit/bottom_navigation_component.rb +437 -0
  23. data/app/components/fluxbit/breadcrumb_component.rb +66 -0
  24. data/app/components/fluxbit/button_component.rb +39 -11
  25. data/app/components/fluxbit/button_group_component.rb +1 -1
  26. data/app/components/fluxbit/card_component.rb +26 -23
  27. data/app/components/fluxbit/carousel_component.rb +154 -0
  28. data/app/components/fluxbit/component.rb +24 -3
  29. data/app/components/fluxbit/drawer_component.html.erb +30 -0
  30. data/app/components/fluxbit/drawer_component.rb +125 -0
  31. data/app/components/fluxbit/dropdown_component.rb +41 -0
  32. data/app/components/fluxbit/dropdown_item_component.rb +68 -0
  33. data/app/components/fluxbit/flex_component.rb +1 -1
  34. data/app/components/fluxbit/form/component.rb +15 -8
  35. data/app/components/fluxbit/form/dropzone_component.rb +3 -3
  36. data/app/components/fluxbit/form/field_component.rb +4 -2
  37. data/app/components/fluxbit/form/help_text_component.rb +1 -1
  38. data/app/components/fluxbit/form/label_component.rb +10 -3
  39. data/app/components/fluxbit/form/password_component.rb +247 -0
  40. data/app/components/fluxbit/form/radio_group_button_component.rb +126 -0
  41. data/app/components/fluxbit/form/select_component.rb +108 -11
  42. data/app/components/fluxbit/form/text_field_component.rb +40 -23
  43. data/app/components/fluxbit/form/toggle_component.rb +2 -2
  44. data/app/components/fluxbit/form/upload_image_component.html.erb +3 -3
  45. data/app/components/fluxbit/form/upload_image_component.rb +12 -1
  46. data/app/components/fluxbit/gravatar_component.rb +7 -0
  47. data/app/components/fluxbit/icon_helpers.rb +167 -0
  48. data/app/components/fluxbit/link_component.rb +42 -0
  49. data/app/components/fluxbit/modal_component.rb +28 -31
  50. data/app/components/fluxbit/pagination_component.rb +206 -0
  51. data/app/components/fluxbit/popover_component.rb +14 -14
  52. data/app/components/fluxbit/progress_component.rb +196 -0
  53. data/app/components/fluxbit/skeleton_component.rb +237 -0
  54. data/app/components/fluxbit/speed_dial_action_component.html.erb +30 -0
  55. data/app/components/fluxbit/speed_dial_action_component.rb +59 -0
  56. data/app/components/fluxbit/speed_dial_component.html.erb +33 -0
  57. data/app/components/fluxbit/speed_dial_component.rb +73 -0
  58. data/app/components/fluxbit/spinner_component.rb +71 -0
  59. data/app/components/fluxbit/spinner_percent_component.rb +174 -0
  60. data/app/components/fluxbit/stepper_component.rb +223 -0
  61. data/app/components/fluxbit/tab_component.rb +44 -25
  62. data/app/components/fluxbit/table_component.rb +186 -0
  63. data/app/components/fluxbit/table_group_component.rb +28 -0
  64. data/app/components/fluxbit/theme_button_component.rb +64 -0
  65. data/app/components/fluxbit/timeline_component.rb +63 -0
  66. data/app/components/fluxbit/timeline_item_component.html.erb +64 -0
  67. data/app/components/fluxbit/timeline_item_component.rb +78 -0
  68. data/app/components/fluxbit/tooltip_component.rb +2 -2
  69. data/app/helpers/fluxbit/components_helper.rb +74 -4
  70. data/app/helpers/fluxbit/form_builder.rb +64 -15
  71. data/app/helpers/fluxbit/view_helper.rb +71 -0
  72. data/config/locales/en.yml +37 -4
  73. data/config/locales/pt-BR.yml +36 -0
  74. data/lib/fluxbit/config/accordion_component.rb +73 -0
  75. data/lib/fluxbit/config/avatar_component.rb +11 -11
  76. data/lib/fluxbit/config/badge_component.rb +14 -11
  77. data/lib/fluxbit/config/banner_component.rb +60 -0
  78. data/lib/fluxbit/config/bottom_navigation_component.rb +74 -0
  79. data/lib/fluxbit/config/breadcrumb_component.rb +24 -0
  80. data/lib/fluxbit/config/button_component.rb +6 -4
  81. data/lib/fluxbit/config/card_component.rb +23 -12
  82. data/lib/fluxbit/config/carousel_component.rb +33 -0
  83. data/lib/fluxbit/config/drawer_component.rb +48 -0
  84. data/lib/fluxbit/config/dropdown_component.rb +29 -0
  85. data/lib/fluxbit/config/form/check_box_component.rb +1 -1
  86. data/lib/fluxbit/config/form/dropzone_component.rb +1 -1
  87. data/lib/fluxbit/config/form/help_text_component.rb +1 -1
  88. data/lib/fluxbit/config/form/label_component.rb +3 -2
  89. data/lib/fluxbit/config/form/password_component.rb +19 -0
  90. data/lib/fluxbit/config/form/radio_group_button_component.rb +24 -0
  91. data/lib/fluxbit/config/form/text_field_component.rb +11 -11
  92. data/lib/fluxbit/config/form/toggle_component.rb +5 -5
  93. data/lib/fluxbit/config/link_component.rb +24 -0
  94. data/lib/fluxbit/config/modal_component.rb +1 -1
  95. data/lib/fluxbit/config/pagination_component.rb +31 -0
  96. data/lib/fluxbit/config/popover_component.rb +1 -1
  97. data/lib/fluxbit/config/progress_component.rb +63 -0
  98. data/lib/fluxbit/config/skeleton_component.rb +82 -0
  99. data/lib/fluxbit/config/speed_dial_component.rb +50 -0
  100. data/lib/fluxbit/config/spinner_component.rb +30 -0
  101. data/lib/fluxbit/config/spinner_percent_component.rb +61 -0
  102. data/lib/fluxbit/config/stepper_component.rb +299 -0
  103. data/lib/fluxbit/config/tab_component.rb +6 -0
  104. data/lib/fluxbit/config/table_component.rb +75 -0
  105. data/lib/fluxbit/config/theme_button_component.rb +19 -0
  106. data/lib/fluxbit/config/timeline_component.rb +77 -0
  107. data/lib/fluxbit/view_components/engine.rb +11 -3
  108. data/lib/fluxbit/view_components/version.rb +1 -1
  109. data/lib/fluxbit/view_components.rb +20 -0
  110. data/lib/generators/fluxbit/devise_views_generator.rb +116 -0
  111. data/lib/generators/fluxbit/pagy_generator.rb +39 -0
  112. data/lib/generators/fluxbit/scaffold_generator.rb +165 -0
  113. data/lib/generators/fluxbit/templates/_alert.html.erb.tt +1 -0
  114. data/lib/generators/fluxbit/templates/_flash.html.erb.tt +15 -0
  115. data/lib/generators/fluxbit/templates/_form.html.erb.tt +38 -0
  116. data/lib/generators/fluxbit/templates/_metadata.html.erb.tt +44 -0
  117. data/lib/generators/fluxbit/templates/controller.rb.tt +406 -0
  118. data/lib/generators/fluxbit/templates/create.turbo_stream.erb.tt +7 -0
  119. data/lib/generators/fluxbit/templates/destroy.turbo_stream.erb.tt +3 -0
  120. data/lib/generators/fluxbit/templates/destroy_all.turbo_stream.erb.tt +9 -0
  121. data/lib/generators/fluxbit/templates/devise_views/confirmations/new.html.erb +11 -0
  122. data/lib/generators/fluxbit/templates/devise_views/layouts/devise.html.erb +64 -0
  123. data/lib/generators/fluxbit/templates/devise_views/mailer/confirmation_instructions.html.erb +5 -0
  124. data/lib/generators/fluxbit/templates/devise_views/mailer/email_changed.html.erb +7 -0
  125. data/lib/generators/fluxbit/templates/devise_views/mailer/password_changed.html.erb +3 -0
  126. data/lib/generators/fluxbit/templates/devise_views/mailer/reset_password_instructions.html.erb +8 -0
  127. data/lib/generators/fluxbit/templates/devise_views/mailer/unlock_instructions.html.erb +7 -0
  128. data/lib/generators/fluxbit/templates/devise_views/passwords/edit.html.erb +29 -0
  129. data/lib/generators/fluxbit/templates/devise_views/passwords/new.html.erb +11 -0
  130. data/lib/generators/fluxbit/templates/devise_views/registrations/edit.html.erb +43 -0
  131. data/lib/generators/fluxbit/templates/devise_views/registrations/new.html.erb +34 -0
  132. data/lib/generators/fluxbit/templates/devise_views/sessions/new.html.erb +15 -0
  133. data/lib/generators/fluxbit/templates/devise_views/shared/_error_messages.html.erb +14 -0
  134. data/lib/generators/fluxbit/templates/devise_views/shared/_links.html.erb +25 -0
  135. data/lib/generators/fluxbit/templates/devise_views/unlocks/new.html.erb +11 -0
  136. data/lib/generators/fluxbit/templates/edit.html.erb.tt +47 -0
  137. data/lib/generators/fluxbit/templates/fluxbit_pagy.css +27 -0
  138. data/lib/generators/fluxbit/templates/i18n.en.yml.tt +121 -0
  139. data/lib/generators/fluxbit/templates/i18n.pt-BR.yml.tt +121 -0
  140. data/lib/generators/fluxbit/templates/index.html.erb.tt +254 -0
  141. data/lib/generators/fluxbit/templates/index.json.jbuilder.tt +33 -0
  142. data/lib/generators/fluxbit/templates/new.html.erb.tt +47 -0
  143. data/lib/generators/fluxbit/templates/partial.html.erb.tt +61 -0
  144. data/lib/generators/fluxbit/templates/policy.rb.tt +36 -0
  145. data/lib/generators/fluxbit/templates/send_alert_via_drawer.erb.tt +10 -0
  146. data/lib/generators/fluxbit/templates/show.html.erb.tt +44 -0
  147. data/lib/generators/fluxbit/templates/show.json.jbuilder.tt +6 -0
  148. data/lib/generators/fluxbit/templates/update.turbo_stream.erb.tt +10 -0
  149. data/lib/generators/fluxbit/templates/update_all.turbo_stream.erb.tt +20 -0
  150. data/lib/install/install.rb +58 -0
  151. metadata +107 -18
  152. data/app/helpers/fluxbit/classes_helper.rb +0 -9
@@ -0,0 +1,44 @@
1
+ <% singular = file_name.singularize; plural = file_name.pluralize -%>
2
+ <%% if <%= singular %>.persisted? %>
3
+ <%%= fx_flex(vertical: false, gap: 1, class: "mt-4") do %>
4
+ <%%= fx_flex(align_items: :center, justify_content: :center, class: "bg-blue-500 dark:bg-blue-700 size-6 rounded-full") do %>
5
+ <%%= anyicon("heroicons_solid:plus", class: "size-4 text-white justify-center") %>
6
+ <%% end %>
7
+ <%%= fx_flex(vertical: true) do %>
8
+ <p class="text-sm font-medium text-gray-900 dark:text-white"><%%= t("<%= plural %>.fields.created_at") %></p>
9
+ <p class="text-sm text-gray-500">
10
+ <%%= <%= singular %>.created_at.strftime("%A, %B %d, %Y at %I:%M %p") %>
11
+ </p>
12
+ <p class="text-xs text-gray-400">
13
+ <%%= time_ago_in_words(<%= singular %>.created_at) %>
14
+ </p>
15
+ <%% end %>
16
+ <%% end %>
17
+
18
+ <%%= fx_flex(vertical: false, gap: 1, class: "mt-4") do %>
19
+ <%%= fx_flex(align_items: :center, justify_content: :center, class: "bg-green-500 dark:bg-green-700 size-6 rounded-full") do %>
20
+ <%%= anyicon("heroicons_solid:pencil", class: "size-4 text-white justify-center") %>
21
+ <%% end %>
22
+ <%%= fx_flex(vertical: true) do %>
23
+ <p class="text-sm font-medium text-gray-900 dark:text-white"><%%= t("<%= plural %>.fields.updated_at") %></p>
24
+ <p class="text-sm text-gray-500">
25
+ <%%= <%= singular %>.updated_at.strftime("%A, %B %d, %Y at %I:%M %p") %>
26
+ </p>
27
+ <p class="text-xs text-gray-400">
28
+ <%%= time_ago_in_words(<%= singular %>.updated_at) %>
29
+ </p>
30
+ <%% end %>
31
+ <%% end %>
32
+
33
+ <%%= fx_flex(vertical: false, gap: 1, class: "mt-4") do %>
34
+ <%%= fx_flex(align_items: :center, justify_content: :center, class: "bg-gray-500 dark:bg-gray-700 size-6 rounded-full") do %>
35
+ <%%= anyicon("heroicons_solid:finger-print", class: "size-4 text-white justify-center") %>
36
+ <%% end %>
37
+ <%%= fx_flex(vertical: true) do %>
38
+ <p class="text-sm font-medium text-gray-900 dark:text-white"><%%= t("<%= plural %>.fields.id") %></p>
39
+ <p class="text-sm text-gray-500">
40
+ <%%= <%= singular %>.id %>
41
+ </p>
42
+ <%% end %>
43
+ <%% end %>
44
+ <%% end %>
@@ -0,0 +1,406 @@
1
+ <% singular = file_name.singularize; plural = file_name.pluralize -%>
2
+ # frozen_string_literal: true
3
+
4
+ class <%= namespaced? ? "#{namespace_module}::" : "" %><%= class_name.pluralize %>Controller < ApplicationController
5
+ include Pundit::Authorization
6
+ rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
7
+
8
+ before_action :set_<%= singular %>, only: %i[ show edit update destroy ]
9
+ before_action :set_<%= plural %>_for_bulk_actions, only: %i[ update_all destroy_all ]
10
+ <% if options[:pundit] %>
11
+ after_action :verify_authorized, except: :index
12
+ after_action :verify_policy_scoped, only: :index
13
+ <% end %>
14
+
15
+ # GET /<%= "#{namespace_path}/" %><%= plural %>
16
+ def index
17
+ <% if options[:paginator] -%>
18
+ @page_size = 10
19
+ Pagy::DEFAULT[:limit] = (params[:per_page].presence || @page_size).to_i
20
+ <% end -%>
21
+ @<%= plural %> = policy_scope(<%= class_name.singularize %>).all
22
+
23
+ # Search and filter logic
24
+ @q = params[:q]
25
+ @<%= plural %> = @<%= plural %>.where("name LIKE ?", "%#{@q}%") if @q.present?
26
+ <% attributes.first(5).each do |att|
27
+ name, type = att.name, att.type.to_s
28
+ next if type == 'password_digest' # handle password fields separately
29
+ -%>
30
+ <% if ['integer','decimal'].include?(type) -%>
31
+ @<%= name %> = params[:<%= name %>]
32
+ @<%= plural %> = @<%= plural %>.where("<%= name %> >= ?", @<%= name %>) if @<%= name %>.present?
33
+ <% elsif ['text', 'string'].include?(type) -%>
34
+ @<%= name %> = params[:<%= name %>]
35
+ @<%= plural %> = @<%= plural %>.where("<%= name %> LIKE ?", "%#{@<%= name %>}") if @<%= name %>.present?
36
+ <% end -%>
37
+ <% end -%>
38
+ @has_filters = [<%= attributes.reject { |a|
39
+ a.type.to_s == "password_digest" }.map { |a| "@#{a.name}" }.join(', ') %>, @q].compact_blank.any?
40
+
41
+ # Sort logic
42
+ if params[:order].present?
43
+ order = params[:order].rpartition("_")
44
+ order_options = %w[<%= attributes.map(&:name).join(" ") %> created_at updated_at]
45
+ if order.length == 3 && %w[asc desc].include?(order.last) && order_options.include?(order.first)
46
+ @<%= plural %> = @<%= plural %>.order(order.first.to_sym => order.last.to_sym)
47
+ else
48
+ flash[:error] = t("<%= plural %>.messages.order_error")
49
+ @<%= plural %> = @<%= plural %>.order(created_at: :desc)
50
+ end
51
+ else
52
+ @<%= plural %> = @<%= plural %>.order(created_at: :desc) # Default order
53
+ end
54
+
55
+ <% if options[:paginator] -%>
56
+ @pagy, @<%= plural %> = pagy(@<%= plural %>)
57
+ <% end -%>
58
+ respond_to do |format|
59
+ format.html # index.html.erb
60
+ format.json
61
+ format.csv { send_data generate_csv(@<%= plural %>), filename: "<%= plural %>-#{Date.today}.csv" }
62
+ end
63
+ end
64
+
65
+ # GET /<%= "#{namespace_path}/" %><%= plural %>/1
66
+ def show
67
+ authorize @<%= singular %>
68
+ respond_to do |format|
69
+ format.html # show.html.erb
70
+ format.json
71
+ end
72
+ end
73
+
74
+ # GET /<%= "#{namespace_path}/" %><%= plural %>/new
75
+ def new
76
+ @<%= singular %> = <%= class_name.singularize %>.new
77
+ authorize @<%= singular %>
78
+ respond_to do |format|
79
+ format.html # new.html.erb (form in modal)
80
+ format.json { render template: "<%= "#{namespace_path}/" %><%= plural %>/show", locals: { <%= singular %>: @<%= singular %> } }
81
+ end
82
+ end
83
+
84
+ # GET /<%= "#{namespace_path}/" %><%= plural %>/1/edit
85
+ def edit
86
+ authorize @<%= singular %>
87
+ respond_to do |format|
88
+ format.html
89
+ format.json { render template: "<%= "#{namespace_path}/" %><%= plural %>/show", locals: { <%= singular %>: @<%= singular %> } }
90
+ end
91
+ end
92
+
93
+ # POST /<%= "#{namespace_path}/" %><%= plural %>
94
+ def create
95
+ @<%= singular %> = <%= class_name.singularize %>.new(<%= singular %>_params)
96
+ authorize @<%= singular %>
97
+
98
+ respond_to do |format|
99
+ if @<%= singular %>.save
100
+ @message = t("<%= plural %>.messages.create_success")
101
+ format.html do
102
+ flash[:notice] = @message
103
+
104
+ if turbo_frame_request?
105
+ render "shared/send_alert_via_drawer", locals: { message: @message, color: :success }
106
+ else
107
+ redirect_to <%= path_prefix %><%= plural %>_path
108
+ end
109
+ end
110
+ <% if options[:turbo] -%>
111
+ format.turbo_stream # renders create.turbo_stream.erb
112
+ <% end -%>
113
+ format.json { render template: "<%= "#{namespace_path}/" %><%= plural %>/show", locals: { <%= singular %>: @<%= singular %> }, status: :created }
114
+ else
115
+ format.html do
116
+ # Re-render the form for HTML (status 422 for validation errors)
117
+ render :new, status: :unprocessable_content
118
+ end
119
+ <% if options[:turbo] -%>
120
+ format.turbo_stream # renders create.turbo_stream.erb (re-render form with errors)
121
+ <% end -%>
122
+ format.json { render template: "<%= "#{namespace_path}/" %><%= plural %>/show", locals: { <%= singular %>: @<%= singular %> }, status: :unprocessable_content }
123
+ end
124
+ end
125
+ end
126
+
127
+ # PATCH/PUT /<%= "#{namespace_path}/" %><%= plural %>/1
128
+ def update
129
+ authorize @<%= singular %>
130
+ respond_to do |format|
131
+ if @<%= singular %>.update(<%= singular %>_params)
132
+ @message = t("<%= plural %>.messages.update_success")
133
+ format.html do
134
+ flash[:notice] = @message
135
+
136
+ if turbo_frame_request?
137
+ render "shared/send_alert_via_drawer", locals: { message: @message, color: :success }
138
+ else
139
+ redirect_to <%= path_prefix %><%= plural %>_path
140
+ end
141
+ end
142
+ <% if options[:turbo] -%>
143
+ format.turbo_stream # renders update.turbo_stream.erb
144
+ <% end -%>
145
+ format.json { render template: "<%= "#{namespace_path}/" %><%= plural %>/show", locals: { <%= singular %>: @<%= singular %> } }
146
+ else
147
+ format.html do
148
+ render :edit, status: :unprocessable_content
149
+ end
150
+ <% if options[:turbo] -%>
151
+ format.turbo_stream # renders update.turbo_stream.erb (re-render form with errors)
152
+ <% end -%>
153
+ format.json { render template: "<%= "#{namespace_path}/" %><%= plural %>/show", locals: { <%= singular %>: @<%= singular %> }, status: :unprocessable_content }
154
+ end
155
+ end
156
+ end
157
+
158
+ # DELETE /<%= "#{namespace_path}/" %><%= plural %>/1
159
+ def destroy
160
+ authorize @<%= singular %>
161
+ if @<%= singular %>.destroy
162
+ @message = t("<%= plural %>.messages.destroy_success")
163
+ color_alert = :success
164
+ else
165
+ @message = t("<%= plural %>.messages.destroy_failure")
166
+ color_alert = :danger
167
+ end
168
+
169
+ respond_to do |format|
170
+ format.html do
171
+ flash[:notice] = @message
172
+
173
+ if turbo_frame_request?
174
+ render "shared/send_alert_via_drawer", locals: { message: @message, color: color_alert }
175
+ else
176
+ redirect_to <%= path_prefix %><%= plural %>_path
177
+ end
178
+ end
179
+ <% if options[:turbo] -%>
180
+ format.turbo_stream # renders destroy.turbo_stream.erb
181
+ <% end -%>
182
+ format.json { head :no_content }
183
+ end
184
+ end
185
+
186
+ # UPDATE_ALL /<%= "#{namespace_path}/" %><%= plural %>/update_all
187
+ def update_all
188
+ authorize @<%= plural %>
189
+ @errors = []
190
+ @count = 0
191
+
192
+ ActiveRecord::Base.transaction do
193
+ @<%= plural %>.each do |<%= singular %>|
194
+ if <%= singular %>.update(<%= singular %>_params)
195
+ @count += 1
196
+ else
197
+ @errors << { id: <%= singular %>.id, errors: <%= singular %>.errors.full_messages }
198
+ end
199
+ end
200
+
201
+ # Rollback if any validation fails (optional - remove if you want partial updates)
202
+ raise ActiveRecord::Rollback if @errors.any?
203
+ end
204
+
205
+ @message = if @errors.empty?
206
+ t("<%= plural %>.messages.bulk_update_success", count: @count)
207
+ else
208
+ t("<%= plural %>.messages.bulk_update_failure", count: @count, errors: @errors.map { |e| "<%= singular.humanize %> ID #{e[:id]}: #{e[:errors].join(', ')}" }.join('; '))
209
+ end
210
+
211
+ respond_with_bulk_result(@errors.empty?)
212
+ end
213
+
214
+ # DESTROY_ALL /<%= "#{namespace_path}/" %><%= plural %>/destroy_all
215
+ def destroy_all
216
+ authorize @<%= plural %>
217
+ @errors = []
218
+ @count = 0
219
+
220
+ ActiveRecord::Base.transaction do
221
+ @<%= plural %>.each do |<%= singular %>|
222
+ if <%= singular %>.destroy
223
+ @count += 1
224
+ else
225
+ @errors << { id: <%= singular %>.id, errors: <%= singular %>.errors.full_messages }
226
+ end
227
+ end
228
+
229
+ # Rollback if any validation fails (optional - remove if you want partial updates)
230
+ raise ActiveRecord::Rollback if @errors.any?
231
+ end
232
+
233
+ @message = if @errors.empty?
234
+ t("<%= plural %>.messages.bulk_destroy_success", count: @count)
235
+ else
236
+ t("<%= plural %>.messages.bulk_destroy_failure", errors: @errors.map { |e| "<%= singular.humanize %> ID #{e[:id]}: #{e[:errors].join(', ')}" }.join('; '))
237
+ end
238
+ respond_with_bulk_result(@errors.empty?)
239
+ end
240
+
241
+ private
242
+
243
+ def set_<%= singular %>
244
+ @<%= singular %> = <%= class_name.singularize %>.find(params[:id])
245
+ # Note: Pundit authorization for @<%= singular %> happens in each action
246
+ rescue ActiveRecord::RecordNotFound
247
+ @message = t("<%= plural %>.messages.not_found")
248
+ respond_to do |format|
249
+ format.html do
250
+ flash[:error] = @message
251
+
252
+ if turbo_frame_request?
253
+ render "shared/send_alert_via_drawer", locals: { message: @message, color: :success }
254
+ else
255
+ redirect_to <%= path_prefix %><%= plural %>_path
256
+ end
257
+ end
258
+ format.json { render json: { error: @message }, status: :not_found }
259
+ <% if options[:turbo] -%>
260
+ format.turbo_stream { render turbo_stream: turbo_stream.append("notice", partial: "shared/alert", locals: { message: @message, color: :error }) }
261
+ <% end -%>
262
+ end
263
+ end
264
+
265
+ def set_<%= plural %>_for_bulk_actions
266
+ <%= singular %>_ids = Array(params[:<%= singular %>_ids]).reject(&:blank?)
267
+ @<%= plural %> = <%= class_name.singularize %>.where(id: <%= singular %>_ids)
268
+
269
+ if @<%= plural %>.empty?
270
+ @message = t("<%= plural %>.messages.not_selected_for_action")
271
+ respond_to do |format|
272
+ format.html do
273
+ flash[:warning] = @message
274
+
275
+ if turbo_frame_request?
276
+ render "shared/send_alert_via_drawer", locals: { message: @message, color: :danger }
277
+ else
278
+ redirect_to <%= path_prefix %><%= plural %>_path
279
+ end
280
+ end
281
+ <% if options[:turbo] -%>
282
+ format.turbo_stream { render turbo_stream: turbo_stream.append("notice", partial: "shared/alert", locals: { message: @message, color: :danger }) }
283
+ <% end -%>
284
+ format.json { render json: { message: @message }, status: :bad_request }
285
+ end
286
+ end
287
+ end
288
+
289
+ def respond_with_bulk_result(success)
290
+ respond_to do |format|
291
+ if success
292
+ format.html do
293
+ flash[:notice] = @message
294
+
295
+ if turbo_frame_request?
296
+ render "shared/send_alert_via_drawer", locals: { message: @message, color: :success }
297
+ else
298
+ redirect_to <%= path_prefix %><%= plural %>_path
299
+ end
300
+ end
301
+ <% if options[:turbo] -%>
302
+ format.turbo_stream
303
+ <% end -%>
304
+ format.json { render json: { message: @message, count: @count }, status: :ok }
305
+ else
306
+ format.html do
307
+ flash[:error] = @message
308
+
309
+ if turbo_frame_request?
310
+ render "shared/send_alert_via_drawer", locals: { message: @message, color: :danger }
311
+ else
312
+ redirect_to <%= path_prefix %><%= plural %>_path
313
+ end
314
+ end
315
+ <% if options[:turbo] -%>
316
+ format.turbo_stream
317
+ <% end -%>
318
+ format.json {
319
+ render json: {
320
+ message: @message,
321
+ errors: @errors
322
+ }, status: :unprocessable_content
323
+ }
324
+ end
325
+ end
326
+ end
327
+
328
+ # Only allow a list of trusted parameters through.
329
+ def <%= singular %>_params
330
+ params.require(:<%= singular %>).permit(
331
+ <%- allowed_attrs = attributes.map do |att|
332
+ attr_name, attr_type = att.name, att.type.to_s
333
+ if ['references', 'belongs_to'].include?(attr_type)
334
+ ":#{attr_name}_id"
335
+ elsif attr_type == 'password_digest'
336
+ # For password digest, permit password and password_confirmation
337
+ ':password, :password_confirmation'
338
+ else
339
+ ":#{attr_name}"
340
+ end
341
+ end
342
+ -%>
343
+ <%= allowed_attrs.join(', ') %>
344
+ )
345
+ end
346
+
347
+ def user_not_authorized
348
+ @message = t("<%= plural %>.messages.not_authorized")
349
+ respond_to do |format|
350
+ format.html do
351
+ flash[:error] = @message
352
+ redirect_to <%= path_prefix %><%= plural %>_path
353
+ end
354
+ format.json { render json: { error: @message }, status: :forbidden }
355
+ <% if options[:turbo] -%>
356
+ format.turbo_stream { render turbo_stream: turbo_stream.append("error", partial: "shared/alert", locals: { message: @message, color: :error }) }
357
+ <% end -%>
358
+ end
359
+ end
360
+
361
+ def generate_csv(<%= plural %>)
362
+ attribute_names = %w[<%= attributes.map(&:name).map(&:camelize).join(" ") %>]
363
+ require "csv" unless defined?(CSV)
364
+
365
+ CSV.generate(headers: true) do |csv|
366
+ csv << attribute_names
367
+ <%= plural %>.each do |<%= singular %>|
368
+ csv << [
369
+ <% attributes.reject { |attr| attr.type == :password_digest }.each_with_index do |attr, idx| -%>
370
+ <%=
371
+ case attr.type
372
+ when :boolean
373
+ "I18n.t(#{singular}.#{attr.name}?, scope: \"#{plural}.values.#{attr.name}\")"
374
+ when :datetime
375
+ "#{singular}.#{attr.name}.strftime('%Y-%m-%d %H:%M:%S')"
376
+ else
377
+ "#{singular}.#{attr.name}"
378
+ end
379
+ %><%= "," unless idx == attributes.size - 1 %>
380
+ <% end -%>
381
+ ]
382
+ end
383
+ end
384
+ rescue LoadError
385
+ # Fallback if CSV is not available
386
+ content = "#{attribute_names.join(',')}\n"
387
+ <%= plural %>.each do |<%= singular %>|
388
+
389
+ content += [
390
+ <% attributes.reject { |attr| attr.type == :password_digest }.each_with_index do |attr, idx| -%>
391
+ <%=
392
+ case attr.type
393
+ when :boolean
394
+ "I18n.t(#{singular}.#{attr.name}?, scope: \"#{plural}.values.#{attr.name}\")"
395
+ when :datetime
396
+ "#{singular}.#{attr.name}.strftime('%Y-%m-%d %H:%M:%S')"
397
+ else
398
+ "#{singular}.#{attr.name}"
399
+ end
400
+ %><%= "," unless idx == attributes.size - 1 %>
401
+ <% end -%>
402
+ ].join(",")
403
+ end
404
+ content
405
+ end
406
+ end
@@ -0,0 +1,7 @@
1
+ <% singular = file_name.singularize; plural = file_name.pluralize -%>
2
+ <%%= if @<%= singular %>.errors.any?
3
+ turbo_stream.replace "<%= singular %>_form", partial: "<%= namespace_path %>/<%= plural %>/form", locals: { <%= plural %>: [@<%= singular %>] }
4
+ else
5
+ flash[:notice] = @message
6
+ turbo_stream.action :refresh, "body"
7
+ end %>
@@ -0,0 +1,3 @@
1
+ <% singular = file_name.singularize -%>
2
+ <%%= turbo_stream.remove dom_id(@<%= singular %>) %>
3
+ <%%= turbo_stream.append "notice", fx_alert(with_content: @message, color: @<%= singular %>.errors.any? ? :danger : :success) %>
@@ -0,0 +1,9 @@
1
+ <% singular = file_name.singularize; plural = file_name.pluralize -%>
2
+ <%% if @errors.any? %>
3
+ <%%= turbo_stream.append "notice", fx_alert(with_content: @message, color: :danger) %>
4
+ <%% else %>
5
+ <%% @<%= plural %>.each do |<%= singular %>| %>
6
+ <%%= turbo_stream.remove dom_id(<%= singular %>) %>
7
+ <%% end %>
8
+ <%%= turbo_stream.append "notice", fx_alert(with_content: @message, color: :success) %>
9
+ <%% end %>
@@ -0,0 +1,11 @@
1
+ <% content_for(:header) do %>
2
+ <%= fx_heading with_content: "Resend confirmation instructions", size: 2, class: "text-center mt-8" %>
3
+ <% end %>
4
+
5
+ <%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }, builder: Fluxbit::FormBuilder) do |f| %>
6
+ <%= render "devise/shared/error_messages", resource: resource %>
7
+ <%= f.fx_email_field :email, autofocus: true, autocomplete: "email", value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email), wrapper_html: {class: "mb-6 field"} %>
8
+ <%= f.fx_submit "Resend confirmation instructions" %>
9
+ <% end %>
10
+
11
+ <%= render "devise/shared/links" %>
@@ -0,0 +1,64 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%%= content_for(:title) || "Application Name" %></title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <meta name="apple-mobile-web-app-capable" content="yes">
7
+ <meta name="mobile-web-app-capable" content="yes">
8
+ <%%= csrf_meta_tags %>
9
+ <%%= csp_meta_tag %>
10
+
11
+ <%%= yield :head %>
12
+ <link rel="icon" href="/icon.png" type="image/png">
13
+ <link rel="icon" href="/icon.svg" type="image/svg+xml">
14
+ <link rel="apple-touch-icon" href="/icon.png">
15
+
16
+ <%%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
17
+ <%%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
18
+ <script>
19
+ if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
20
+ document.documentElement.classList.add('dark');
21
+ } else {
22
+ document.documentElement.classList.remove('dark')
23
+ }
24
+ </script>
25
+ </head>
26
+ <body class="<%%= fx_body_class %>">
27
+ <p class="notice">
28
+ <%% if notice %>
29
+ <%%= fx_alert(color: :info, 'data-turbo-cache' => "false", with_content: notice) %>
30
+ <%% end %>
31
+ </p>
32
+ <p class="alert">
33
+ <%% if alert %>
34
+ <%%= fx_alert(color: :danger, 'data-turbo-cache' => "false", with_content: alert) %>
35
+ <%% end %>
36
+ </p>
37
+ <div class="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
38
+ <div class="sm:mx-auto sm:w-full sm:max-w-md">
39
+ <!-- Replace with your company logo -->
40
+ <%%= fx_heading(class: "text-center text-blue-900 flex justify-center items-center", remove_class: "text-gray-900") do %>
41
+ <svg width="512" height="512" viewBox="0 0 31.999996 31.999992" fill="#0068e6" aria-hidden="true" data-slot="icon" version="1.1" id="fluxbit" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" alt="Fluxbit Logo" class="h-24 w-auto px-2">
42
+ <g transform="translate(-14.895488,-76.954309)">
43
+ <g transform="matrix(0.03883817,0,0,0.03883817,-18.205404,74.713827)">
44
+ <path d="m 1089.7476,425.74218 c 57.5807,-87.88823 229.5277,-143.25616 138.5588,-203.04776 -35.7449,-21.73146 3.8792,-32.89434 29.8425,-24.79965 56.6117,17.64991 66.3127,69.78167 57.5963,102.44516 -25.5052,95.57597 -173.0966,108.96892 -164.5857,215.80641 14.7244,113.56667 178.4751,104.15893 218.0103,9.19107 13.1682,-31.63159 3.7281,-58.61968 -21.3229,-60.95897 -43.9037,-4.09979 -79.283,70.54958 -103.9213,79.24255 -23.0709,8.13988 -59.7994,-24.2899 -13.7305,-48.62764 51.675,-26.4149 68.1608,-106.93054 145.1876,-91.59293 84.7516,20.2064 88.9232,201.5204 -41.4396,268.20255 -164.4932,84.14041 -337.34273,-103.68571 -244.1955,-245.86079 z"/>
45
+ <rect width="55.374088" height="54.900341" x="1366.8969" y="321.34384" ry="10.65924" />
46
+ <path d="M 1264.2422,57.6875 A 411.96594,411.96594 0 0 0 852.27734,469.65234 411.96594,411.96594 0 0 0 1264.2422,881.61914 411.96594,411.96594 0 0 0 1676.209,469.65234 411.96594,411.96594 0 0 0 1264.2422,57.6875 Z m 0,40.025391 A 371.93936,371.93936 0 0 1 1636.1816,469.65234 371.93936,371.93936 0 0 1 1264.2422,841.5918 371.93936,371.93936 0 0 1 892.30469,469.65234 371.93936,371.93936 0 0 1 1264.2422,97.712891 Z" />
47
+ </g>
48
+ </g>
49
+ </svg>
50
+ Fluxbit
51
+ <%% end %>
52
+ <!-- /End replace -->
53
+
54
+ <%%= yield(:header) if content_for?(:header) %>
55
+ </div>
56
+
57
+ <div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
58
+ <div class="py-8 px-4 sm:px-10 bg-white border border-gray-200 rounded-lg shadow dark:bg-slate-800 dark:border-slate-700">
59
+ <%%= yield %>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ </body>
64
+ </html>
@@ -0,0 +1,5 @@
1
+ <p>Welcome <%= @email %>!</p>
2
+
3
+ <p>You can confirm your account email through the link below:</p>
4
+
5
+ <p><%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %></p>
@@ -0,0 +1,7 @@
1
+ <p>Hello <%= @email %>!</p>
2
+
3
+ <% if @resource.try(:unconfirmed_email?) %>
4
+ <p>We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.</p>
5
+ <% else %>
6
+ <p>We're contacting you to notify you that your email has been changed to <%= @resource.email %>.</p>
7
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <p>Hello <%= @resource.email %>!</p>
2
+
3
+ <p>We're contacting you to notify you that your password has been changed.</p>
@@ -0,0 +1,8 @@
1
+ <p>Hello <%= @resource.email %>!</p>
2
+
3
+ <p>Someone has requested a link to change your password. You can do this through the link below.</p>
4
+
5
+ <p><%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %></p>
6
+
7
+ <p>If you didn't request this, please ignore this email.</p>
8
+ <p>Your password won't change until you access the link above and create a new one.</p>
@@ -0,0 +1,7 @@
1
+ <p>Hello <%= @resource.email %>!</p>
2
+
3
+ <p>Your account has been locked due to an excessive number of unsuccessful sign in attempts.</p>
4
+
5
+ <p>Click the link below to unlock your account:</p>
6
+
7
+ <p><%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %></p>
@@ -0,0 +1,29 @@
1
+ <% content_for(:header) do %>
2
+ <%= fx_heading with_content: "Change your password", size: 2, class: "text-center mt-8" %>
3
+ <% end %>
4
+
5
+ <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }, builder: Fluxbit::FormBuilder) do |f| %>
6
+ <%= render "devise/shared/error_messages", resource: resource %>
7
+ <%= f.hidden_field :reset_password_token %>
8
+ <%= f.fx_password :password,
9
+ autofocus: true,
10
+ autocomplete: "new-password",
11
+ min_length: 6,
12
+ require_uppercase: true,
13
+ require_lowercase: true,
14
+ require_numbers: true,
15
+ require_special: true,
16
+ wrapper_html: {class: "mb-6 field"} %>
17
+ <%= f.fx_password :password_confirmation,
18
+ autocomplete: "new-password",
19
+ min_length: 6,
20
+ require_uppercase: true,
21
+ require_lowercase: true,
22
+ require_numbers: true,
23
+ require_special: true,
24
+ wrapper_html: {class: "mb-6 field"},
25
+ help_text: "We need your current password to confirm your changes" %>
26
+ <%= f.fx_submit "Change my password" %>
27
+ <% end %>
28
+
29
+ <%= render "devise/shared/links" %>
@@ -0,0 +1,11 @@
1
+ <% content_for(:header) do %>
2
+ <%= fx_heading with_content: "Forgot your password?", size: 2, class: "text-center mt-8" %>
3
+ <% end %>
4
+
5
+ <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }, builder: Fluxbit::FormBuilder) do |f| %>
6
+ <%= render "devise/shared/error_messages", resource: resource %>
7
+ <%= f.fx_email_field :email, autofocus: true, autocomplete: "email", wrapper_html: {class: "mb-6 field"} %>
8
+ <%= f.fx_submit "Send me password reset instructions" %>
9
+ <% end %>
10
+
11
+ <%= render "devise/shared/links" %>