Class: Panda::Core::NavigationRegistry
- Inherits:
-
Object
- Object
- Panda::Core::NavigationRegistry
- Defined in:
- lib/panda/core/navigation_registry.rb
Overview
Declarative registry for extending admin sidebar navigation.
Gems and host apps register navigation sections and items at boot time,
and the registry merges them with the base admin_navigation_items lambda
at render time. This avoids the fragile lambda-wrapping pattern where each
gem must capture the existing proc, create a new one, and manipulate the
resulting array with index lookups and insertions.
Path resolution
path:— auto-prefixed withadmin_path(e.g. path: "feature_flags" becomes "/admin/feature_flags")url:— used as-is, for full URLs or absolute pathspath_helper:— a route helper symbol resolved at build time viahelperstarget:— HTML target attribute (e.g. "_blank"), omitted whennil
Visibility and filtering
visible:— lambda receiving user, evaluated at build time; item/section hidden when falsefilter()— post-build removal of items/sections by label, conditional on a user-receiving lambda
Usage
Convenience methods on Configuration delegate here, so the
typical usage is inside a Panda::Core.configure block:
# In a host app initializer or engine:
Panda::Core.configure do |config|
# Add a new section with items, positioned after "Website"
config. "Members",
icon: "fa-solid fa-users",
after: "Website" do |section|
section.item "Onboarding", path: "members/onboarding"
end
# Add an item to an existing section
config. "Feature Flags",
section: "Settings",
path: "feature_flags"
# Permission-based visibility
config. "Suggestions",
section: "Tools",
path: "cms/content_suggestions",
visible: -> (user) { user.admin? || user.(:review_content) }
# Post-build filter (hides for non-admins)
config. "Roles",
visible: -> (user) { user.admin? }
# Bottom section (rendered with UserMenuComponent)
config. "Notifications",
position: :bottom do |s|
s.item "All", path: "notifications"
end
end
You can also call the class methods directly:
Panda::Core::NavigationRegistry.section("Tools", icon: "fa-solid fa-wrench")
Panda::Core::NavigationRegistry.item("Database", section: "Tools", path: "tools/database")
Build order
NavigationRegistry.build is called once per request by the sidebar component:
- The base
admin_navigation_itemslambda is called (backward compatible) - Registered sections are inserted (skipped if a section with the same label already exists in the base)
- Registered items are appended to their target sections
- Legacy
admin_user_menu_itemsare migrated into the "My Account" bottom section - Visibility lambdas are evaluated; items/sections hidden when
visible:returns false - Post-build filters are applied
Paths are resolved at build time, so admin_path can be configured after
registrations are made.
Defined Under Namespace
Classes: SectionContext
Class Attribute Summary collapse
-
.filters ⇒ Object
readonly
Returns the value of attribute filters.
-
.items ⇒ Object
readonly
Returns the value of attribute items.
-
.sections ⇒ Object
readonly
Returns the value of attribute sections.
Class Method Summary collapse
-
.build(user, helpers: nil) ⇒ Array<Hash>
Build the final navigation array for the current user.
-
.filter(label, visible:) ⇒ Object
Register a post-build filter that conditionally hides items by label.
-
.item(label, section:, path: nil, url: nil, target: nil, visible: nil, permission: nil, before: nil, after: nil, method: nil, button_options: {}, path_helper: nil) ⇒ Object
Register an item to be appended to a named section.
-
.reset! ⇒ Object
Clear all registrations and re-register defaults (for test isolation).
-
.section(label, icon: nil, after: nil, before: nil, visible: nil, permission: nil, position: :top) {|SectionContext| ... } ⇒ Object
Register a new navigation section.
Class Attribute Details
.filters ⇒ Object (readonly)
Returns the value of attribute filters.
142 143 144 |
# File 'lib/panda/core/navigation_registry.rb', line 142 def filters @filters end |
.items ⇒ Object (readonly)
Returns the value of attribute items.
142 143 144 |
# File 'lib/panda/core/navigation_registry.rb', line 142 def items @items end |
.sections ⇒ Object (readonly)
Returns the value of attribute sections.
142 143 144 |
# File 'lib/panda/core/navigation_registry.rb', line 142 def sections @sections end |
Class Method Details
.build(user, helpers: nil) ⇒ Array<Hash>
Build the final navigation array for the current user.
Called once per request by the sidebar component. Combines the base
admin_navigation_items lambda with all registered sections, items,
and filters.
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 |
# File 'lib/panda/core/navigation_registry.rb', line 254 def build(user, helpers: nil) base = Panda::Core.config.&.call(user) || [] admin_path = Panda::Core.config.admin_path # Convert permission: keys from base lambda items into _visible lambdas (base) # Apply registered sections @sections.each do |section| # Skip if a section with this label already exists in base next if base.any? { |item| item[:label] == section[:label] } entry = {label: section[:label], icon: section[:icon], position: section[:position], _visible: section[:visible]} if section[:items].any? entry[:children] = section[:items].map { |item| resolved = resolve_item(item, admin_path, helpers) resolved[:_visible] = item[:visible] resolved } end insert_section(base, entry, after: section[:after], before: section[:before]) end # Apply registered items to their target sections @items.each do |item| target_section = base.find { |s| s[:label] == item[:section] } next unless target_section target_section[:children] ||= [] resolved = resolve_item(item, admin_path, helpers) resolved[:_visible] = item[:visible] insert_item(target_section[:children], resolved, before: item[:before], after: item[:after]) end # Migrate legacy admin_user_menu_items into the "My Account" bottom section legacy_items = Panda::Core.config. if legacy_items&.any? my_account = base.find { |s| s[:label] == "My Account" && s[:position] == :bottom } if my_account legacy_items.each do || next if [:path].blank? && [:url].blank? resolved = {label: [:label], path: [:path]} resolved[:_visible] = [:visible] # Insert before Logout if it exists logout_idx = my_account[:children]&.index { |c| c[:label] == "Logout" } if logout_idx my_account[:children].insert(logout_idx, resolved) else (my_account[:children] ||= []) << resolved end end end end # Apply section-level visible: — remove sections hidden for this user base.reject! { |entry| entry[:_visible] && !entry[:_visible].call(user) } # Apply item-level visible: — remove children hidden for this user base.each do |entry| next unless entry[:children] entry[:children].reject! { |child| child[:_visible] && !child[:_visible].call(user) } end # Apply registered filters — walk all sections and children @filters.each do |filter| base.reject! { |entry| entry[:label] == filter[:label] && !filter[:visible].call(user) } base.each do |entry| next unless entry[:children] entry[:children].reject! { |child| child[:label] == filter[:label] && !filter[:visible].call(user) } end end # Clean up internal keys before returning base.each do |entry| entry.delete(:_visible) entry[:position] ||= :top entry[:children]&.each { |child| child.delete(:_visible) } end base end |
.filter(label, visible:) ⇒ Object
Register a post-build filter that conditionally hides items by label. Walks all sections and their children during build().
241 242 243 |
# File 'lib/panda/core/navigation_registry.rb', line 241 def filter(label, visible:) @filters << {label: label, visible: visible} end |
.item(label, section:, path: nil, url: nil, target: nil, visible: nil, permission: nil, before: nil, after: nil, method: nil, button_options: {}, path_helper: nil) ⇒ Object
Register an item to be appended to a named section.
The target section can come from either the base lambda or a previously registered section. If the section doesn't exist at build time, the item is silently skipped.
rubocop:disable Metrics/ParameterLists
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 |
# File 'lib/panda/core/navigation_registry.rb', line 218 def item(label, section:, path: nil, url: nil, target: nil, visible: nil, permission: nil, before: nil, after: nil, method: nil, button_options: {}, path_helper: nil) effective_visible = visible || () @items << { label: label, section: section, path: path, url: url, target: target, visible: effective_visible, before: before, after: after, method: method, button_options: , path_helper: path_helper } end |
.reset! ⇒ Object
Clear all registrations and re-register defaults (for test isolation). This ensures the default "My Account" bottom section is always present after a reset, preventing order-dependent test failures.
341 342 343 344 345 346 |
# File 'lib/panda/core/navigation_registry.rb', line 341 def reset! @sections = [] @items = [] @filters = [] Panda::Core.config.send(:register_default_user_menu) end |
.section(label, icon: nil, after: nil, before: nil, visible: nil, permission: nil, position: :top) {|SectionContext| ... } ⇒ Object
Register a new navigation section.
If a section with the same label already exists in the base navigation
(from the admin_navigation_items lambda), the registration is skipped
at build time — this prevents duplicate sections.
rubocop:disable Metrics/ParameterLists
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
# File 'lib/panda/core/navigation_registry.rb', line 173 def section(label, icon: nil, after: nil, before: nil, visible: nil, permission: nil, position: :top, &block) context = SectionContext.new yield context if block effective_visible = visible || () @sections << { label: label, icon: icon, after: after, before: before, visible: effective_visible, position: position, items: context.items } end |