Module: Avo::ApplicationHelper

Constant Summary collapse

UNRENDERABLE_IMAGE_CONTENT_TYPES =

The image formats no browser paints: HEIC and HEIF decode in Safari alone, TIFF and PSD nowhere. Not ActiveStorage.web_image_content_types -- that one picks a variant's output format, and browsers render plenty it leaves out (AVIF always, WebP unless the app loads Rails 7.2 defaults).

%w[image/heic image/heif image/tiff image/vnd.adobe.photoshop].freeze

Instance Method Summary collapse

Methods included from SummaryChartHelper

#summary_chart_params_for

Methods included from ResourcesHelper

#field_wrapper, #index_field_wrapper, #item_selector_data_attributes, #record_path, #record_title, #resource_for_record, #resource_grid, #resource_show_path, #resource_table

Instance Method Details

#a_button(**args, &block) ⇒ Object



108
109
110
111
112
# File 'app/helpers/avo/application_helper.rb', line 108

def a_button(**args, &block)
  render Avo::ButtonComponent.new(is_link: false, **args) do
    capture(&block) if block.present?
  end
end


114
115
116
117
118
# File 'app/helpers/avo/application_helper.rb', line 114

def a_link(path = nil, **args, &block)
  render Avo::ButtonComponent.new(path, is_link: true, **args) do
    capture(&block) if block.present?
  end
end

#appearance_neutral_labels(neutrals) ⇒ Object



302
303
304
# File 'app/helpers/avo/application_helper.rb', line 302

def appearance_neutral_labels(neutrals)
  neutrals.index_with { |theme| avo_appearance_t("neutrals_list.#{theme}", default: theme.capitalize) }
end

#avo_appearance_t(key, **options) ⇒ Object

Translates appearance UI strings with English fallback when the active locale omits Avo 4 appearance keys. Returns plain text safe for HTML attributes.



294
295
296
297
298
299
300
# File 'app/helpers/avo/application_helper.rb', line 294

def avo_appearance_t(key, **options)
  full_key = "avo.appearance.#{key}"
  defaults = []
  defaults << I18n.t(full_key, locale: :en, raise: false) if I18n.locale != :en
  defaults << options[:default] if options.key?(:default)
  I18n.t(full_key, **options.except(:default), default: defaults.presence)
end

#body_classesObject



214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'app/helpers/avo/application_helper.rb', line 214

def body_classes
  os_class = request.user_agent.to_s.include?("Mac") ? "os-mac" : "os-pc"

  view_class = if controller.class.superclass.to_s == "Avo::ResourcesController"
    case action_name.to_sym
    when :index
      "resource-index-view"
    when :show
      "resource-show-view"
    when :edit, :update
      "resource-edit-view"
    when :new, :create
      "resource-new-view"
    end
  end

  [os_class, view_class, *Array.wrap(Avo.configuration.body_classes)]
end

#button_classes(extra_classes = nil, color: nil, variant: nil, size: :md, active: false) ⇒ Object



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'app/helpers/avo/application_helper.rb', line 120

def button_classes(extra_classes = nil, color: nil, variant: nil, size: :md, active: false)
  classes = "inline-flex grow-0 items-center text-sm font-semibold leading-6 fill-current whitespace-nowrap transition duration-100 rounded-sm transform transition duration-100 active:translate-x-px active:translate-y-px cursor-pointer disabled:cursor-not-allowed #{extra_classes}"

  if color.present?
    if variant.present? && (variant.to_sym == :outlined)
      classes += " bg-white border"

      classes += " hover:border-#{color}-700 border-#{color}-500 text-#{color}-600 hover:text-#{color}-700 disabled:border-gray-300 disabled:text-gray-600"
    else
      classes += " text-white bg-#{color}-500 hover:bg-#{color}-600 disabled:bg-#{color}-300"
    end
  else
    classes += " text-gray-700 bg-white hover:bg-gray-100 disabled:bg-gray-300"
  end

  size = size.present? ? size.to_sym : :md
  classes += case size
  when :xs
    " p-2 py-1"
  when :sm
    " py-1 px-4"
  when :md
    " py-2 px-4"
  when :xl
    " py-3 px-4"
  else
    " p-4"
  end

  classes
end

#chart_color(index) ⇒ Object



205
206
207
208
# File 'app/helpers/avo/application_helper.rb', line 205

def chart_color(index)
  colors = Avo.configuration.appearance.chart_colors
  colors[index % colors.length]
end

#container_classesObject



243
244
245
246
247
248
# File 'app/helpers/avo/application_helper.rb', line 243

def container_classes
  return "container-#{container_width_css_suffix(@container_size.to_sym)}" if @container_size.present?

  width = Avo.configuration.container_width.fetch(@view&.to_sym, :large)
  "container-#{container_width_css_suffix(width)}"
end

#custom_sidebar_width_defaultObject

nil when the host has not changed the starting width, so the pre-paint carrier emits nothing at all and the stylesheet's own --spacing(64) fallback stands — same discipline as sidebar_width returning nil for "no cookie". Keeps the CSP surface and the bytes at zero for everyone who never configures this.



321
322
323
324
325
# File 'app/helpers/avo/application_helper.rb', line 321

def custom_sidebar_width_default
  width = sidebar_width_default

  width unless width == SIDEBAR_WIDTH_DEFAULT
end

#d(value) ⇒ Object

decrypt & decode params



257
258
259
260
261
262
# File 'app/helpers/avo/application_helper.rb', line 257

def d(value)
  decoded = Base64.urlsafe_decode64(value.to_s)
  Avo::Services::EncryptionService.decrypt(message: decoded, purpose: :return_to, serializer: Marshal)
rescue
  nil
end

#decode_filter_params(encoded_params) ⇒ Object



178
179
180
# File 'app/helpers/avo/application_helper.rb', line 178

def decode_filter_params(encoded_params)
  Avo::Filters::BaseFilter.decode_filters(encoded_params)
end

#e(value) ⇒ Object

encode & encrypt params



251
252
253
254
# File 'app/helpers/avo/application_helper.rb', line 251

def e(value)
  encrypted = Avo::Services::EncryptionService.encrypt(message: value, purpose: :return_to, serializer: Marshal)
  Base64.urlsafe_encode64(encrypted, padding: false)
end

#editor_file_path(path) ⇒ Object



272
273
274
# File 'app/helpers/avo/application_helper.rb', line 272

def editor_file_path(path)
  editor_url(Object.const_source_location(path.class.to_s)&.first)
end

#editor_url(path) ⇒ Object



276
277
278
# File 'app/helpers/avo/application_helper.rb', line 276

def editor_url(path)
  Avo.configuration.default_editor_url.gsub("%{path}", path)
end

#empty_state(**args) ⇒ Object



104
105
106
# File 'app/helpers/avo/application_helper.rb', line 104

def empty_state(**args)
  render Avo::EmptyStateComponent.new(**args)
end

#encode_filter_params(filter_params) ⇒ Object



182
183
184
# File 'app/helpers/avo/application_helper.rb', line 182

def encode_filter_params(filter_params)
  Avo::Filters::BaseFilter.encode_filters(filter_params)
end

#frame_id(resource) ⇒ Object



201
202
203
# File 'app/helpers/avo/application_helper.rb', line 201

def frame_id(resource)
  ["frame", resource.model_name.singular, resource.record_param].compact.join("-")
end

#get_model_class(model) ⇒ Object



160
161
162
163
164
165
166
# File 'app/helpers/avo/application_helper.rb', line 160

def get_model_class(model)
  if model.instance_of?(Class)
    model
  else
    model.class
  end
end

#input_classes(extra_classes = "", has_error: false, size: :md) ⇒ Object



152
153
154
155
156
157
158
# File 'app/helpers/avo/application_helper.rb', line 152

def input_classes(extra_classes = "", has_error: false, size: :md)
  classes = ""
  classes += "input--size-#{size}" if [:sm, :md, :lg].include?(size)
  classes += " input-field--error" if has_error
  classes += " #{extra_classes}"
  classes.strip
end

The cookie name that remembers an opened manual frame. Derived from the frame's deferred URL (which already encodes resource + record + frame), so the memory is scoped per record + association/tab. Hashed to keep the name short and cookie-safe. The manual-frame Stimulus controller writes this same name client-side on a successful load (see manual_frame_controller.js).



13
14
15
# File 'app/helpers/avo/application_helper.rb', line 13

def manual_frame_cookie_name(url)
  "amf_#{Digest::MD5.hexdigest(url.to_s)}"
end

#manual_frame_remembered?(url, auto_load_for) ⇒ Boolean

Whether this manual frame was opened recently enough to skip the placeholder and render a real auto-loading <turbo-frame src> (so the Load button never appears). Presence-based: the cookie carries max-age, so the browser drops it when the window lapses — a present cookie means "still remembered".

Sliding: every render that finds it remembered refreshes the cookie, so the window keeps moving forward as long as the user keeps coming back.

Returns false (and touches nothing) for plain loading: :manual frames, which carry no auto_load_for window.

Returns:

  • (Boolean)


27
28
29
30
31
32
33
34
35
# File 'app/helpers/avo/application_helper.rb', line 27

def manual_frame_remembered?(url, auto_load_for)
  return false if auto_load_for.blank?

  name = manual_frame_cookie_name(url)
  return false if request.cookies[name].blank?

  cookies[name] = {value: "1", path: "/", max_age: auto_load_for.to_i, same_site: :lax}
  true
end

#mount_pathObject



174
175
176
# File 'app/helpers/avo/application_helper.rb', line 174

def mount_path
  Avo::Engine.routes.find_script_name(params.permit!.to_h.symbolize_keys)
end

#number_to_social(number, start_at: 10_000) ⇒ Object



186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'app/helpers/avo/application_helper.rb', line 186

def number_to_social(number, start_at: 10_000)
  return number_with_delimiter(number) if number < start_at

  number_to_human(number,
    precision: 1,
    significant: false,
    round_mode: :down,
    format: "%n%u",
    units: {
      thousand: "K",
      million: "M",
      billion: "B"
    })
end

#possibly_rails_authentication?Boolean

Returns:

  • (Boolean)


210
211
212
# File 'app/helpers/avo/application_helper.rb', line 210

def possibly_rails_authentication?
  defined?(Authentication) && Authentication.private_instance_methods.include?(:require_authentication) && Authentication.private_instance_methods.include?(:authenticated?)
end

#render_header_menu_itemsObject



280
281
282
283
284
285
286
287
288
289
290
# File 'app/helpers/avo/application_helper.rb', line 280

def render_header_menu_items
  items = Avo.configuration.header_menu_items
  return link_to(Avo.configuration.app_name, "/", target: :_blank) if items.blank?

  safe_join(items.map { |item|
    link_to(item.name, item.to_path,
      target: item.target,
      title: item.title,
      data: item.method ? {turbo_method: item.method} : nil)
  })
end

#render_license_warning(title: "", message: "", icon: "exclamation") ⇒ Object



96
97
98
99
100
101
102
# File 'app/helpers/avo/application_helper.rb', line 96

def render_license_warning(title: "", message: "", icon: "exclamation")
  render partial: "avo/sidebar/license_warning", locals: {
    title: title,
    message: message,
    icon: icon
  }
end

#root_path_without_urlObject



168
169
170
171
172
# File 'app/helpers/avo/application_helper.rb', line 168

def root_path_without_url
  "#{Avo.configuration.prefix_path}#{mount_path}"
rescue
  Avo.configuration.root_path
end

#rtl?Boolean

Check if the current locale is RTL (Right-to-Left)

Returns:

  • (Boolean)


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

def rtl?
  Avo.configuration.rtl?
end

#safe_blob_path(blob) ⇒ Object



51
52
53
54
55
56
57
58
59
# File 'app/helpers/avo/application_helper.rb', line 51

def safe_blob_path(blob)
  if blob.filename.to_s.blank?
    main_app.rails_service_blob_path(blob.signed_id, routable_blob_filename(blob))
  else
    main_app.rails_blob_path(blob)
  end
rescue ActionController::UrlGenerationError
  nil
end

#safe_blob_representation_url(representation) ⇒ Object



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

def safe_blob_representation_url(representation)
  blob = representation.blob

  if blob.filename.to_s.blank?
    main_app.rails_blob_representation_url(
      blob.signed_id,
      representation.variation.key,
      routable_blob_filename(blob)
    )
  else
    main_app.url_for(representation)
  end
rescue ActionController::UrlGenerationError
  nil
end

#safe_blob_url(blob) ⇒ Object

Active Storage URL helpers raise UrlGenerationError when a blob's filename is blank (the route requires a :filename segment). Use a synthetic filename so attach mode and other callers still get a routable URL; return nil only when generation still fails for another reason.



41
42
43
44
45
46
47
48
49
# File 'app/helpers/avo/application_helper.rb', line 41

def safe_blob_url(blob)
  if blob.filename.to_s.blank?
    main_app.rails_service_blob_url(blob.signed_id, routable_blob_filename(blob))
  else
    main_app.url_for(blob)
  end
rescue ActionController::UrlGenerationError
  nil
end

#safe_image_url(attachment) ⇒ Object

Serves an attachment the browser can't paint through a variant, which ActiveStorage renders as PNG. Everything else keeps serving the original, so apps without an image processor are unaffected.



70
71
72
73
74
75
76
77
78
# File 'app/helpers/avo/application_helper.rb', line 70

def safe_image_url(attachment)
  blob = attachment.try(:blob)

  if blob&.variable? && blob.content_type.in?(UNRENDERABLE_IMAGE_CONTENT_TYPES)
    safe_blob_representation_url(blob.variant({})) || main_app.url_for(attachment)
  else
    main_app.url_for(attachment)
  end
end

Already clamped into the bounds by Avo::Configuration.



314
# File 'app/helpers/avo/application_helper.rb', line 314

def sidebar_width_default = Avo.configuration.sidebar[:default_width]


311
# File 'app/helpers/avo/application_helper.rb', line 311

def sidebar_width_max = SIDEBAR_WIDTH_MAX

Resizable-sidebar width bounds for the JS layer (window.Avo.configuration). Read the private_constant bounds via unqualified lexical lookup — the constants themselves stay non-public API (see lib/avo.rb).



309
# File 'app/helpers/avo/application_helper.rb', line 309

def sidebar_width_min = SIDEBAR_WIDTH_MIN

#text_directionObject

Returns "rtl" or "ltr" based on current locale



239
240
241
# File 'app/helpers/avo/application_helper.rb', line 239

def text_direction
  Avo.configuration.text_direction
end

#uiObject



6
# File 'app/helpers/avo/application_helper.rb', line 6

def ui = Avo::UIInstance

#wrap_in_modal(content) ⇒ Object



264
265
266
267
268
269
270
# File 'app/helpers/avo/application_helper.rb', line 264

def wrap_in_modal(content)
  turbo_frame_tag Avo::MODAL_FRAME_ID do
    render(Avo::ModalComponent.new(width: :xl, body_class: "bg-application")) do |c|
      content
    end
  end
end