Skip to main content

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:

StateWhat Renders
Record exists and user has accessProfile view with fields, Edit/Delete/custom actions
Record does not existEmpty 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

NameTypeRequiredDefaultDescription
tab_nameString | SymbolYesTab label shown in the navigation.
action_nameStringYesUnique action name (used in routes and policy scopes).
associated_modelStringYesThe has_one association name on the parent model.
layout_typeStringYesMust be 'cm_association_show' for has-one tabs.
relationship_typeSymbolYes:has_manySet to :has_one to enable single-record display.
show_countBooleanNotrueSet to false to hide the badge, or leave true to show 0/1.
display_ifProcNo-> { true }Lambda returning a boolean to conditionally show the tab.
allow_create_actionProcNo-> { true }Lambda controlling whether the Add button appears on the empty state.
eager_load_associationsArrayNonilAssociations 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 ID
  • associated_class — the parent model name
  • foreign_key — the foreign key column
  • polymorphic_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 referrer param 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

Behaviourhas_many (default)has_one
layout_typecm_association_indexcm_association_show
relationship_type:has_many (default):has_one
RendersPaginated table / card listSingle record profile view
No record stateEmpty tableEmpty state with Add button
Permission denied stateStandard Pundit scope on listDedicated permission denied view
Tab badgeRecord count0 or 1
show_countUsually trueUsually false