Has One Tab
Overview
The Has One Tab feature enables displaying a has_one associated record directly within a tab on a parent model's show page. Instead of a paginated list (used for has_many), it renders a single-record profile view with Edit, Delete, and custom action support.
When the associated record does not exist yet, an empty state is shown with a button to create one. When the user lacks permission to view the record, a permission denied state is shown.
How It Works
Adding relationship_type: :has_one to a tab declaration changes the tab behaviour:
| State | What Renders |
|---|---|
| Record exists and user has access | Profile view with fields, Edit/Delete/custom actions |
| Record does not exist | Empty state with an Add button that pre-fills the association |
| User lacks permission (Pundit scope) | Permission denied message |
The tab badge shows 0 or 1 instead of a record count.
DSL
Pass relationship_type: :has_one alongside layout_type: 'cm_association_show' in the tab declaration.
Parameters
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
tab_name | String | Symbol | Yes | — | Tab label shown in the navigation. |
action_name | String | Yes | — | Unique action name (used in routes and policy scopes). |
associated_model | String | Yes | — | The has_one association name on the parent model. |
layout_type | String | Yes | — | Must be 'cm_association_show' for has-one tabs. |
relationship_type | Symbol | Yes | :has_many | Set to :has_one to enable single-record display. |
show_count | Boolean | No | true | Set to false to hide the badge, or leave true to show 0/1. |
display_if | Proc | No | -> { true } | Lambda returning a boolean to conditionally show the tab. |
allow_create_action | Proc | No | -> { true } | Lambda controlling whether the Add button appears on the empty state. |
eager_load_associations | Array | No | nil | Associations to eager-load when fetching the record. |
Updated Method Signature
tab(tab_name, action_name,
associated_model: nil,
layout_type: nil,
relationship_type: :has_many,
show_count: true,
display_if: nil,
allow_create_action: -> { true },
eager_load_associations: nil,
&block)
Example
1. Model Setup
class User < ApplicationRecord
has_one :profile
end
class Profile < ApplicationRecord
belongs_to :user
end
2. cm_admin Configuration
# app/models/cm_user.rb
cm_admin do
cm_show do
tab :profile, 'profile',
associated_model: 'profile',
layout_type: 'cm_association_show',
relationship_type: :has_one,
show_count: false
end
end
# app/models/cm_profile.rb
cm_admin do
cm_show do
cm_section 'Personal Details' do
field :first_name
field :last_name
field :bio
end
end
end
3. Permissions
Add the action to the Pundit policy and policy scope for the associated model. For example:
# app/policies/cm_admin/profile_policy.rb
class CmAdmin::ProfilePolicy < CmAdmin::ApplicationPolicy
class ProfileScope < Scope
def resolve
scope.where(user: user)
end
end
def show? = true
def edit? = true
def destroy? = true
end
After adding the tab, update the permissions for the relevant roles in the Permissions tab inside cm_admin.
Behaviour Details
Empty State
When no associated record exists, the tab renders an empty state with an Add button. Clicking the button opens the creation form for the associated model and pre-fills:
associated_id— the parent record's IDassociated_class— the parent model nameforeign_key— the foreign key columnpolymorphic_name— set automatically for polymorphic associations
Permission Denied State
If a Pundit policy scope exists for the action and the record does not pass the scope check, the permission denied state is shown instead of the record. No record data is leaked.
Tab Badge
For has_one tabs, the badge displays 0 (no record) or 1 (record exists), instead of a full count query.
Actions Bar
The profile view renders an actions bar above the record fields with:
- Edit button — links to the associated model's edit page with a
referrerparam so the user is returned to the parent show page after saving. - Custom actions dropdown — respects
display_type(:button,:modal,:form_modal,:link). - Delete button — triggers the destroy modal; shown only when the user has permission.
Comparison: has_many vs has_one Tab
| Behaviour | has_many (default) | has_one |
|---|---|---|
layout_type | cm_association_index | cm_association_show |
relationship_type | :has_many (default) | :has_one |
| Renders | Paginated table / card list | Single record profile view |
| No record state | Empty table | Empty state with Add button |
| Permission denied state | Standard Pundit scope on list | Dedicated permission denied view |
| Tab badge | Record count | 0 or 1 |
show_count | Usually true | Usually false |