cm-admin 2.1.4 → 2.2.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.
@@ -62,6 +62,21 @@ end
62
62
  4. Include `CmRole` in the `config.included_models` section of `config/initializers/zcm_admin.rb`.
63
63
  5. Assign `cm_role_id` to `1` for any user in the `User` Model, and use that user to log in.
64
64
 
65
+ ## Setting up scopes
66
+
67
+ By default, `Full Access` scopes is added to each permission item. To add additional scopes, use the following syntax:
68
+
69
+ ```ruby
70
+ ...
71
+ cm_admin do
72
+ actions only: []
73
+ set_icon "fa fa-user"
74
+ set_policy_scopes [{scope_name: 'test_supplier_filter', display_name: 'By Test Supplier'}]
75
+ cm_index do
76
+ page_title 'User'
77
+ end
78
+ end
79
+
65
80
  ## Overriding Policies
66
81
 
67
82
  By default, roles and policies are enabled for all models in the application. To override a policy, use the following syntax:
@@ -90,3 +105,24 @@ end
90
105
  ```
91
106
 
92
107
  This structure helps ensure that your application's role and permission management is both flexible and secure.
108
+
109
+
110
+ ## Permission based fields
111
+
112
+ We can apply permission logic to display a field on the interface. You can do this with the following syntax.
113
+
114
+ ```ruby
115
+ ...
116
+ tab :details, '' do
117
+ row do
118
+ cm_show_section 'Details' do
119
+ field :status, field_type: :tag, tag_class: Item::STATUS_TAG_COLOR, display_if: -> (record) {
120
+ scoped_model = CmAdmin::ItemPolicy::ArchiveScope.new(Current.user, ::Item).resolve
121
+ return scoped_model.find_by(id: record.id).present?
122
+ }
123
+ end
124
+ end
125
+ end
126
+
127
+ ```
128
+
@@ -31,11 +31,12 @@ module CmAdmin
31
31
 
32
32
  attr_accessor :available_actions, :actions_set, :available_fields, :additional_permitted_fields,
33
33
  :current_action, :params, :filters, :available_tabs, :icon_name, :bulk_actions, :display_name,
34
- :policy_scopes, :override_policy, :alerts
34
+ :policy_scopes, :override_policy, :alerts, :sort_columns, :default_sort_column, :default_sort_direction
35
35
  attr_reader :name, :ar_model, :is_visible_on_sidebar, :importer
36
36
 
37
37
  def initialize(entity, &block)
38
38
  @name = entity.name
39
+ @display_name = entity.name
39
40
  @ar_model = entity
40
41
  @is_visible_on_sidebar = true
41
42
  @icon_name = 'fa fa-th-large'
@@ -48,7 +49,9 @@ module CmAdmin
48
49
  @params = nil
49
50
  @override_policy = false
50
51
  @filters ||= []
51
- @policy_scopes ||= [{display_name: 'Full Access', scope_name: 'all'}]
52
+ @policy_scopes ||= [{ display_name: 'Full Access', scope_name: 'all' }]
53
+ @sort_columns ||= []
54
+ @default_sort_direction ||= 'asc'
52
55
  @alerts = []
53
56
  instance_eval(&block) if block_given?
54
57
  actions unless @actions_set
@@ -65,28 +68,28 @@ module CmAdmin
65
68
 
66
69
  def custom_controller_action(action_name, params)
67
70
  current_action = CmAdmin::Models::Action.find_by(self, name: action_name.to_s)
68
- if current_action
69
- @current_action = current_action
70
- @ar_object = @ar_model.name.classify.constantize.find(params[:id])
71
- if @current_action.child_records
72
- child_records = @ar_object.send(@current_action.child_records)
73
- @associated_model = CmAdmin::Model.find_by(name: @ar_model.reflect_on_association(@current_action.child_records).klass.name)
74
- if child_records.is_a? ActiveRecord::Relation
75
- @associated_ar_object = filter_by(params, child_records)
76
- else
77
- @associated_ar_object = child_records
78
- end
79
- return @ar_object, @associated_model, @associated_ar_object
80
- end
81
- return @ar_object
71
+ return unless current_action
72
+
73
+ @current_action = current_action
74
+ @ar_object = @ar_model.name.classify.constantize.find(params[:id])
75
+ if @current_action.child_records
76
+ child_records = @ar_object.send(@current_action.child_records)
77
+ @associated_model = CmAdmin::Model.find_by(name: @ar_model.reflect_on_association(@current_action.child_records).klass.name)
78
+ @associated_ar_object = if child_records.is_a? ActiveRecord::Relation
79
+ filter_by(params, child_records)
80
+ else
81
+ child_records
82
+ end
83
+ return @ar_object, @associated_model, @associated_ar_object
82
84
  end
85
+ @ar_object
83
86
  end
84
87
 
85
88
  # Insert into actions according to config block
86
89
  def actions(only: [], except: [])
87
90
  acts = CmAdmin::DEFAULT_ACTIONS.keys
88
- acts = acts & (Array.new << only).flatten if only.present?
89
- acts = acts - (Array.new << except).flatten if except.present?
91
+ acts &= ([] << only).flatten if only.present?
92
+ acts -= ([] << except).flatten if except.present?
90
93
  acts.each do |act|
91
94
  action_defaults = CmAdmin::DEFAULT_ACTIONS[act]
92
95
  @available_actions << CmAdmin::Models::Action.new(name: act.to_s, verb: action_defaults[:verb], path: action_defaults[:path])
@@ -106,7 +109,7 @@ module CmAdmin
106
109
  @icon_name = name
107
110
  end
108
111
 
109
- def override_pundit_policy(override_status=false)
112
+ def override_pundit_policy(override_status = false)
110
113
  @override_policy = override_status
111
114
  end
112
115
 
@@ -114,7 +117,7 @@ module CmAdmin
114
117
  @display_name = name
115
118
  end
116
119
 
117
- def permit_additional_fields(fields=[])
120
+ def permit_additional_fields(fields = [])
118
121
  @additional_permitted_fields = fields
119
122
  end
120
123
 
@@ -130,17 +133,17 @@ module CmAdmin
130
133
  @display_name.present? ? @display_name : @name
131
134
  end
132
135
 
133
- def set_policy_scopes(scopes=[])
134
- @policy_scopes = ([{display_name: 'Full Access', scope_name: 'all'}] + scopes).uniq
136
+ def set_policy_scopes(scopes = [])
137
+ @policy_scopes = ([{ display_name: 'Full Access', scope_name: 'all' }] + scopes).uniq
135
138
  end
136
139
 
137
140
  # Shared between export controller and resource controller
138
141
  def filter_params(params)
139
142
  # OPTIMIZE: Need to check if we can permit the filter_params in a better way
140
- date_columns = self.filters.select{|x| x.filter_type.eql?(:date)}.map(&:db_column_name)
141
- range_columns = self.filters.select{|x| x.filter_type.eql?(:range)}.map(&:db_column_name)
142
- single_select_columns = self.filters.select{|x| x.filter_type.eql?(:single_select)}.map(&:db_column_name)
143
- multi_select_columns = self.filters.select{|x| x.filter_type.eql?(:multi_select)}.map{|x| Hash["#{x.db_column_name}", []]}
143
+ date_columns = filters.select { |x| x.filter_type.eql?(:date) }.map(&:db_column_name)
144
+ range_columns = filters.select { |x| x.filter_type.eql?(:range) }.map(&:db_column_name)
145
+ single_select_columns = filters.select { |x| x.filter_type.eql?(:single_select) }.map(&:db_column_name)
146
+ multi_select_columns = filters.select { |x| x.filter_type.eql?(:multi_select) }.map { |x| Hash["#{x.db_column_name}", []] }
144
147
 
145
148
  params.require(:filters).permit(:search, date: date_columns, range: range_columns, single_select: single_select_columns, multi_select: multi_select_columns) if params[:filters]
146
149
  end
@@ -150,74 +153,79 @@ module CmAdmin
150
153
  # Controller defined for each model
151
154
  # If model is User, controller will be UsersController
152
155
  def define_controller
153
- klass = Class.new(CmAdmin::ResourceController) do
154
- include Pundit::Authorization
155
- rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
156
-
157
- $available_actions.each do |action|
158
- define_method action.name.to_sym do
159
- # controller_name & action_name from ActionController
160
- @model = CmAdmin::Model.find_by(name: controller_name.classify)
161
- @model.params = params
162
- @action = CmAdmin::Models::Action.find_by(@model, name: action_name)
163
- @model.current_action = @action
164
- send(@action.controller_action_name, params)
165
- # @ar_object = @model.try(@action.parent || action_name, params)
156
+ if $available_actions.present?
157
+ klass = Class.new(CmAdmin::ResourceController) do
158
+ include Pundit::Authorization
159
+ rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
160
+
161
+ $available_actions.each do |action|
162
+ define_method action.name.to_sym do
163
+ # controller_name & action_name from ActionController
164
+ @model = CmAdmin::Model.find_by(name: controller_name.classify)
165
+ @model.params = params
166
+ @action = CmAdmin::Models::Action.find_by(@model, name: action_name)
167
+ @model.current_action = @action
168
+ send(@action.controller_action_name, params)
169
+ # @ar_object = @model.try(@action.parent || action_name, params)
170
+ end
166
171
  end
167
- end
168
172
 
169
- def pundit_user
170
- Current.user
171
- end
173
+ def pundit_user
174
+ Current.user
175
+ end
172
176
 
173
- private
177
+ private
174
178
 
175
- def user_not_authorized
176
- flash[:alert] = 'You are not authorized to perform this action.'
177
- redirect_to CmAdmin::Engine.mount_path + '/access-denied'
179
+ def user_not_authorized
180
+ flash[:alert] = 'You are not authorized to perform this action.'
181
+ redirect_to CmAdmin::Engine.mount_path + '/access-denied'
182
+ end
178
183
  end
179
- end if $available_actions.present?
184
+ end
180
185
  CmAdmin.const_set "#{@name}Controller", klass
181
186
  end
182
187
 
183
188
  def define_pundit_policy(ar_model)
184
- klass = Class.new(ApplicationPolicy) do
189
+ if $available_actions.present?
190
+ klass = Class.new(ApplicationPolicy) do
191
+ $available_actions.each do |action|
192
+ define_method "#{action.name}?".to_sym do
193
+ return false unless Current.user.respond_to?(:cm_role_id)
194
+ return false if Current.user.cm_role.nil?
195
+
196
+ Current.user.cm_role.cm_permissions.where(action_name: action.name, ar_model_name: ar_model.name).present?
197
+ end
198
+ end
199
+ end
200
+ end
201
+ policy = CmAdmin.const_set "#{ar_model.name}Policy", klass
185
202
 
186
- $available_actions.each do |action|
187
- define_method "#{action.name}?".to_sym do
203
+ $available_actions.each do |action|
204
+ next if %w[custom_action_modal custom_action create update].include?(action.name)
188
205
 
189
- return false unless Current.user.respond_to?(:cm_role_id)
190
- return false if Current.user.cm_role.nil?
206
+ klass = Class.new(policy) do
207
+ def initialize(user, scope)
208
+ @user = user
209
+ @scope = scope
210
+ end
191
211
 
192
- Current.user.cm_role.cm_permissions.where(action_name: action.name, ar_model_name: ar_model.name).present?
212
+ define_method :resolve do
213
+ # action_name = Current.request_params.dig("action")
214
+ permission = Current.user.cm_role.cm_permissions.find_by(action_name: action.name, ar_model_name: ar_model.name)
215
+ if permission.present? && permission.scope_name.present?
216
+ scope.send(permission.scope_name)
217
+ else
218
+ scope.all
219
+ end
193
220
  end
194
- end
195
221
 
196
- end if $available_actions.present?
197
- policy = CmAdmin.const_set "#{ar_model.name}Policy", klass
222
+ private
198
223
 
199
- klass = Class.new(policy) do
200
- def initialize(user, scope)
201
- @user = user
202
- @scope = scope
224
+ attr_reader :user, :scope
203
225
  end
204
-
205
- define_method :resolve do
206
- action_name = Current.request_params.dig("action")
207
- permission = Current.user.cm_role.cm_permissions.find_by(action_name: action_name, ar_model_name: ar_model.name)
208
- if permission.present? && permission.scope_name.present?
209
- scope.send(permission.scope_name)
210
- else
211
- scope.all
212
- end
213
- end
214
-
215
- private
216
-
217
- attr_reader :user, :scope
218
- end
219
226
 
220
- policy.const_set 'Scope', klass
227
+ policy.const_set "#{action.name.classify}Scope", klass
228
+ end
221
229
  end
222
230
  end
223
231
  end
@@ -319,6 +319,7 @@ module CmAdmin
319
319
  @filters << CmAdmin::Models::Filter.new(db_column_name:, filter_type:, options:)
320
320
  end
321
321
 
322
+ # @deprecated: use {#sortable_columns} instead of this method
322
323
  # Set sort direction for filters
323
324
  # @param direction [Symbol] the direction of sort, +:asc+, +:desc+
324
325
  # @example Setting sort direction
@@ -329,6 +330,7 @@ module CmAdmin
329
330
  @current_action.sort_direction = direction.to_sym if @current_action
330
331
  end
331
332
 
333
+ # @deprecated: use {#sortable_columns} instead of this method
332
334
  # Set sort column for filters
333
335
  # @param column [Symbol] the column name
334
336
  # @example Setting sort column
@@ -374,6 +376,27 @@ module CmAdmin
374
376
  def alert_box(header: nil, body: nil, type: nil, partial: nil, display_if: nil, html_attrs: {})
375
377
  @section_fields << CmAdmin::Models::Alert.new(header, body, type, partial:, display_if:, html_attrs:)
376
378
  end
379
+
380
+ # Configure sortable columns for model
381
+ # @param columns [Array] the array of hash for column and display name
382
+ # @example Sortable Columns
383
+ # sortable_columns([{column: 'id', display_name: 'ID'}, {column: 'updated_at', display_name: 'Last Updated At'}])
384
+ #
385
+ # @example Sortable Columns with default column and direction
386
+ # sortable_columns([{column: 'id', display_name: 'ID', default: true, default_direction: 'desc'}, {column: 'updated_at', display_name: 'Last Updated At'}])
387
+ def sortable_columns(columns)
388
+ @sort_columns = columns
389
+ default_column = columns.filter do |column|
390
+ column.key?(:default) || column.key?(:default_direction)
391
+ end
392
+ raise ArgumentError, 'only one column can be default' if default_column.size > 1
393
+ return if default_column.blank?
394
+
395
+ default_column = default_column.first
396
+
397
+ @default_sort_column = default_column[:column]
398
+ @default_sort_direction = default_column[:default_direction] if default_column[:default_direction].present?
399
+ end
377
400
  end
378
401
  end
379
402
  end
@@ -1,3 +1,3 @@
1
1
  module CmAdmin
2
- VERSION = '2.1.4'
2
+ VERSION = '2.2.0'
3
3
  end
@@ -39,7 +39,8 @@ module CmAdmin
39
39
 
40
40
  def custom_action_items(custom_action, current_action_name)
41
41
  if custom_action.name.present? && policy([:cm_admin, @model.name.classify.constantize]).send(:"#{custom_action.name}?")
42
- if custom_action.display_if.call(@ar_object)
42
+ scoped_model = "CmAdmin::#{@model.name}Policy::#{custom_action.name.classify}Scope".constantize.new(Current.user, @model.name.constantize).resolve
43
+ if custom_action.display_if.call(@ar_object) && scoped_model.find_by(id: params[:id])
43
44
  case custom_action.display_type
44
45
  when :icon_only
45
46
  custom_action_icon(custom_action, current_action_name)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cm-admin
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.4
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael
@@ -14,7 +14,7 @@ authors:
14
14
  autorequire:
15
15
  bindir: exe
16
16
  cert_chain: []
17
- date: 2024-09-24 00:00:00.000000000 Z
17
+ date: 2024-10-01 00:00:00.000000000 Z
18
18
  dependencies:
19
19
  - !ruby/object:Gem::Dependency
20
20
  name: caxlsx_rails
@@ -345,7 +345,6 @@ files:
345
345
  - app/assets/stylesheets/cm_admin/dependency/bootstrap/scss/utilities/_api.scss
346
346
  - app/assets/stylesheets/cm_admin/dependency/bootstrap/scss/vendor/_rfs.scss
347
347
  - app/assets/stylesheets/cm_admin/dependency/flatpickr.min.css
348
- - app/assets/stylesheets/cm_admin/dependency/fontawesome.all.css
349
348
  - app/assets/stylesheets/cm_admin/dependency/jquery-jgrowl.min.css
350
349
  - app/assets/stylesheets/cm_admin/helpers/_mixins.scss
351
350
  - app/assets/stylesheets/cm_admin/helpers/_variable.scss
@@ -387,6 +386,7 @@ files:
387
386
  - app/views/cm_admin/main/_show_as_drawer.html.slim
388
387
  - app/views/cm_admin/main/_show_content.html.slim
389
388
  - app/views/cm_admin/main/_show_section.html.slim
389
+ - app/views/cm_admin/main/_sort.html.slim
390
390
  - app/views/cm_admin/main/_table.html.slim
391
391
  - app/views/cm_admin/main/_tabs.html.slim
392
392
  - app/views/cm_admin/main/_top_navbar.html.slim