easy-admin-rails 0.1.15 → 0.2.1

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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/easy_admin.base.js +254 -18
  3. data/app/assets/builds/easy_admin.base.js.map +4 -4
  4. data/app/assets/builds/easy_admin.css +112 -18
  5. data/app/components/easy_admin/base_component.rb +1 -0
  6. data/app/components/easy_admin/form_tabs_component.rb +5 -2
  7. data/app/components/easy_admin/navbar_component.rb +5 -1
  8. data/app/components/easy_admin/permissions/user_role_assignment_component.rb +254 -0
  9. data/app/components/easy_admin/permissions/user_role_permissions_component.rb +186 -0
  10. data/app/components/easy_admin/resources/index_component.rb +1 -4
  11. data/app/components/easy_admin/sidebar_component.rb +67 -2
  12. data/app/controllers/easy_admin/application_controller.rb +131 -1
  13. data/app/controllers/easy_admin/batch_actions_controller.rb +27 -0
  14. data/app/controllers/easy_admin/concerns/belongs_to_editing.rb +201 -0
  15. data/app/controllers/easy_admin/concerns/inline_field_editing.rb +297 -0
  16. data/app/controllers/easy_admin/concerns/resource_authorization.rb +55 -0
  17. data/app/controllers/easy_admin/concerns/resource_filtering.rb +178 -0
  18. data/app/controllers/easy_admin/concerns/resource_loading.rb +149 -0
  19. data/app/controllers/easy_admin/concerns/resource_pagination.rb +135 -0
  20. data/app/controllers/easy_admin/dashboard_controller.rb +2 -1
  21. data/app/controllers/easy_admin/dashboards_controller.rb +6 -40
  22. data/app/controllers/easy_admin/resources_controller.rb +13 -762
  23. data/app/controllers/easy_admin/row_actions_controller.rb +25 -0
  24. data/app/helpers/easy_admin/fields_helper.rb +61 -9
  25. data/app/javascript/easy_admin/controllers/event_emitter_controller.js +2 -4
  26. data/app/javascript/easy_admin/controllers/infinite_scroll_controller.js +0 -10
  27. data/app/javascript/easy_admin/controllers/jsoneditor_controller.js +1 -4
  28. data/app/javascript/easy_admin/controllers/permission_toggle_controller.js +227 -0
  29. data/app/javascript/easy_admin/controllers/role_preview_controller.js +93 -0
  30. data/app/javascript/easy_admin/controllers/select_field_controller.js +1 -2
  31. data/app/javascript/easy_admin/controllers/settings_button_controller.js +1 -2
  32. data/app/javascript/easy_admin/controllers/settings_sidebar_controller.js +1 -4
  33. data/app/javascript/easy_admin/controllers/turbo_stream_redirect.js +0 -2
  34. data/app/javascript/easy_admin/controllers.js +5 -1
  35. data/app/models/easy_admin/admin_user.rb +6 -0
  36. data/app/policies/admin_user_policy.rb +36 -0
  37. data/app/policies/application_policy.rb +83 -0
  38. data/app/views/easy_admin/application/authorization_failure.turbo_stream.erb +8 -0
  39. data/app/views/easy_admin/dashboards/card.html.erb +5 -0
  40. data/app/views/easy_admin/dashboards/card.turbo_stream.erb +7 -0
  41. data/app/views/easy_admin/dashboards/card_error.html.erb +3 -0
  42. data/app/views/easy_admin/dashboards/card_error.turbo_stream.erb +5 -0
  43. data/app/views/easy_admin/dashboards/show.turbo_stream.erb +7 -0
  44. data/app/views/easy_admin/resources/belongs_to_edit_attached.html.erb +6 -0
  45. data/app/views/easy_admin/resources/belongs_to_edit_attached.turbo_stream.erb +8 -0
  46. data/app/views/easy_admin/resources/belongs_to_reattach.html.erb +5 -0
  47. data/app/views/easy_admin/resources/edit.html.erb +1 -1
  48. data/app/views/easy_admin/resources/edit_field.html.erb +5 -0
  49. data/app/views/easy_admin/resources/edit_field.turbo_stream.erb +7 -0
  50. data/app/views/easy_admin/resources/index.html.erb +1 -1
  51. data/app/views/easy_admin/resources/index_frame.html.erb +8 -142
  52. data/app/views/easy_admin/resources/update_belongs_to_attached.turbo_stream.erb +25 -0
  53. data/app/views/layouts/easy_admin/application.html.erb +15 -2
  54. data/config/initializers/easy_admin_permissions.rb +73 -0
  55. data/db/seeds/easy_admin_permissions.rb +121 -0
  56. data/lib/easy-admin-rails.rb +2 -0
  57. data/lib/easy_admin/permissions/component.rb +168 -0
  58. data/lib/easy_admin/permissions/configuration.rb +37 -0
  59. data/lib/easy_admin/permissions/controller.rb +164 -0
  60. data/lib/easy_admin/permissions/dsl.rb +160 -0
  61. data/lib/easy_admin/permissions/models.rb +44 -0
  62. data/lib/easy_admin/permissions/permission_denied_component.rb +121 -0
  63. data/lib/easy_admin/permissions/resource_permissions.rb +231 -0
  64. data/lib/easy_admin/permissions/role_definition.rb +45 -0
  65. data/lib/easy_admin/permissions/role_denied_component.rb +159 -0
  66. data/lib/easy_admin/permissions/role_dsl.rb +73 -0
  67. data/lib/easy_admin/permissions/user_extensions.rb +129 -0
  68. data/lib/easy_admin/permissions.rb +113 -0
  69. data/lib/easy_admin/resource/base.rb +119 -0
  70. data/lib/easy_admin/resource/configuration.rb +148 -0
  71. data/lib/easy_admin/resource/dsl.rb +117 -0
  72. data/lib/easy_admin/resource/field_registry.rb +189 -0
  73. data/lib/easy_admin/resource/form_builder.rb +123 -0
  74. data/lib/easy_admin/resource/layout_builder.rb +249 -0
  75. data/lib/easy_admin/resource/scope_manager.rb +252 -0
  76. data/lib/easy_admin/resource/show_builder.rb +359 -0
  77. data/lib/easy_admin/resource.rb +8 -835
  78. data/lib/easy_admin/resource_modules.rb +11 -0
  79. data/lib/easy_admin/version.rb +1 -1
  80. data/lib/generators/easy_admin/permissions/install_generator.rb +90 -0
  81. data/lib/generators/easy_admin/permissions/templates/initializers/permissions.rb +37 -0
  82. data/lib/generators/easy_admin/permissions/templates/migrations/create_permission_tables.rb +27 -0
  83. data/lib/generators/easy_admin/permissions/templates/migrations/update_users_for_permissions.rb +6 -0
  84. data/lib/generators/easy_admin/permissions/templates/models/permission.rb +9 -0
  85. data/lib/generators/easy_admin/permissions/templates/models/role.rb +9 -0
  86. data/lib/generators/easy_admin/permissions/templates/models/role_permission.rb +9 -0
  87. data/lib/generators/easy_admin/permissions/templates/models/user_role.rb +9 -0
  88. data/lib/generators/easy_admin/permissions/templates/policies/application_policy.rb +47 -0
  89. data/lib/generators/easy_admin/permissions/templates/policies/user_policy.rb +36 -0
  90. data/lib/generators/easy_admin/permissions/templates/seeds/permissions.rb +89 -0
  91. metadata +62 -5
  92. data/db/migrate/20250101000001_create_easy_admin_admin_users.rb +0 -45
@@ -3418,10 +3418,6 @@ input:checked + .toggle-switch .toggle-slider:before {
3418
3418
  left: 50%;
3419
3419
  }
3420
3420
 
3421
- .left-10 {
3422
- left: 2.5rem;
3423
- }
3424
-
3425
3421
  .left-2 {
3426
3422
  left: 0.5rem;
3427
3423
  }
@@ -3430,10 +3426,6 @@ input:checked + .toggle-switch .toggle-slider:before {
3430
3426
  left: 1.5rem;
3431
3427
  }
3432
3428
 
3433
- .left-8 {
3434
- left: 2rem;
3435
- }
3436
-
3437
3429
  .right-0 {
3438
3430
  right: 0px;
3439
3431
  }
@@ -3616,6 +3608,10 @@ input:checked + .toggle-switch .toggle-slider:before {
3616
3608
  margin-inline-start: auto;
3617
3609
  }
3618
3610
 
3611
+ .mt-0\.5 {
3612
+ margin-top: 0.125rem;
3613
+ }
3614
+
3619
3615
  .mt-1 {
3620
3616
  margin-top: 0.25rem;
3621
3617
  }
@@ -3712,6 +3708,10 @@ input:checked + .toggle-switch .toggle-slider:before {
3712
3708
  height: 0.5rem;
3713
3709
  }
3714
3710
 
3711
+ .h-20 {
3712
+ height: 5rem;
3713
+ }
3714
+
3715
3715
  .h-3 {
3716
3716
  height: 0.75rem;
3717
3717
  }
@@ -3764,6 +3764,14 @@ input:checked + .toggle-switch .toggle-slider:before {
3764
3764
  min-height: 2.5rem;
3765
3765
  }
3766
3766
 
3767
+ .min-h-96 {
3768
+ min-height: 24rem;
3769
+ }
3770
+
3771
+ .min-h-\[2rem\] {
3772
+ min-height: 2rem;
3773
+ }
3774
+
3767
3775
  .min-h-\[40px\] {
3768
3776
  min-height: 40px;
3769
3777
  }
@@ -4063,6 +4071,10 @@ input:checked + .toggle-switch .toggle-slider:before {
4063
4071
  grid-template-columns: repeat(2, minmax(0, 1fr));
4064
4072
  }
4065
4073
 
4074
+ .grid-cols-3 {
4075
+ grid-template-columns: repeat(3, minmax(0, 1fr));
4076
+ }
4077
+
4066
4078
  .grid-cols-4 {
4067
4079
  grid-template-columns: repeat(4, minmax(0, 1fr));
4068
4080
  }
@@ -4355,6 +4367,11 @@ input:checked + .toggle-switch .toggle-slider:before {
4355
4367
  border-style: none;
4356
4368
  }
4357
4369
 
4370
+ .border-amber-200 {
4371
+ --tw-border-opacity: 1;
4372
+ border-color: rgb(253 230 138 / var(--tw-border-opacity, 1));
4373
+ }
4374
+
4358
4375
  .border-blue-100 {
4359
4376
  --tw-border-opacity: 1;
4360
4377
  border-color: rgb(219 234 254 / var(--tw-border-opacity, 1));
@@ -4458,6 +4475,11 @@ input:checked + .toggle-switch .toggle-slider:before {
4458
4475
  border-color: transparent;
4459
4476
  }
4460
4477
 
4478
+ .border-white {
4479
+ --tw-border-opacity: 1;
4480
+ border-color: rgb(255 255 255 / var(--tw-border-opacity, 1));
4481
+ }
4482
+
4461
4483
  .border-white\/20 {
4462
4484
  border-color: rgb(255 255 255 / 0.2);
4463
4485
  }
@@ -4472,6 +4494,16 @@ input:checked + .toggle-switch .toggle-slider:before {
4472
4494
  border-color: rgb(254 240 138 / var(--tw-border-opacity, 1));
4473
4495
  }
4474
4496
 
4497
+ .bg-amber-50 {
4498
+ --tw-bg-opacity: 1;
4499
+ background-color: rgb(255 251 235 / var(--tw-bg-opacity, 1));
4500
+ }
4501
+
4502
+ .bg-amber-600 {
4503
+ --tw-bg-opacity: 1;
4504
+ background-color: rgb(217 119 6 / var(--tw-bg-opacity, 1));
4505
+ }
4506
+
4475
4507
  .bg-black {
4476
4508
  --tw-bg-opacity: 1;
4477
4509
  background-color: rgb(0 0 0 / var(--tw-bg-opacity, 1));
@@ -4822,6 +4854,11 @@ input:checked + .toggle-switch .toggle-slider:before {
4822
4854
  padding-right: 0px;
4823
4855
  }
4824
4856
 
4857
+ .px-1\.5 {
4858
+ padding-left: 0.375rem;
4859
+ padding-right: 0.375rem;
4860
+ }
4861
+
4825
4862
  .px-2 {
4826
4863
  padding-left: 0.5rem;
4827
4864
  padding-right: 0.5rem;
@@ -4916,6 +4953,10 @@ input:checked + .toggle-switch .toggle-slider:before {
4916
4953
  padding-bottom: 1rem;
4917
4954
  }
4918
4955
 
4956
+ .pb-6 {
4957
+ padding-bottom: 1.5rem;
4958
+ }
4959
+
4919
4960
  .pl-3 {
4920
4961
  padding-left: 0.75rem;
4921
4962
  }
@@ -4952,6 +4993,10 @@ input:checked + .toggle-switch .toggle-slider:before {
4952
4993
  padding-top: 5rem;
4953
4994
  }
4954
4995
 
4996
+ .pt-3 {
4997
+ padding-top: 0.75rem;
4998
+ }
4999
+
4955
5000
  .pt-4 {
4956
5001
  padding-top: 1rem;
4957
5002
  }
@@ -4994,6 +5039,11 @@ input:checked + .toggle-switch .toggle-slider:before {
4994
5039
  line-height: 2.25rem;
4995
5040
  }
4996
5041
 
5042
+ .text-6xl {
5043
+ font-size: 3.75rem;
5044
+ line-height: 1;
5045
+ }
5046
+
4997
5047
  .text-base {
4998
5048
  font-size: 1rem;
4999
5049
  line-height: 1.5rem;
@@ -5051,6 +5101,10 @@ input:checked + .toggle-switch .toggle-slider:before {
5051
5101
  line-height: 1.5rem;
5052
5102
  }
5053
5103
 
5104
+ .leading-relaxed {
5105
+ line-height: 1.625;
5106
+ }
5107
+
5054
5108
  .tracking-wide {
5055
5109
  letter-spacing: 0.025em;
5056
5110
  }
@@ -5059,6 +5113,21 @@ input:checked + .toggle-switch .toggle-slider:before {
5059
5113
  letter-spacing: 0.05em;
5060
5114
  }
5061
5115
 
5116
+ .text-amber-400 {
5117
+ --tw-text-opacity: 1;
5118
+ color: rgb(251 191 36 / var(--tw-text-opacity, 1));
5119
+ }
5120
+
5121
+ .text-amber-600 {
5122
+ --tw-text-opacity: 1;
5123
+ color: rgb(217 119 6 / var(--tw-text-opacity, 1));
5124
+ }
5125
+
5126
+ .text-amber-700 {
5127
+ --tw-text-opacity: 1;
5128
+ color: rgb(180 83 9 / var(--tw-text-opacity, 1));
5129
+ }
5130
+
5062
5131
  .text-blue-400 {
5063
5132
  --tw-text-opacity: 1;
5064
5133
  color: rgb(96 165 250 / var(--tw-text-opacity, 1));
@@ -5129,6 +5198,11 @@ input:checked + .toggle-switch .toggle-slider:before {
5129
5198
  color: rgb(74 222 128 / var(--tw-text-opacity, 1));
5130
5199
  }
5131
5200
 
5201
+ .text-green-500 {
5202
+ --tw-text-opacity: 1;
5203
+ color: rgb(34 197 94 / var(--tw-text-opacity, 1));
5204
+ }
5205
+
5132
5206
  .text-green-600 {
5133
5207
  --tw-text-opacity: 1;
5134
5208
  color: rgb(22 163 74 / var(--tw-text-opacity, 1));
@@ -5144,6 +5218,11 @@ input:checked + .toggle-switch .toggle-slider:before {
5144
5218
  color: rgb(22 101 52 / var(--tw-text-opacity, 1));
5145
5219
  }
5146
5220
 
5221
+ .text-green-900 {
5222
+ --tw-text-opacity: 1;
5223
+ color: rgb(20 83 45 / var(--tw-text-opacity, 1));
5224
+ }
5225
+
5147
5226
  .text-red-400 {
5148
5227
  --tw-text-opacity: 1;
5149
5228
  color: rgb(248 113 113 / var(--tw-text-opacity, 1));
@@ -5417,12 +5496,6 @@ input:checked + .toggle-switch .toggle-slider:before {
5417
5496
  transition-duration: 150ms;
5418
5497
  }
5419
5498
 
5420
- .transition-shadow {
5421
- transition-property: box-shadow;
5422
- transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
5423
- transition-duration: 150ms;
5424
- }
5425
-
5426
5499
  .transition-transform {
5427
5500
  transition-property: transform;
5428
5501
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
@@ -5624,6 +5697,21 @@ input:checked + .toggle-switch .toggle-slider:before {
5624
5697
  border-color: rgb(156 163 175 / var(--tw-border-opacity, 1));
5625
5698
  }
5626
5699
 
5700
+ .hover\:border-green-300:hover {
5701
+ --tw-border-opacity: 1;
5702
+ border-color: rgb(134 239 172 / var(--tw-border-opacity, 1));
5703
+ }
5704
+
5705
+ .hover\:border-red-300:hover {
5706
+ --tw-border-opacity: 1;
5707
+ border-color: rgb(252 165 165 / var(--tw-border-opacity, 1));
5708
+ }
5709
+
5710
+ .hover\:bg-amber-700:hover {
5711
+ --tw-bg-opacity: 1;
5712
+ background-color: rgb(180 83 9 / var(--tw-bg-opacity, 1));
5713
+ }
5714
+
5627
5715
  .hover\:bg-blue-100:hover {
5628
5716
  --tw-bg-opacity: 1;
5629
5717
  background-color: rgb(219 234 254 / var(--tw-bg-opacity, 1));
@@ -5684,6 +5772,11 @@ input:checked + .toggle-switch .toggle-slider:before {
5684
5772
  background-color: rgb(107 114 128 / var(--tw-bg-opacity, 1));
5685
5773
  }
5686
5774
 
5775
+ .hover\:bg-green-100:hover {
5776
+ --tw-bg-opacity: 1;
5777
+ background-color: rgb(220 252 231 / var(--tw-bg-opacity, 1));
5778
+ }
5779
+
5687
5780
  .hover\:bg-green-50:hover {
5688
5781
  --tw-bg-opacity: 1;
5689
5782
  background-color: rgb(240 253 244 / var(--tw-bg-opacity, 1));
@@ -5748,6 +5841,11 @@ input:checked + .toggle-switch .toggle-slider:before {
5748
5841
  --tw-gradient-to: #1d4ed8 var(--tw-gradient-to-position);
5749
5842
  }
5750
5843
 
5844
+ .hover\:text-amber-800:hover {
5845
+ --tw-text-opacity: 1;
5846
+ color: rgb(146 64 14 / var(--tw-text-opacity, 1));
5847
+ }
5848
+
5751
5849
  .hover\:text-blue-600:hover {
5752
5850
  --tw-text-opacity: 1;
5753
5851
  color: rgb(37 99 235 / var(--tw-text-opacity, 1));
@@ -6065,10 +6163,6 @@ input:checked + .toggle-switch .toggle-slider:before {
6065
6163
  margin-bottom: 1.5rem;
6066
6164
  }
6067
6165
 
6068
- .sm\:mb-8 {
6069
- margin-bottom: 2rem;
6070
- }
6071
-
6072
6166
  .sm\:ml-16 {
6073
6167
  margin-left: 4rem;
6074
6168
  }
@@ -12,6 +12,7 @@ module EasyAdmin
12
12
  include ActionView::Helpers::NumberHelper
13
13
  include EasyAdmin::DashboardsHelper
14
14
  include EasyAdmin::FieldsHelper
15
+ include EasyAdmin::Permissions::Component if defined?(EasyAdmin::Permissions::Component)
15
16
 
16
17
  # Add method to access all Rails helpers if needed
17
18
  def helpers
@@ -2,9 +2,10 @@ module EasyAdmin
2
2
  class FormTabsComponent < Phlex::HTML
3
3
  include EasyAdmin::FieldsHelper
4
4
 
5
- def initialize(resource_class:, form:)
5
+ def initialize(resource_class:, form:, record: nil)
6
6
  @resource_class = resource_class
7
7
  @form = form
8
+ @record = record
8
9
  end
9
10
 
10
11
  def view_template
@@ -120,7 +121,7 @@ module EasyAdmin
120
121
  div(class: "grid grid-cols-1 gap-6") do
121
122
  tab[:fields].each do |field|
122
123
  div do
123
- unsafe_raw render_field(field, action: :form, form: @form)
124
+ unsafe_raw render_field(field, action: :form, form: @form, record: @record)
124
125
  end
125
126
  end
126
127
  end
@@ -158,6 +159,8 @@ module EasyAdmin
158
159
  '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>'
159
160
  when :info
160
161
  '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>'
162
+ when :shield
163
+ '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/></svg>'
161
164
  else
162
165
  '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>'
163
166
  end
@@ -191,7 +191,11 @@ module EasyAdmin
191
191
  end
192
192
 
193
193
  def user_role
194
- @current_user&.respond_to?(:role) ? @current_user.role.capitalize : "Administrator"
194
+ if @current_user&.respond_to?(:role) && @current_user.role
195
+ @current_user.role.name || @current_user.role.slug || "Administrator"
196
+ else
197
+ "No Role Assigned"
198
+ end
195
199
  end
196
200
 
197
201
  # SVG Icons
@@ -0,0 +1,254 @@
1
+ module EasyAdmin
2
+ module Permissions
3
+ class UserRoleAssignmentComponent < EasyAdmin::BaseComponent
4
+ def initialize(user:, form: nil)
5
+ @user = user
6
+ @form = form
7
+ @current_role = user&.respond_to?(:role) ? user.role : nil
8
+
9
+ # Get actual permissions from permissions_cache
10
+ @user_permissions_cache = get_user_permissions_from_cache(user)
11
+
12
+ # Get all available resources and generate permissions dynamically
13
+ @available_resources = EasyAdmin::Permissions.available_resources
14
+ @all_permissions = generate_permissions_from_resources
15
+ end
16
+
17
+ def view_template
18
+ div(class: "space-y-6") do
19
+ # Current Role Section
20
+ render_current_role_section
21
+
22
+ # Permissions Section
23
+ if @form
24
+ render_permissions_form_section
25
+ else
26
+ render_permissions_display_section
27
+ end
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def render_current_role_section
34
+ div(class: "bg-white border border-gray-200 rounded-lg p-6") do
35
+ div(class: "flex items-center justify-between mb-4") do
36
+ h3(class: "text-lg font-medium text-gray-900") { "Role Assignment" }
37
+
38
+ if @current_role
39
+ span(class: "inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800") do
40
+ @current_role.name
41
+ end
42
+ else
43
+ span(class: "inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-800") do
44
+ "No role assigned"
45
+ end
46
+ end
47
+ end
48
+
49
+ if @current_role&.description.present?
50
+ p(class: "text-gray-600") { @current_role.description }
51
+ end
52
+ end
53
+ end
54
+
55
+ # Show enabled permissions (read-only view)
56
+ def render_permissions_display_section
57
+ div(class: "bg-white border border-gray-200 rounded-lg p-6") do
58
+ h3(class: "text-lg font-medium text-gray-900 mb-4") { "Current Permissions" }
59
+
60
+ enabled_permissions = @user_permissions_cache.select { |k, v| v == "true" }
61
+
62
+ if enabled_permissions.any?
63
+ div(class: "grid grid-cols-1 md:grid-cols-2 gap-4") do
64
+ @available_resources.each do |resource|
65
+ plural_resource = resource.pluralize
66
+ resource_permissions = enabled_permissions.select { |k, _| k.start_with?("#{plural_resource}:") }
67
+ next if resource_permissions.empty?
68
+
69
+ div(class: "border border-green-200 bg-green-50 rounded-lg p-4") do
70
+ h4(class: "font-medium text-green-900 mb-2") { plural_resource.humanize.titleize }
71
+ div(class: "space-y-1") do
72
+ resource_permissions.each do |permission_name, _|
73
+ action = permission_name.split(':').last
74
+ span(class: "inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800 mr-2 mb-1") do
75
+ action.humanize
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ else
83
+ p(class: "text-gray-500 italic") { "No permissions currently assigned." }
84
+ end
85
+ end
86
+ end
87
+
88
+ # Show permission form (editable)
89
+ def render_permissions_form_section
90
+ div(class: "bg-white border border-gray-200 rounded-lg p-6") do
91
+ h3(class: "text-lg font-medium text-gray-900 mb-4") { "Manage Permissions" }
92
+ p(class: "text-sm text-gray-600 mb-6") { "Click to toggle permissions for each resource:" }
93
+
94
+ div(class: "space-y-4", data: { controller: "permission-toggle", permission_toggle_user_id_value: @user&.id }) do
95
+ @available_resources.each do |resource|
96
+ plural_resource = resource.pluralize
97
+ render_resource_permission_group(plural_resource)
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ def render_resource_permission_group(resource)
104
+ div(class: "border border-gray-200 bg-white rounded-lg p-4") do
105
+ div(class: "flex items-center justify-between mb-4") do
106
+ div(class: "flex items-center") do
107
+ h4(class: "text-md font-medium text-gray-900 capitalize") { resource.humanize }
108
+ end
109
+
110
+ div(class: "flex items-center space-x-2") do
111
+ span(class: "inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-700") do
112
+ "4 permissions"
113
+ end
114
+ # Toggle all button for this resource
115
+ button(
116
+ type: "button",
117
+ class: "text-xs px-2 py-1 bg-gray-200 text-gray-700 hover:bg-gray-300 rounded transition-colors",
118
+ data: {
119
+ action: "click->permission-toggle#toggleAllForResource",
120
+ resource_type: resource
121
+ }
122
+ ) do
123
+ "Toggle All"
124
+ end
125
+ end
126
+ end
127
+
128
+ # Permissions grid with old card style (2 per row)
129
+ div(class: "grid grid-cols-1 md:grid-cols-2 gap-3") do
130
+ %w[read create update delete].each do |action|
131
+ permission_name = "#{resource}:#{action}"
132
+ is_granted = permission_granted?(permission_name)
133
+
134
+ render_permission_toggle_card(permission_name, action, is_granted)
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+ def render_permission_toggle_card(permission_name, action, is_granted)
141
+ div(
142
+ class: "permission-card cursor-pointer transition-all duration-200 #{permission_card_classes(is_granted)}",
143
+ data: {
144
+ action: "click->permission-toggle#togglePermission",
145
+ permission_toggle_target: "permissionCard",
146
+ permission_name: permission_name,
147
+ granted: is_granted.to_s,
148
+ resource_type: permission_name.split(':').first
149
+ }
150
+ ) do
151
+ div(class: "flex items-start p-3") do
152
+ # Permission icon
153
+ div(class: "flex-shrink-0 mr-3") do
154
+ unsafe_raw permission_icon_svg(is_granted)
155
+ end
156
+
157
+ # Permission details
158
+ div(class: "flex-1 min-w-0") do
159
+ div(class: "mb-1") do
160
+ span(class: "text-sm font-medium #{is_granted ? 'text-green-900' : 'text-gray-900'} capitalize") do
161
+ action.humanize
162
+ end
163
+ end
164
+
165
+ # Permission description
166
+ p(class: "text-xs #{is_granted ? 'text-green-700' : 'text-gray-600'} leading-relaxed") do
167
+ permission_description(action)
168
+ end
169
+
170
+ # Permission name (technical)
171
+ p(class: "text-xs #{is_granted ? 'text-green-600' : 'text-gray-500'} font-mono mt-1") do
172
+ permission_name
173
+ end
174
+ end
175
+ end
176
+
177
+ # Hidden input for form submission
178
+ if @form
179
+ model_name = @form.object.class.name.underscore
180
+ field_name = "#{model_name}[permissions_cache][#{permission_name}]"
181
+ input(
182
+ type: "hidden",
183
+ name: field_name,
184
+ value: is_granted.to_s,
185
+ data: { permission_toggle_target: "hiddenInput" }
186
+ )
187
+ end
188
+ end
189
+ end
190
+
191
+ def generate_permissions_from_resources
192
+ permissions = {}
193
+ @available_resources.each do |resource|
194
+ # Pluralize resource name to match permissions_cache format
195
+ plural_resource = resource.pluralize
196
+ %w[read create update delete].each do |action|
197
+ permission_name = "#{plural_resource}:#{action}"
198
+ permissions[permission_name] = {
199
+ name: permission_name,
200
+ resource: plural_resource,
201
+ action: action
202
+ }
203
+ end
204
+ end
205
+ permissions
206
+ end
207
+
208
+ def get_user_permissions_from_cache(user)
209
+ return {} unless user&.permissions_cache.present?
210
+
211
+ case user.permissions_cache
212
+ when Hash
213
+ user.permissions_cache
214
+ when String
215
+ JSON.parse(user.permissions_cache) rescue {}
216
+ else
217
+ {}
218
+ end
219
+ end
220
+
221
+ def permission_card_classes(is_granted)
222
+ if is_granted
223
+ "bg-green-50 border border-green-200 hover:border-green-300 hover:bg-green-100"
224
+ else
225
+ "bg-red-50 border border-red-200 hover:border-red-300 hover:bg-red-100"
226
+ end
227
+ end
228
+
229
+ def permission_granted?(permission_name)
230
+ permission_value = @user_permissions_cache[permission_name]
231
+ # Handle string "true", boolean true, and string "1"
232
+ permission_value == "true" || permission_value == true || permission_value == "1"
233
+ end
234
+
235
+ def permission_description(action)
236
+ descriptions = {
237
+ 'read' => 'View and list records',
238
+ 'create' => 'Create new records',
239
+ 'update' => 'Edit existing records',
240
+ 'delete' => 'Remove records'
241
+ }
242
+ descriptions[action] || action.humanize
243
+ end
244
+
245
+ def permission_icon_svg(is_granted)
246
+ if is_granted
247
+ '<svg class="w-5 h-5 text-green-500 mt-0.5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path></svg>'
248
+ else
249
+ '<svg class="w-5 h-5 text-red-500 mt-0.5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>'
250
+ end
251
+ end
252
+ end
253
+ end
254
+ end