cccux 0.1.0 โ†’ 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.
@@ -2,9 +2,9 @@
2
2
 
3
3
  module Cccux
4
4
  class Ability
5
- include CanCan::Ability
5
+ include CanCan::Ability
6
6
 
7
- def initialize(user, context = nil)
7
+ def initialize(user, context = nil)
8
8
  user ||= User.new # guest user (not logged in)
9
9
  @context = context || {}
10
10
 
@@ -60,24 +60,33 @@ module Cccux
60
60
  def apply_access_ability(role_ability, permission, model_class, user)
61
61
  action = permission.action.to_sym
62
62
 
63
+ # Handle action aliases (read includes show and index)
64
+ actions_to_grant = case action
65
+ when :read
66
+ [:read, :show, :index]
67
+ when :update
68
+ [:update, :edit]
69
+ when :create
70
+ [:create, :new]
71
+ when :destroy
72
+ [:destroy, :delete]
73
+ else
74
+ [action]
75
+ end
76
+
63
77
  # For User resource, keep owned logic for now
64
78
  if permission.subject == 'User'
65
- if role_ability.context == 'owned' || (role_ability.owned && user&.persisted?)
66
- apply_owned_ability(action, model_class, user, role_ability)
79
+ if role_ability.context == 'owned' || role_ability.owned
80
+ actions_to_grant.each { |act| apply_owned_ability(act, model_class, user, role_ability) }
67
81
  else
68
- can action, model_class
82
+ actions_to_grant.each { |act| can act, model_class }
69
83
  end
70
84
  else
71
- # For all other resources, use global/owned (contextual is now handled by owned with configuration)
72
- case role_ability.access_type
73
- when 'global'
74
- can action, model_class
75
- when 'owned'
76
- apply_owned_ability(action, model_class, user, role_ability)
85
+ # For all other resources, use global/owned logic based on context field
86
+ if role_ability.context == 'owned' || role_ability.owned
87
+ actions_to_grant.each { |act| apply_owned_ability(act, model_class, user, role_ability) }
77
88
  else
78
- # Default: deny access if access_type is not recognized
79
- Rails.logger.warn "CCCUX: Unknown access_type '#{role_ability.access_type}' for #{model_class.name}, denying access"
80
- # Don't grant any permissions - CanCanCan denies by default
89
+ actions_to_grant.each { |act| can act, model_class }
81
90
  end
82
91
  end
83
92
  end
@@ -89,13 +98,31 @@ module Cccux
89
98
  if ownership_model && user&.persisted?
90
99
  # Parse conditions (should be a JSON string or nil)
91
100
  conditions = role_ability.ownership_conditions.present? ? JSON.parse(role_ability.ownership_conditions) : {}
92
- foreign_key = conditions["foreign_key"] || (model_class.name.foreign_key)
101
+ foreign_key = conditions["foreign_key"]
93
102
  user_key = conditions["user_key"] || "user_id"
94
- # Find all records owned by user via the join model
95
- owned_ids = ownership_model.where(user_key => user.id).pluck(foreign_key)
96
- can action, model_class, id: owned_ids if owned_ids.any?
103
+
104
+ # Require foreign_key to be explicitly specified when using ownership model
105
+ if foreign_key.present?
106
+ # Find all records owned by user via the join model
107
+ owned_ids = ownership_model.where(user_key => user.id).pluck(foreign_key)
108
+ if owned_ids.any?
109
+ # Check if the target model has the foreign key column
110
+ if model_class.column_names.include?(foreign_key)
111
+ # Direct ownership: model has the foreign key (e.g., Comment has post_id)
112
+ can action, model_class, foreign_key.to_sym => owned_ids
113
+ else
114
+ # Indirect ownership: model doesn't have the foreign key, use primary key
115
+ # This means the foreign key in the join table refers to the target model's primary key
116
+ can action, model_class, id: owned_ids
117
+ end
118
+ else
119
+ can action, model_class, id: []
120
+ end
121
+ else
122
+ # Fall back to no access if foreign_key is not specified
123
+ can action, model_class, id: []
124
+ end
97
125
  else
98
- Rails.logger.warn "CCCUX: Invalid ownership_source #{role_ability.ownership_source} for #{model_class.name}"
99
126
  can action, model_class, id: []
100
127
  end
101
128
  # 2. Model custom owned_by?
@@ -112,30 +139,53 @@ module Cccux
112
139
  else
113
140
  can action, model_class, scoped_records
114
141
  end
115
- # 4. Standard user_id
142
+ # 4. Special case for User model (self-ownership)
143
+ elsif model_class == User
144
+ can action, model_class, id: user.id
145
+ # 5. Standard user_id
116
146
  elsif model_class.column_names.include?('user_id')
117
147
  can action, model_class, user_id: user.id
118
- # 5. Standard creator_id
148
+ # 6. Standard creator_id
119
149
  elsif model_class.column_names.include?('creator_id')
120
150
  can action, model_class, creator_id: user.id
151
+ # 7. Dynamic ownership check for individual records
121
152
  else
122
- # Default: deny access when no ownership pattern is found
123
- Rails.logger.warn "CCCUX: No ownership pattern found for #{model_class.name}, denying access"
124
- # Don't grant any permissions - CanCanCan denies by default
153
+ # For cases where we need to check individual record attributes
154
+ can action, model_class do |record|
155
+ if record.respond_to?(:creator_id)
156
+ record.creator_id == user.id
157
+ elsif record.respond_to?(:user_id)
158
+ record.user_id == user.id
159
+ else
160
+ false
161
+ end
162
+ end
125
163
  end
126
164
  end
127
165
 
128
166
  def resolve_model_class(subject)
129
- # Handle namespaced models
130
- if subject.include?('::')
131
- subject.constantize
167
+ # Try to resolve the model class in a robust way
168
+ candidates = []
169
+ if subject.include?("::")
170
+ candidates << subject
171
+ candidates << subject.split("::").last
132
172
  else
133
- # Try to find the model in the host app
134
- Object.const_get(subject)
173
+ candidates << subject
174
+ candidates << "Cccux::#{subject}"
175
+ end
176
+
177
+ # Add more candidates for common patterns
178
+ candidates << subject.split("::").last if subject.include?("::")
179
+ candidates << subject.gsub("Cccux::", "") if subject.start_with?("Cccux::")
180
+
181
+ candidates.each do |candidate|
182
+ begin
183
+ klass = candidate.constantize
184
+ return klass
185
+ rescue NameError => e
186
+ next
187
+ end
135
188
  end
136
- rescue NameError
137
- # If the model doesn't exist, we can't define permissions for it
138
- Rails.logger.warn "CCCUX: Model '#{subject}' not found, skipping permission"
139
189
  nil
140
190
  end
141
191
  end
@@ -0,0 +1,5 @@
1
+ module Cccux
2
+ class Post < ApplicationRecord
3
+ belongs_to :user
4
+ end
5
+ end
@@ -9,10 +9,11 @@ module Cccux
9
9
  has_many :role_abilities, dependent: :destroy, class_name: 'Cccux::RoleAbility'
10
10
  has_many :ability_permissions, through: :role_abilities, class_name: 'Cccux::AbilityPermission'
11
11
 
12
- validates :name, presence: true, uniqueness: true
12
+ validates :name, presence: true
13
13
  validates :priority, presence: true, numericality: { only_integer: true, greater_than: 0 }
14
14
 
15
15
  after_initialize :set_default_priority, if: :new_record?
16
+ validate :name_uniqueness_case_insensitive
16
17
 
17
18
  scope :active, -> { where(active: true) }
18
19
  scope :ordered, -> { order(:priority, :name) }
@@ -81,6 +82,23 @@ module Cccux
81
82
  end
82
83
  end
83
84
 
85
+ # Generate slug from name
86
+ def slug
87
+ name.parameterize.underscore if name.present?
88
+ end
89
+
90
+ # Case insensitive name validation
91
+ def name_uniqueness_case_insensitive
92
+ return unless name.present?
93
+
94
+ existing_role = self.class.where('LOWER(name) = ?', name.downcase)
95
+ existing_role = existing_role.where.not(id: id) if persisted?
96
+
97
+ if existing_role.exists?
98
+ errors.add(:name, 'has already been taken')
99
+ end
100
+ end
101
+
84
102
  private
85
103
 
86
104
  def set_default_priority
@@ -5,6 +5,9 @@ module Cccux
5
5
  belongs_to :role, class_name: 'Cccux::Role'
6
6
  belongs_to :ability_permission, class_name: 'Cccux::AbilityPermission'
7
7
 
8
+ # Delegate methods to ability_permission
9
+ delegate :action, :subject, :active?, to: :ability_permission, allow_nil: true
10
+
8
11
  # Simplified access types: global, owned
9
12
  ACCESS_TYPES = %w[global owned].freeze
10
13
 
@@ -15,7 +15,7 @@ module Cccux
15
15
 
16
16
  # Instance methods for user authorization
17
17
  def has_role?(role_name)
18
- cccux_roles.active.exists?(name: role_name)
18
+ cccux_user_roles.active.joins(:role).where(cccux_roles: { name: role_name }).exists?
19
19
  end
20
20
 
21
21
  def has_any_role?(*role_names)
@@ -59,7 +59,7 @@ module Cccux
59
59
  end
60
60
 
61
61
  def role_names
62
- cccux_roles.active.pluck(:name)
62
+ cccux_user_roles.active.joins(:role).pluck('cccux_roles.name')
63
63
  end
64
64
 
65
65
  def highest_priority_role
@@ -1,78 +1,31 @@
1
- <%= turbo_frame_tag role.new_record? ? "new_role_form" : "role_#{role.id}" do %>
2
- <div style="background-color: white; border: 1px solid #dee2e6; border-radius: 8px; padding: 1.5rem;">
3
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
4
- <h3 style="margin: 0; color: #495057;"><%= role.new_record? ? 'Create New Role' : 'Edit Role' %></h3>
5
- <%= link_to "Cancel", cccux.roles_path,
6
- data: { turbo_frame: role.new_record? ? "new_role_form" : "role_#{role.id}" },
7
- style: "color: #6c757d; text-decoration: none; font-size: 0.9rem;" %>
1
+ <%= form_with(model: role, local: true) do |form| %>
2
+ <% if role.errors.any? %>
3
+ <div id="error_explanation">
4
+ <h2><%= pluralize(role.errors.count, "error") %> prohibited this role from being saved:</h2>
5
+ <ul>
6
+ <% role.errors.each do |error| %>
7
+ <li><%= error.full_message %></li>
8
+ <% end %>
9
+ </ul>
8
10
  </div>
9
-
10
- <%= form_with model: [role], url: role.new_record? ? cccux.roles_path : cccux.role_path(role), local: false do |form| %>
11
- <% if role.errors.any? %>
12
- <div style="background-color: #f8d7da; border: 1px solid #721c24; padding: 1rem; margin-bottom: 1rem; border-radius: 4px;">
13
- <h4 style="margin-top: 0; color: #721c24;"><%= pluralize(role.errors.count, "error") %> prohibited this role from being saved:</h4>
14
- <ul style="margin-bottom: 0; color: #721c24;">
15
- <% role.errors.full_messages.each do |message| %>
16
- <li><%= message %></li>
17
- <% end %>
18
- </ul>
19
- </div>
20
- <% end %>
11
+ <% end %>
21
12
 
22
- <div style="margin-bottom: 1.5rem;">
23
- <%= form.label :name, style: "display: block; margin-bottom: 0.5rem; font-weight: bold; color: #495057;" %>
24
- <%= form.text_field :name,
25
- style: "width: 100%; padding: 0.75rem; border: 1px solid #ced4da; border-radius: 4px; font-size: 1rem;",
26
- placeholder: "Enter role name (e.g., Manager, Support, etc.)" %>
27
- </div>
28
-
29
- <div style="margin-bottom: 1.5rem;">
30
- <%= form.label :description, style: "display: block; margin-bottom: 0.5rem; font-weight: bold; color: #495057;" %>
31
- <%= form.text_area :description, rows: 3,
32
- style: "width: 100%; padding: 0.75rem; border: 1px solid #ced4da; border-radius: 4px; font-size: 1rem; resize: vertical;",
33
- placeholder: "Describe what this role can do and its responsibilities..." %>
34
- </div>
35
-
36
- <div style="margin-bottom: 1.5rem;">
37
- <label style="display: flex; align-items: center; cursor: pointer;">
38
- <%= form.check_box :active, { checked: role.active.nil? ? true : role.active },
39
- style: "margin-right: 0.5rem; transform: scale(1.2);" %>
40
- <span style="font-weight: bold; color: #495057;">Active Role</span>
41
- </label>
42
- <small style="color: #6c757d; font-size: 0.9rem; margin-top: 0.25rem; display: block; margin-left: 1.5rem;">
43
- Only active roles can be assigned to users. Inactive roles are hidden from user assignment forms.
44
- </small>
45
- </div>
13
+ <div class="field">
14
+ <%= form.label :name %>
15
+ <%= form.text_field :name %>
16
+ </div>
46
17
 
47
- <div style="margin-bottom: 1.5rem;">
48
- <%= form.label :priority, style: "display: block; margin-bottom: 0.5rem; font-weight: bold; color: #495057;" %>
49
- <%= form.number_field :priority,
50
- style: "width: 100%; padding: 0.75rem; border: 1px solid #ced4da; border-radius: 4px; font-size: 1rem;",
51
- placeholder: "Enter priority (lower numbers = higher priority)",
52
- min: 1,
53
- value: role.priority || 50 %>
54
- <small style="color: #6c757d; font-size: 0.9rem; margin-top: 0.25rem; display: block;">
55
- Priority determines role hierarchy. Lower numbers = higher priority (e.g., Admin=1, Manager=10, User=50)
56
- </small>
57
- </div>
18
+ <div class="field">
19
+ <%= form.label :description %>
20
+ <%= form.text_area :description %>
21
+ </div>
58
22
 
59
- <% if role.new_record? %>
60
- <div style="background-color: #e7f3ff; border: 1px solid #b8daff; padding: 1rem; border-radius: 4px; margin-bottom: 1.5rem;">
61
- <h4 style="margin-top: 0; color: #004085; font-size: 1rem;">๐Ÿ“ Note about Permissions</h4>
62
- <p style="margin-bottom: 0; color: #004085; font-size: 0.9rem;">
63
- After creating this role, you'll be able to assign specific permissions (read, create, update, destroy)
64
- for each resource (Orders, Users, etc.) on the role's edit page.
65
- </p>
66
- </div>
67
- <% end %>
23
+ <div class="field">
24
+ <%= form.label :priority %>
25
+ <%= form.number_field :priority %>
26
+ </div>
68
27
 
69
- <div style="display: flex; gap: 0.5rem;">
70
- <%= form.submit role.new_record? ? "Create Role" : "Update Role",
71
- style: "background-color: #007bff; color: white; padding: 0.75rem 1.5rem; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem;" %>
72
- <%= link_to "Cancel", cccux.roles_path,
73
- data: { turbo_frame: role.new_record? ? "new_role_form" : "role_#{role.id}" },
74
- style: "color: #6c757d; text-decoration: none; padding: 0.75rem 1rem; font-size: 1rem;" %>
75
- </div>
76
- <% end %>
28
+ <div class="actions">
29
+ <%= form.submit %>
77
30
  </div>
78
31
  <% end %>
@@ -1,14 +1,14 @@
1
1
  <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
2
2
  <h1>Edit Role: <%= @role.name %></h1>
3
3
  <div>
4
- <%= link_to "View Role", cccux.role_path(@role),
4
+ <%= link_to "View Role", role_path(@role),
5
5
  style: "background-color: #17a2b8; color: white; padding: 0.5rem 1rem; text-decoration: none; border-radius: 4px; margin-right: 0.5rem;" %>
6
- <%= link_to "Back to Roles", cccux.roles_path,
6
+ <%= link_to "Back to Roles", roles_path,
7
7
  style: "background-color: #6c757d; color: white; padding: 0.5rem 1rem; text-decoration: none; border-radius: 4px;" %>
8
8
  </div>
9
9
  </div>
10
10
 
11
- <%= form_with model: [@role], url: cccux.role_path(@role), local: true, method: :patch do |form| %>
11
+ <%= form_with model: [@role], url: role_path(@role), local: true, method: :patch do |form| %>
12
12
  <% if @role.errors.any? %>
13
13
  <div style="background-color: #f8d7da; border: 1px solid #721c24; padding: 1rem; margin-bottom: 2rem; border-radius: 4px;">
14
14
  <h4><%= pluralize(@role.errors.count, "error") %> prohibited this role from being saved:</h4>
@@ -297,13 +297,13 @@
297
297
  <div>
298
298
  <%= form.submit "Update Role",
299
299
  style: "background-color: #28a745; color: white; padding: 0.75rem 1.5rem; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; margin-right: 1rem;" %>
300
- <%= link_to "Cancel", cccux.role_path(@role),
300
+ <%= link_to "Cancel", role_path(@role),
301
301
  style: "color: #6c757d; text-decoration: none; padding: 0.75rem 1rem; font-size: 1rem;" %>
302
302
  </div>
303
303
 
304
304
  <% unless @role.users.any? %>
305
305
  <div>
306
- <%= link_to "Delete Role", cccux.role_path(@role),
306
+ <%= link_to "Delete Role", role_path(@role),
307
307
  data: { "turbo-method": :delete, "turbo-confirm": "Are you sure? This action cannot be undone." },
308
308
  style: "background-color: #dc3545; color: white; padding: 0.75rem 1.5rem; text-decoration: none; border-radius: 4px; font-size: 1rem;" %>
309
309
  </div>
@@ -1,7 +1,6 @@
1
1
  <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
2
2
  <h1>Role Management</h1>
3
3
  <%= link_to "New Role", cccux.new_role_path,
4
- data: { turbo_frame: "new_role_form" },
5
4
  style: "background-color: #007bff; color: white; padding: 0.5rem 1rem; text-decoration: none; border-radius: 4px;" %>
6
5
  </div>
7
6
 
@@ -19,16 +18,11 @@
19
18
  <% end %>
20
19
  </div>
21
20
 
22
- <!-- New Role Form Area -->
23
- <%= turbo_frame_tag "new_role_form" do %>
24
- <!-- Form will load here when "New Role" is clicked -->
25
- <% end %>
26
-
27
21
  <!-- Roles List -->
28
22
  <div style="margin-bottom: 1rem;">
29
23
  <h3 style="color: #495057; margin-bottom: 0.5rem;">Role Hierarchy</h3>
30
24
  <p style="color: #6c757d; font-size: 0.9rem; margin-bottom: 1.5rem;">
31
- <strong>Drag and drop</strong> roles to reorder their priority. Higher positions = higher priority.
25
+ Roles are listed by priority. Higher positions = higher priority.
32
26
  </p>
33
27
  </div>
34
28
 
@@ -45,7 +39,6 @@
45
39
  <h3>No roles found</h3>
46
40
  <p>Create your first role to get started.</p>
47
41
  <%= link_to "Create Role", cccux.new_role_path,
48
- data: { turbo_frame: "new_role_form" },
49
42
  style: "background-color: #007bff; color: white; padding: 0.5rem 1rem; text-decoration: none; border-radius: 4px;" %>
50
43
  </div>
51
44
  <% end %>
@@ -1,3 +1 @@
1
- <%= turbo_frame_tag "new_role_form" do %>
2
- <%= render 'form', role: @role %>
3
- <% end %>
1
+ <%= render 'form', role: @role %>
@@ -1,10 +1,10 @@
1
1
  <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
2
2
  <h1>Edit User: <%= @user.email %></h1>
3
- <%= link_to "โ† Back to User", cccux.user_path(@user),
3
+ <%= link_to "โ† Back to User", user_path(@user),
4
4
  style: "background-color: #6c757d; color: white; padding: 0.5rem 1rem; text-decoration: none; border-radius: 4px;" %>
5
5
  </div>
6
6
 
7
- <%= form_with model: [@user], url: cccux.user_path(@user), method: :patch, local: true do |form| %>
7
+ <%= form_with model: [@user], url: user_path(@user), method: :patch, local: true do |form| %>
8
8
  <% if @user.errors.any? %>
9
9
  <div style="background-color: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; padding: 1rem; border-radius: 4px; margin-bottom: 2rem;">
10
10
  <h4 style="margin-top: 0;">Please fix the following errors:</h4>
@@ -103,12 +103,12 @@
103
103
  <div>
104
104
  <%= form.submit "Update User",
105
105
  style: "background-color: #28a745; color: white; padding: 0.75rem 1.5rem; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; margin-right: 1rem;" %>
106
- <%= link_to "Cancel", cccux.user_path(@user),
106
+ <%= link_to "Cancel", user_path(@user),
107
107
  style: "color: #6c757d; text-decoration: none; padding: 0.75rem 1rem; font-size: 1rem;" %>
108
108
  </div>
109
109
 
110
110
  <div>
111
- <%= link_to "Delete User", cccux.user_path(@user),
111
+ <%= link_to "Delete User", user_path(@user),
112
112
  data: { "turbo-method": :delete, "turbo-confirm": "Are you sure? This will permanently delete the user and all their role assignments." },
113
113
  style: "background-color: #dc3545; color: white; padding: 0.75rem 1.5rem; text-decoration: none; border-radius: 4px; font-size: 1rem;" %>
114
114
  </div>
@@ -1,10 +1,10 @@
1
1
  <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
2
2
  <h1>Create New User</h1>
3
- <%= link_to "โ† Back to Users", cccux.users_path,
3
+ <%= link_to "โ† Back to Users", users_path,
4
4
  style: "background-color: #6c757d; color: white; padding: 0.5rem 1rem; text-decoration: none; border-radius: 4px;" %>
5
5
  </div>
6
6
 
7
- <%= form_with model: [@user], url: cccux.users_path, local: true do |form| %>
7
+ <%= form_with model: [@user], url: users_path, local: true do |form| %>
8
8
  <% if @user.errors.any? %>
9
9
  <div style="background-color: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; padding: 1rem; border-radius: 4px; margin-bottom: 2rem;">
10
10
  <h4 style="margin-top: 0;">Please fix the following errors:</h4>
@@ -16,7 +16,7 @@
16
16
  </div>
17
17
  <% end %>
18
18
 
19
- <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; margin-bottom: 2rem;">
19
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem;">
20
20
  <!-- User Information -->
21
21
  <div style="background-color: white; border: 1px solid #dee2e6; border-radius: 8px; padding: 1.5rem;">
22
22
  <h3 style="margin-top: 0; color: #495057;">User Information</h3>
@@ -31,7 +31,6 @@
31
31
  <%= form.label :password, style: "display: block; margin-bottom: 0.5rem; font-weight: bold; color: #495057;" %>
32
32
  <%= form.password_field :password,
33
33
  style: "width: 100%; padding: 0.75rem; border: 1px solid #ced4da; border-radius: 4px; font-size: 1rem;" %>
34
- <small style="color: #6c757d; font-size: 0.875rem;">Minimum 6 characters</small>
35
34
  </div>
36
35
 
37
36
  <div style="margin-bottom: 1.5rem;">
@@ -39,21 +38,32 @@
39
38
  <%= form.password_field :password_confirmation,
40
39
  style: "width: 100%; padding: 0.75rem; border: 1px solid #ced4da; border-radius: 4px; font-size: 1rem;" %>
41
40
  </div>
41
+
42
+ <div style="margin-bottom: 1.5rem;">
43
+ <%= form.label :first_name, style: "display: block; margin-bottom: 0.5rem; font-weight: bold; color: #495057;" %>
44
+ <%= form.text_field :first_name,
45
+ style: "width: 100%; padding: 0.75rem; border: 1px solid #ced4da; border-radius: 4px; font-size: 1rem;" %>
46
+ </div>
47
+
48
+ <div style="margin-bottom: 1.5rem;">
49
+ <%= form.label :last_name, style: "display: block; margin-bottom: 0.5rem; font-weight: bold; color: #495057;" %>
50
+ <%= form.text_field :last_name,
51
+ style: "width: 100%; padding: 0.75rem; border: 1px solid #ced4da; border-radius: 4px; font-size: 1rem;" %>
52
+ </div>
42
53
  </div>
43
54
 
44
55
  <!-- Role Assignment -->
45
56
  <div style="background-color: white; border: 1px solid #dee2e6; border-radius: 8px; padding: 1.5rem;">
46
57
  <h3 style="margin-top: 0; color: #495057;">Role Assignment</h3>
47
58
 
48
- <div style="background-color: #e7f3ff; border: 1px solid #b8daff; padding: 1rem; border-radius: 4px; margin-bottom: 1rem;">
49
- <p style="margin: 0; color: #004085; font-size: 0.9rem;">
50
- <strong>Note:</strong> Users automatically receive the "Basic User" role when created.
51
- You can assign additional roles here.
59
+ <div style="background-color: #fff3cd; border: 1px solid #ffeaa7; padding: 1rem; border-radius: 4px; margin-bottom: 1rem;">
60
+ <p style="margin: 0; color: #856404; font-size: 0.9rem;">
61
+ <strong>Note:</strong> Users will automatically receive the "Basic User" role.
52
62
  </p>
53
63
  </div>
54
64
 
55
65
  <% if @roles.any? %>
56
- <div style="max-height: 200px; overflow-y: auto; border: 1px solid #e9ecef; border-radius: 4px; padding: 0.5rem;">
66
+ <div style="max-height: 300px; overflow-y: auto; border: 1px solid #e9ecef; border-radius: 4px; padding: 0.5rem;">
57
67
  <% @roles.each do |role| %>
58
68
  <div style="padding: 0.5rem; border-bottom: 1px solid #f8f9fa; display: flex; align-items: center;">
59
69
  <%= check_box_tag "user[role_ids][]", role.id, false,
@@ -68,6 +78,15 @@
68
78
  </div>
69
79
  <% end %>
70
80
  </div>
81
+
82
+ <div style="margin-top: 1rem; padding: 1rem; background-color: #e7f3ff; border-radius: 4px;">
83
+ <h5 style="margin: 0 0 0.5rem 0; color: #004085;">Role Selection Tips:</h5>
84
+ <ul style="margin: 0; padding-left: 1.5rem; color: #004085; font-size: 0.9rem;">
85
+ <li>Check roles to assign them to this user</li>
86
+ <li>Users automatically keep their "Basic User" role</li>
87
+ <li>Role assignments can be modified later</li>
88
+ </ul>
89
+ </div>
71
90
  <% else %>
72
91
  <div style="text-align: center; padding: 2rem; color: #6c757d; font-style: italic;">
73
92
  No roles available. Create roles first.
@@ -77,18 +96,14 @@
77
96
  </div>
78
97
 
79
98
  <!-- Form Actions -->
80
- <div style="background-color: white; border: 1px solid #dee2e6; border-radius: 8px; padding: 1.5rem;">
99
+ <div style="background-color: white; border: 1px solid #dee2e6; border-radius: 8px; padding: 1.5rem; margin-top: 2rem;">
81
100
  <div style="display: flex; justify-content: space-between; align-items: center;">
82
101
  <div>
83
102
  <%= form.submit "Create User",
84
103
  style: "background-color: #28a745; color: white; padding: 0.75rem 1.5rem; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; margin-right: 1rem;" %>
85
- <%= link_to "Cancel", cccux.users_path,
104
+ <%= link_to "Cancel", users_path,
86
105
  style: "color: #6c757d; text-decoration: none; padding: 0.75rem 1rem; font-size: 1rem;" %>
87
106
  </div>
88
-
89
- <div style="color: #6c757d; font-size: 0.9rem;">
90
- <em>User will receive login credentials and can change password after first login</em>
91
- </div>
92
107
  </div>
93
108
  </div>
94
109
  <% end %>
@@ -7,8 +7,7 @@
7
7
  <%= csp_meta_tag %>
8
8
 
9
9
  <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
10
- <%= stylesheet_link_tag "stylesheets/cccux/application", "data-turbo-track": "reload" %>
11
- <%= javascript_importmap_tags %>
10
+ <%# Remove JavaScript for now to avoid asset pipeline issues in tests %>
12
11
 
13
12
  <!-- Sortable.js for drag and drop -->
14
13
  <script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
data/config/routes.rb CHANGED
@@ -2,8 +2,11 @@ Cccux::Engine.routes.draw do
2
2
  # Devise authentication routes - use different path to avoid conflicts
3
3
  # devise_for :users, class_name: 'Cccux::User', path: 'auth'
4
4
 
5
+ # Root route for the engine - goes to dashboard
5
6
  root 'dashboard#index'
6
- get '/test', to: 'simple#index'
7
+
8
+ # Dashboard route (alias for root)
9
+ get '/dashboard', to: 'dashboard#index', as: :dashboard
7
10
 
8
11
  # Model Discovery Routes
9
12
  get 'model-discovery', to: 'dashboard#model_discovery', as: :model_discovery
@@ -22,6 +25,7 @@ Cccux::Engine.routes.draw do
22
25
  end
23
26
 
24
27
  resources :roles do
28
+ resources :role_abilities, only: [:index, :create, :destroy]
25
29
  member do
26
30
  patch :toggle_active
27
31
  get :permissions
@@ -52,11 +56,7 @@ Cccux::Engine.routes.draw do
52
56
  end
53
57
  end
54
58
 
55
- resources :role_abilities, only: [:index, :create, :destroy] do
56
- collection do
57
- get :search
58
- end
59
- end
59
+
60
60
 
61
61
  # Catch-all route for any unmatched paths in CCCUX - redirect to home
62
62
  match '*unmatched', to: 'dashboard#not_found', via: :all
data/lib/cccux/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Cccux
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.1"
3
3
  end
data/lib/tasks/cccux.rake CHANGED
@@ -59,7 +59,15 @@ namespace :cccux do
59
59
 
60
60
  # Step 4: Run CCCUX migrations
61
61
  puts "๐Ÿ“‹ Step 4: Running CCCUX migrations..."
62
- Rake::Task['db:migrate'].invoke
62
+ begin
63
+ Rake::Task['db:migrate'].invoke
64
+ rescue RuntimeError => e
65
+ if e.message.include?("Don't know how to build task 'db:migrate'")
66
+ puts " โš ๏ธ Skipping migrations (not available in engine context)"
67
+ else
68
+ raise e
69
+ end
70
+ end
63
71
  puts "โœ… CCCUX migrations completed"
64
72
 
65
73
  # Step 5: Include CCCUX concern in User model
@@ -106,6 +114,19 @@ namespace :cccux do
106
114
  puts "๐Ÿ“š Need help? Check the CCCUX documentation or README"
107
115
  end
108
116
 
117
+ desc 'test:prepare - Prepare test database for CCCUX engine'
118
+ task 'test:prepare' => :environment do
119
+ puts "๐Ÿงช Preparing CCCUX test database..."
120
+
121
+ # Switch to test environment
122
+ Rails.env = 'test'
123
+
124
+ # Load schema into test database
125
+ system("cd #{Rails.root.join('..', '..')} && RAILS_ENV=test rails db:schema:load")
126
+
127
+ puts "โœ… Test database prepared"
128
+ end
129
+
109
130
  desc 'test - Test CCCUX + Devise integration'
110
131
  task test: :environment do
111
132
  puts "๐Ÿงช Testing CCCUX + Devise integration..."
@@ -661,7 +682,7 @@ namespace :cccux do
661
682
  layout_content = File.read(layout_path)
662
683
 
663
684
  # Check if footer is already included
664
- unless layout_content.include?('render "shared/footer"')
685
+ unless layout_content.include?("render 'shared/footer'")
665
686
  # Add footer before closing body tag
666
687
  if layout_content.include?('</body>')
667
688
  updated_content = layout_content.gsub(