Module: ApplicationHelper

Overview

View helper: application.

Constant Summary collapse

CF_WEB_ANALYTICS_SKIP_PATHS =

Paths kept OUT of Cloudflare Web Analytics: the account/cart/checkout funnel
(app telemetry, not the marketing site) and /support/request, which is
overwhelmingly email-security-scanner fetches of the link in support mail
rather than real visitors. Locale prefix optional.

%r{\A/(?:[a-z]{2}-[A-Z]{2}/)?(?:my_account|my_cart|accounts|payments|support/request)(?:/|\z)}
MARKDOWN_ALLOWED_TAGS =

Rails' default safe list plus the table tags/attributes it omits — docling
spec tables are the whole reason for rendering Markdown, and colspan /
rowspan carry their structure. Additive rather than hand-maintained so we
don't silently strip the 16 other tags Rails already considers safe
(abbr, cite, div, span, mark, small, …), several of which docling emits.

img/src are deliberately REMOVED: this content comes from third-party
PDFs, and a remote ![](http://…) would make a CRM viewer's browser fetch
an attacker-controlled URL, leaking their IP and the fact they opened the
page. Nothing is lost today — docling reduces images to <!-- image -->
placeholders (1,323 publications carry those; zero carry a remote image
URL). Rehost server-side first if inline images are ever wanted here.

(
  Rails::HTML5::SafeListSanitizer.allowed_tags - %w[img] + %w[table thead tbody tfoot tr th td]
).freeze
MARKDOWN_ALLOWED_ATTRIBUTES =

Attribute counterpart to MARKDOWN_ALLOWED_TAGS: Rails' default safe list
plus colspan/rowspan/align for docling's spec tables, minus src
(see MARKDOWN_ALLOWED_TAGS for why remote images are stripped).

(
  Rails::HTML5::SafeListSanitizer.allowed_attributes - %w[src] + %w[colspan rowspan align]
).freeze

Constants included from Www::SeoHelper

Www::SeoHelper::AWARDS, Www::SeoHelper::CA_ADDRESS, Www::SeoHelper::CA_BUSINESS_HOURS, Www::SeoHelper::CA_CONTACT_POINT, Www::SeoHelper::CA_CURRENCIES, Www::SeoHelper::CA_DESCRIPTION, Www::SeoHelper::CA_FOUNDING_DATE, Www::SeoHelper::CA_GLOBAL_LOCATION_NUMBER, Www::SeoHelper::CA_LEGAL_NAME, Www::SeoHelper::CA_LOCAL_BUSINESS, Www::SeoHelper::CA_ONLINE_STORE, Www::SeoHelper::CA_RETURN_POLICY, Www::SeoHelper::CA_SALES_DEPARTMENT, Www::SeoHelper::CA_SERVICE_AREA, Www::SeoHelper::CA_URL, Www::SeoHelper::CA_VAT_ID, Www::SeoHelper::CA_WAREHOUSE_DEPARTMENT, Www::SeoHelper::CA_WAREHOUSE_HOURS, Www::SeoHelper::COMPANY_EMAIL, Www::SeoHelper::COMPANY_LOGO, Www::SeoHelper::COMPANY_NAME, Www::SeoHelper::COMPANY_SLOGAN, Www::SeoHelper::EXPERTISE, Www::SeoHelper::FAX_NUMBER, Www::SeoHelper::GS1_COMPANY_PREFIX, Www::SeoHelper::ISO6523_CODE, Www::SeoHelper::PAYMENT_METHODS, Www::SeoHelper::PHONE_NUMBER, Www::SeoHelper::PRIMARY_NAICS, Www::SeoHelper::REFUND_TYPE, Www::SeoHelper::RETURN_FEES, Www::SeoHelper::RETURN_METHOD, Www::SeoHelper::RETURN_POLICY_CATEGORY, Www::SeoHelper::SECONDARY_NAICS, Www::SeoHelper::SOCIAL_PROFILES, Www::SeoHelper::US_ADDRESS, Www::SeoHelper::US_BUSINESS_HOURS, Www::SeoHelper::US_CONTACT_POINT, Www::SeoHelper::US_CURRENCIES, Www::SeoHelper::US_DESCRIPTION, Www::SeoHelper::US_FOUNDING_DATE, Www::SeoHelper::US_GLOBAL_LOCATION_NUMBER, Www::SeoHelper::US_IMAGE, Www::SeoHelper::US_LEGAL_NAME, Www::SeoHelper::US_LOCAL_BUSINESS, Www::SeoHelper::US_ONLINE_STORE, Www::SeoHelper::US_RETURN_POLICY, Www::SeoHelper::US_SALES_DEPARTMENT, Www::SeoHelper::US_SERVICE_AREA, Www::SeoHelper::US_TAX_ID, Www::SeoHelper::US_URL, Www::SeoHelper::US_WAREHOUSE_DEPARTMENT, Www::SeoHelper::US_WAREHOUSE_HOURS

Constants included from IconHelper

IconHelper::CUSTOM_ICON_MAP, IconHelper::CUSTOM_SVG_DIR, IconHelper::DEFAULT_FAMILY

Instance Method Summary collapse

Methods included from UppyUploaderHelper

#file_uploader, #image_uploader, #large_file_uploader_s3, #lead_sketch_uploader, #public_warranty_card_uploader, #rma_image_uploader, #rma_image_uploader_s3, #uppy_uploader, #video_uploader, #warranty_card_uploader

Methods included from Www::ImagesHelper

#image_asset_tag, #image_asset_url, #picture_asset_tag

Methods included from Www::SeoHelper

#add_page_schema, #add_webpage_schema, #canada?, #collect_schema, #company_social_links, #ensure_context_json, #json_ld_script_tag, #local_business_schema, #online_store_id, #online_store_schema, #page_main_entity, #page_main_entity_json, #render_auto_collection_page_schema, #render_collection_page_schema, #render_local_business_schema, #render_online_store_schema, #render_page_schemas, #render_page_video_schemas, #render_webpage_schema, #render_webpage_schema_with_collections, #usa?

Methods included from UrlsHelper

#catalog_breadcrumb_links, #catalog_link, #catalog_link_for_product_line, #catalog_link_for_sku, #cms_link, #delocalized_path, #path_to_sales_product_sku, #path_to_sales_product_sku_for_product_line, #path_to_sales_product_sku_for_product_line_slug, #product_line_from_catalog_link, #protocol_neutral_url, #sanitize_external_url, #valid_external_url?

Methods included from IconHelper

#account_nav_icon, #fa_icon, #star_rating_html

Instance Method Details

#better_number_to_currency(number, options = {}) ⇒ String?

Like number_to_currency, but strips insignificant zeros from whole-dollar amounts

Parameters:

  • number (Numeric)

    amount to format

  • options (Hash) (defaults to: {})

    options forwarded to Rails' +number_to_currency+
    (+:precision+ and +:strip_insignificant_zeros+ are overridden)

Options Hash (options):

  • :unit (String)

    currency unit symbol

  • :delimiter (String)

    thousands delimiter

  • :separator (String)

    decimal separator

Returns:

  • (String, nil)

    formatted currency string, or nil when number is nil



109
110
111
112
113
114
# File 'app/helpers/application_helper.rb', line 109

def better_number_to_currency(number, options = {})
  return unless number

  strip_insignificant_zeros = ((number * 100) % 100).zero? # e.g 209.00 = 20900 % 100 = 0 therefore we can strip zero. but 209.20 we wouldn't want to render as 209.2
  number_to_currency(number, options.merge(strip_insignificant_zeros:, precision: 2))
end

#cf_web_analytics?(path = request.path) ⇒ Boolean

Whether to emit the Cloudflare Web Analytics beacon for this request.

Production only — staging is its own Rails env, so it can't report into the
production RUM site. Path is the ONLY other dimension we gate on, and that is
deliberate: the beacon ships inside edge-cached HTML, and the path is part of
the cache key, so it varies safely per cached object. Visitor geo/consent
can NOT be decided server-side here — the same cached page is served to
everyone (see shared/_tracking_init, which is JS-driven for exactly this
reason).

Parameters:

  • path (String) (defaults to: request.path)

    request path, defaults to the current request's

Returns:

  • (Boolean)


35
36
37
# File 'app/helpers/application_helper.rb', line 35

def cf_web_analytics?(path = request.path)
  Rails.env.production? && !path.match?(CF_WEB_ANALYTICS_SKIP_PATHS)
end

#check_or_cross(value, options = {}) ⇒ String

Truthy = green check mark other wise red stop

Parameters:

  • value (Object)

    value coerced with +to_b+

  • options (Hash) (defaults to: {})

    icon options, forwarded to +fa_icon+

Options Hash (options):

  • :title (String)

    icon title attribute (default: "Yes"/"No")

  • :class (String)

    CSS classes (default: "text-green"/"text-red")

  • :style (String)

    inline style attribute (default: green/red color)

  • :check_icon (String)

    Font Awesome icon for truthy values (default: "check-circle")

  • :cross_icon (String)

    Font Awesome icon for falsy values (default: "ban")

Returns:

  • (String)

    HTML for the icon



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'app/helpers/application_helper.rb', line 132

def check_or_cross(value, options = {})
  if value.to_b
    options[:title] = 'Yes'
    options[:class] ||= 'text-green'
    options[:style] ||= 'color:green;font-size:1.2em'
    check_icon = options.delete(:check_icon) || 'check-circle'
    fa_icon(check_icon, **options)
  else
    options[:title] = 'No'
    options[:class] ||= 'text-red'
    options[:style] ||= 'color:red;font-size:1.2em'
    cross_icon = options.delete(:cross_icon) || 'ban'
    fa_icon(cross_icon, **options)
  end
end

#check_or_times(value, _options = {}) ⇒ String

Like +check_or_cross+, but uses a "times" icon for falsy values.

Parameters:

  • value (Object)

    value coerced with +to_b+

  • _options (Hash) (defaults to: {})

    unused; kept for signature parity with +check_or_cross+

Returns:

  • (String)

    HTML for the icon

See Also:



154
155
156
# File 'app/helpers/application_helper.rb', line 154

def check_or_times(value, _options = {})
  check_or_cross(value, cross_icon: 'times')
end

#embedded_tab_frame_idString

Frame id for views that are embeddable inside ANY tab regardless of caller.

Use for dual-purpose views (search_and_show, generic list pages) that render
full-screen when navigated directly but should slot into the calling tab when
fetched via Turbo-Frame: tab-content-*. Unlike tab_frame_id, this does
NOT gate on a parent_id route param — the embedded view doesn't know its
caller's resource.

Falls back to tab_frame_id (controller-derived default) when there's no
tab-content-* header, so direct navigation still works.

Returns:

  • (String)

    the caller's tab frame id, or the +tab_frame_id+ default



476
477
478
479
480
481
# File 'app/helpers/application_helper.rb', line 476

def embedded_tab_frame_id
  header = request.headers['Turbo-Frame']
  return header if header&.start_with?('tab-content-')

  tab_frame_id
end

#error_messages(object) ⇒ ActiveSupport::SafeBuffer?

Renders a danger card listing a record's validation errors.

Parameters:

  • object (ActiveModel::Model)

    record responding to +errors+

Returns:

  • (ActiveSupport::SafeBuffer, nil)

    error card HTML, or nil when the
    record has no errors



221
222
223
224
225
226
227
228
# File 'app/helpers/application_helper.rb', line 221

def error_messages(object)
  return unless object.errors.any?

   :div, class: 'card bg-danger' do
    (:div, "#{pluralize(object.errors.count, 'error')} prohibited this record from being saved:", class: 'card-title') +
      render_error_messages_list(object)
  end
end

#general_disclaimer_on_product_installation_and_local_codesString

The general disclaimer shown for product installation and local codes.

Returns:

  • (String)

    disclaimer text
    (GENERAL_DISCLAIMER_ON_PRODUCT_INSTALLATION_AND_LOCAL_CODES)



317
318
319
# File 'app/helpers/application_helper.rb', line 317

def general_disclaimer_on_product_installation_and_local_codes
  GENERAL_DISCLAIMER_ON_PRODUCT_INSTALLATION_AND_LOCAL_CODES
end

#markdown_to_html(text, tags: MARKDOWN_ALLOWED_TAGS, attributes: MARKDOWN_ALLOWED_ATTRIBUTES) ⇒ ActiveSupport::SafeBuffer

Render Markdown as sanitized HTML for display in admin/CRM views.

Kramdown's GFM parser (already a dependency) handles the tables, headings and
lists that docling emits when it extracts a PDF. Output is always run through
Rails' sanitize — Kramdown passes raw HTML in the source straight through,
and the Markdown here comes from third-party documents, not from us.

For Sunny's chat responses use Assistant::ResponseFormatter instead: it adds
code-block extraction, copy/preview buttons and citation handling on top.

The allowlist is overridable because the default one is tuned for
third-party PDF text (see MARKDOWN_ALLOWED_TAGS on why it drops images).
Content we author ourselves — a showcase story, say — can afford a wider
list; it passes its own rather than getting a second Markdown pipeline.

Examples:

Rendering third-party document text in a view

markdown_to_html(publication.search_text)

Widening the allowlist for content we author

markdown_to_html(story, tags: Assistant::ShowcaseToolBuilder::SHOWCASE_ALLOWED_TAGS)

Parameters:

  • text (String, nil)

    Markdown source.

  • tags (Set<String>, Array<String>) (defaults to: MARKDOWN_ALLOWED_TAGS)

    allowed element names.

  • attributes (Set<String>, Array<String>) (defaults to: MARKDOWN_ALLOWED_ATTRIBUTES)

    allowed attribute names.

Returns:

  • (ActiveSupport::SafeBuffer)

    sanitized HTML, blank when text is blank.



506
507
508
509
# File 'app/helpers/application_helper.rb', line 506

def markdown_to_html(text, tags: MARKDOWN_ALLOWED_TAGS, attributes: MARKDOWN_ALLOWED_ATTRIBUTES)
  html = Kramdown::Document.new(text.to_s, input: 'GFM', auto_ids: false).to_html
  sanitize(html, tags: tags, attributes: attributes)
end

#parent_layout(layout) ⇒ ActionView::OutputBuffer

Renders the current template output inside another layout.

The layout output is forced to UTF-8 to tolerate random binary or
non-UTF-8 input from the wild wild web — mixing encodings errors out.

Parameters:

  • layout (String)

    layout template name (rendered as +layouts/+)

Returns:

  • (ActionView::OutputBuffer)

    the layout output, assigned as the new buffer



306
307
308
309
310
311
# File 'app/helpers/application_helper.rb', line 306

def parent_layout(layout)
  @view_flow.set(:layout, output_buffer)
  # this allows us to deal with random binary or non UTF-8 encoded input from the wild wild web and prevent mixing encodings which ruby/rails errors out on
  output = render(template: "layouts/#{layout}").force_encoding('UTF-8')
  self.output_buffer = ActionView::OutputBuffer.new(output)
end

#pass_or_fail(result) ⇒ String?

Colored label and icon for a pass/fail style result string.

Parameters:

  • result (String, nil)

    "pass", "fail", "unavailable", or "unchecked";
    nil renders nothing. Other values render with an undefined icon/color.

Returns:

  • (String, nil)

    label + icon HTML, or nil when result is nil



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'app/helpers/application_helper.rb', line 199

def pass_or_fail(result)
  return nil if result.nil?

  case result
  when 'pass'
    color = 'green'
    icon = 'check-circle'
  when 'fail'
    color = 'red'
    icon = 'times-circle'
  when 'unavailable', 'unchecked'
    color = 'orange'
    icon = 'exclamation-circle'
  end
  "#{(:span, result.titleize)} #{fa_icon(icon, style: "color:#{color};font-size:1.2em")}"
end

#render_error_messages_list(object) ⇒ ActiveSupport::SafeBuffer

Renders a record's full error messages as a Bootstrap list group.

Parameters:

  • object (ActiveModel::Model)

    record responding to +errors+

Returns:

  • (ActiveSupport::SafeBuffer)

    unordered list of error messages



234
235
236
237
238
# File 'app/helpers/application_helper.rb', line 234

def render_error_messages_list(object)
  (:ul, class: 'list-group') do
    object.errors.full_messages.map { |msg| (:li, msg, class: 'list-group-item') }.join.html_safe
  end
end

#render_video_card(video, layout: 'card', card_style: 'default', hide_title: false, hide_description: false, display_category_badge: false, display_duration: false, show_popup: true, show_direct_link: true, styles: '') ⇒ String?

Renders a video card with consistent options via Www::VideoCardComponent.

Parameters:

  • video (Video, Www::VideoPresenter, nil)

    video to render; an existing
    Www::VideoPresenter is used as-is, anything else is wrapped with
    Www::VideoPresenter. Nil renders nothing.

  • layout (String) (defaults to: 'card')

    card layout variant (default: "card")

  • card_style (String) (defaults to: 'default')

    card style variant (default: "default")

  • hide_title (Boolean) (defaults to: false)

    omit the video title (default: false)

  • hide_description (Boolean) (defaults to: false)

    omit the video description (default: false)

  • display_category_badge (Boolean) (defaults to: false)

    show the category badge (default: false)

  • display_duration (Boolean) (defaults to: false)

    show the video duration (default: false)

  • show_popup (Boolean) (defaults to: true)

    enable the popup player (default: true)

  • show_direct_link (Boolean) (defaults to: true)

    link to the video page (default: true)

  • styles (String) (defaults to: '')

    extra CSS classes for the card

Returns:

  • (String, nil)

    rendered card HTML, or nil when video is nil



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'app/helpers/application_helper.rb', line 80

def render_video_card(video, layout: 'card', card_style: 'default', hide_title: false, hide_description: false, display_category_badge: false, display_duration: false, show_popup: true, show_direct_link: true, styles: '')
  return unless video

  # If video is already a presenter (from video_by_slug), use it directly
  # Otherwise, present it with Www::VideoPresenter
  video_presenter = video.is_a?(Www::VideoPresenter) ? video : present(video, Www::VideoPresenter)

  render Www::VideoCardComponent.new(
    video: video_presenter,
    layout: layout,
    card_style: card_style,
    hide_title: hide_title,
    hide_description: hide_description,
    display_category_badge: display_category_badge,
    display_duration: display_duration,
    show_popup: show_popup,
    show_direct_link: show_direct_link,
    styles: styles
  )
end

#resolved_auth_form_turbo_frame(turbo_frame: RESOLVED_AUTH_FRAME_UNSET) ⇒ String?

Resolves the Turbo Frame id wrapping the account login/register form.

Parameters:

  • turbo_frame (String, nil) (defaults to: RESOLVED_AUTH_FRAME_UNSET)

    explicit frame id forwarded by the caller
    (partials should pass +local_assigns[:turbo_frame]+); when omitted, the
    frame is inferred from +params[:turbo_target]+ so navbar offcanvas
    streams match the real frame

Returns:

  • (String, nil)

    the resolved frame id, or nil when no frame applies



55
56
57
58
59
# File 'app/helpers/application_helper.rb', line 55

def resolved_auth_form_turbo_frame(turbo_frame: RESOLVED_AUTH_FRAME_UNSET)
  return turbo_frame unless turbo_frame.equal?(RESOLVED_AUTH_FRAME_UNSET)

  params[:turbo_target].to_s == 'navbar-account-frame' ? 'navbar-account-frame' : nil
end

#return_path_or(default) ⇒ String

Returns @return_path (set by Controllers::ReturnPathHandling) when present,
otherwise falls back to the supplied default. Used in views for "Cancel"
links and similar nav-context links — does NOT perform a redirect.

Parameters:

  • default (String)

    path to use when +@return_path+ is not set

Returns:

  • (String)

    +@return_path+ when present, otherwise +default+



246
247
248
# File 'app/helpers/application_helper.rb', line 246

def return_path_or(default)
  @return_path || default
end

#safe_css_color(color) ⇒ String?

Validates a CSS color value against safe patterns (hex codes and named colors).
Returns nil for anything that could contain injection payloads.

Parameters:

  • color (String, nil)

    CSS color value to validate

Returns:

  • (String, nil)

    the stripped color when it matches a safe pattern,
    nil otherwise



342
343
344
345
346
347
348
349
# File 'app/helpers/application_helper.rb', line 342

def safe_css_color(color)
  return nil if color.blank?

  stripped = color.strip
  return stripped if stripped.match?(/\A#[0-9a-fA-F]{3,8}\z/) || stripped.match?(/\A[a-zA-Z]{1,30}\z/)

  nil
end

#set_return_path_if_present(return_path: @return_path, return_title: nil) ⇒ ActiveSupport::SafeBuffer

Emits hidden +return_path+ / +return_title+ fields for form posts.

+return_path+ is only emitted when present and on the same domain as the
current request (see +url_on_same_domain_as_request+).

Parameters:

  • return_path (String, nil) (defaults to: @return_path)

    path to return to after the form posts
    (defaults to +@return_path+)

  • return_title (String, nil) (defaults to: nil)

    title used when building the return link

Returns:

  • (ActiveSupport::SafeBuffer)

    hidden field tags (may be empty)



259
260
261
262
263
264
# File 'app/helpers/application_helper.rb', line 259

def set_return_path_if_present(return_path: @return_path, return_title: nil)
  capture do
    concat hidden_field_tag(:return_path, return_path) if return_path.present? && url_on_same_domain_as_request(return_path)
    concat hidden_field_tag(:return_title, return_title) if return_title.present?
  end
end

#set_section_if_presentActiveSupport::SafeBuffer?

Emits a hidden +section+ field echoing +params[:section]+.

Returns:

  • (ActiveSupport::SafeBuffer, nil)

    hidden field tag, or nil when no
    section param is present



287
288
289
# File 'app/helpers/application_helper.rb', line 287

def set_section_if_present
  hidden_field_tag :section, params[:section] if params[:section].present?
end

#tab_frame_idString

Frame id for tab partial views.

Returns the instance-scoped tab-content-<controller_name>-<id> by default
(tab-content-<controller_name> when the route has no :id, e.g.
dashboards). Echoes the caller's Turbo-Frame: tab-content-<parent>… header
only when the URL carries the parent's <resource>_id route param —
i.e. the request is genuinely scoped to that parent (e.g.
/customers/:customer_id/activities).

Why the :id suffix:

Without it, a record's show page and a sibling record's tab pane share
one frame id. Clicking an item link inside another item's kit-contents tab
(/items/4702 fetched with Turbo-Frame: tab-content-items) returned a
show page whose own tab_panel frame matched the caller, so Turbo slotted
an empty lazy shell into the pane and the empty-shell trap Drive-visited
the inner tab URL — a chrome-less fragment page. With instance scoping,
item 13140's pane is tab-content-items-13140 while item 4702's response
renders tab-content-items-4702: the mismatch fires turbo:frame-missing
and the global handler upgrades to a full Drive visit of the already-
fetched page — same-controller navigation now rides the same proven
breakout path as cross-controller navigation.

Why the parent_id gate:

When a top-level resource (e.g. /orders/:id) is requested via a Turbo Frame
fetch from inside a parent's tab (Turbo-Frame: tab-content-warehouses-2), we
want the response to contain tab-content-orders-<id> (this controller's own
frame), NOT the parent's frame id. The frame mismatch fires
turbo:frame-missing, which the global handler in turbo_stream_actions.js
upgrades to a full Drive visit — the right UX for clicking "into" a different
resource. Genuinely nested content (/customers/134/activities,
/quotes?customer_id=134) still echoes so it renders inside the parent pane.

The server-side partner is ApplicationController#tab_frame_breakout_request?,
which keeps the full layout on breakout responses so visit(response)
paints a complete page instead of a bare fragment.

Embed-anywhere views (search/list views meant to render under any tab) should
use embedded_tab_frame_id instead — that one always echoes.

Returns:

  • (String)

    the tab frame id to render for this request



400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
# File 'app/helpers/application_helper.rb', line 400

def tab_frame_id
  default = ['tab-content', controller_name, params[:id].presence].compact.join('-')
  header = request.headers['Turbo-Frame']
  return default unless header&.start_with?('tab-content-')
  return header if header == default

  # Echo the parent pane's id only when this request is genuinely scoped to
  # that parent (`<resource>_id` param) AND the header's instance suffix —
  # when present — names that same record. Without the suffix check, a
  # request scoped to customer 134 issued from customer 999's pane would
  # echo the 999 pane and swap the wrong parent instance instead of
  # breaking out. Controller names never contain dashes, so the first
  # dash-segment is the resource and the rest is the instance id (which
  # itself may contain dashes, e.g. slugs); a suffix-less header comes from
  # legacy pages rendered before instance scoping and still echoes.
  resource, *instance = header.delete_prefix('tab-content-').split('-')
  suffix = instance.join('-')
  parent_id = params["#{resource.singularize}_id"].presence
  if parent_id
    return header if suffix.blank? || suffix == parent_id.to_s
  end

  # Same-record alias echo for member routes. FriendlyId (and any custom
  # to_param) gives one record several URL spellings: /product_lines/snow-melting
  # renders its pane with the slug suffix while the tab hrefs carry the
  # numeric id (or vice versa, or a historical slug). A fetch whose header
  # names THIS controller and whose suffix resolves to the SAME record the
  # controller loaded is an in-pane fetch, not a breakout to a sibling —
  # treating it as a breakout is what dumped bare tab fragments onto
  # /product_lines/:id/tab_items as a full-page visit.
  if resource == controller_name && suffix.present?
    record = controller.instance_variable_get(:"@#{controller_name.singularize}")
    return header if tab_frame_suffix_aliases_record?(record, suffix)
  end

  default
end

#to_underscore(term) ⇒ String

Underscores a term for use as an identifier (tableized, singular).

Parameters:

  • term (String)

    source term

Returns:

  • (String)

    underscored, singular term



295
296
297
# File 'app/helpers/application_helper.rb', line 295

def to_underscore(term)
  term.tableize.singularize.tr(' ', '_')
end

#turbo_section_wrapper(id: nil, class_names: nil) ⇒ ActiveSupport::SafeBuffer

Enable Turbo functionality for specific sections without affecting the entire page

Parameters:

  • id (String, nil) (defaults to: nil)

    DOM id for the wrapper div

  • class_names (String, nil) (defaults to: nil)

    CSS classes for the wrapper div

Returns:

  • (ActiveSupport::SafeBuffer)

    wrapper div containing the block content



326
327
328
329
330
331
332
333
334
# File 'app/helpers/application_helper.rb', line 326

def turbo_section_wrapper(id: nil, class_names: nil, &)
  turbo_attrs = {}
  turbo_attrs[:id] = id if id
  turbo_attrs[:class] = class_names if class_names

  turbo_attrs[:'data-turbo'] = 'true' if @turbo_frames_enabled

  (:div, turbo_attrs, &)
end

#turbo_tabs_request?Boolean

Whether the current request is a Turbo Frame fetch for a tab pane.

Returns:

  • (Boolean)


354
355
356
# File 'app/helpers/application_helper.rb', line 354

def turbo_tabs_request?
  request.headers['Turbo-Frame']&.start_with?('tab-content')
end

#url_on_same_domain_as_request(path) ⇒ Boolean?

Whether a URL/path belongs to the current request's host.

Relative paths (no host) count as same-domain; blank or unparseable input
returns nil.

Parameters:

  • path (String, nil)

    URL or path to check

Returns:

  • (Boolean, nil)

    nil for blank/invalid input, otherwise the host check



273
274
275
276
277
278
279
280
281
# File 'app/helpers/application_helper.rb', line 273

def url_on_same_domain_as_request(path)
  if path.present? && (uri = begin
    URI(path)
  rescue StandardError
    nil
  end)
    (uri.host.nil? or uri.host.index(request.host).present?)
  end
end

#working_hours?Boolean

Whether the current time falls within business working hours.

Returns:

  • (Boolean)


119
120
121
# File 'app/helpers/application_helper.rb', line 119

def working_hours?
  Time.current.in_working_hours?
end

#yes_or_no(value) ⇒ String

"Yes"/"No" label for a value coerced with +to_b+.

Parameters:

  • value (Object)

    value coerced with +to_b+

Returns:

  • (String)

    "Yes" or "No"



162
163
164
# File 'app/helpers/application_helper.rb', line 162

def yes_or_no(value)
  value.to_b ? 'Yes' : 'No'
end

#yes_or_no_highlighted(b, reverse_check = false) ⇒ ActiveSupport::SafeBuffer?

"Yes"/"No" label in a green or red span.

Parameters:

  • b (Boolean, nil)

    value to render; nil renders nothing

  • reverse_check (Boolean) (defaults to: false)

    invert the colors (Yes renders red)

Returns:

  • (ActiveSupport::SafeBuffer, nil)

    colored span, or nil when b is nil



182
183
184
185
186
187
188
189
190
191
192
# File 'app/helpers/application_helper.rb', line 182

def yes_or_no_highlighted(b, reverse_check = false)
  return nil if b.nil?

  res = yes_or_no(b)
  color = if reverse_check
            res == 'Yes' ? 'red' : 'green'
          else
            res == 'Yes' ? 'green' : 'red'
          end
  (:span, res, style: "color:#{color}")
end

#yes_or_no_with_check_or_cross(b, reverse_check = false) ⇒ String?

"Yes"/"No" label followed by a check or cross icon.

Parameters:

  • b (Boolean, nil)

    value to render; nil renders nothing

  • reverse_check (Boolean) (defaults to: false)

    invert the icon logic (truthy shows the cross)

Returns:

  • (String, nil)

    label + icon HTML, or nil when b is nil



171
172
173
174
175
# File 'app/helpers/application_helper.rb', line 171

def yes_or_no_with_check_or_cross(b, reverse_check = false)
  return nil if b.nil?

  "#{yes_or_no(b)} #{check_or_cross(reverse_check ? !b : b)}"
end