diff --git a/app/helpers/trestle/avatar_helper.rb b/app/helpers/trestle/avatar_helper.rb index 129d3c47..68fe5ddd 100644 --- a/app/helpers/trestle/avatar_helper.rb +++ b/app/helpers/trestle/avatar_helper.rb @@ -1,23 +1,29 @@ module Trestle module AvatarHelper - def avatar(options={}) - fallback = options.delete(:fallback) if options[:fallback] - - content_tag(:div, default_avatar_options.merge(options)) do - concat content_tag(:span, fallback, class: "avatar-fallback") if fallback + # Renders an avatar (or similar image) styled as a circle typically designed to + # represent a user (though not limited to that). The avatar helper accepts a block + # within which the image should be provided, which will be resized to fit. + # + # fallback - Optional short text (2-3 characters) shown when no image is available + # attributes - Additional HTML attributes to add to the
tag + # + # Examples + # + # <%= avatar { image_tag("person.jpg") } %> + # + # <%= avatar(fallback: "SP", class: "avatar-lg") { gravatar("sam@trestle.io") } %> + # + # <%= avatar(style: "--avatar-size: 3rem") { gravatar("sam@trestle.io") } + # + # Return the HTML div containing the avatar image. + def avatar(fallback: nil, **attributes, &block) + tag.div(**default_avatar_options.merge(attributes)) do + concat tag.span(fallback, class: "avatar-fallback") if fallback concat yield if block_given? end end - def gravatar(email, options={}) - options = { d: "mp" }.merge(options) - - url = "https://www.gravatar.com/avatar/#{Digest::MD5.hexdigest(email.to_s.downcase)}.png" - url << "?#{options.to_query}" if options.any? - - image_tag(url) - end - + protected def default_avatar_options Trestle::Options.new(class: ["avatar"]) end diff --git a/app/helpers/trestle/card_helper.rb b/app/helpers/trestle/card_helper.rb index 4232c911..00222286 100644 --- a/app/helpers/trestle/card_helper.rb +++ b/app/helpers/trestle/card_helper.rb @@ -1,23 +1,41 @@ module Trestle module CardHelper - def card(options={}, &block) - content_tag(:div, options.slice(:id, :data).merge(class: ["card", options[:class]].compact)) do + # Renders a card container element (sometimes also known as a panel or well), based on + # Bootstrap's card element (https://getbootstrap.com/docs/5.3/components/card/). + # + # header - Optional header to add within a .card-header + # footer - Optional footer to add within a .card-footer + # attributes - Additional HTML attributes to add to the container
tag + # + # Examples + # + # <%= card do %> + #

Card content here...

+ # <% end %> + # + # <%= card header: "Header", footer: "Footer", class: "text-bg-primary" do %>... + # + # Returns a HTML-safe String. + def card(header: nil, footer: nil, **attributes, &block) + tag.div(**attributes.merge(class: ["card", attributes[:class]])) do safe_join([ - (content_tag(:div, options[:header], class: "card-header") if options[:header]), - content_tag(:div, class: "card-body", &block), - (content_tag(:div, options[:footer], class: "card-footer") if options[:footer]) + (tag.header(header, class: "card-header") if header), + tag.div(class: "card-body", &block), + (tag.footer(footer, class: "card-footer") if footer) ].compact) end end - def panel(options={}, &block) + # [DEPRECATED] Alias for card + def panel(**attributes, &block) Trestle.deprecator.warn("The panel helper is deprecated and will be removed in future versions of Trestle. Please use the card helper instead.") - card(options.merge(header: options[:title]), &block) + card(**attributes.merge(header: attributes[:title]), &block) end - def well(options={}, &block) + # [DEPRECATED] Alias for card + def well(**attributes, &block) Trestle.deprecator.warn("The well helper is deprecated and will be removed in future versions of Trestle. Please use the card helper instead.") - card(options, &block) + card(**attributes, &block) end end end diff --git a/app/helpers/trestle/container_helper.rb b/app/helpers/trestle/container_helper.rb index 85cf8298..8234ec80 100644 --- a/app/helpers/trestle/container_helper.rb +++ b/app/helpers/trestle/container_helper.rb @@ -1,11 +1,35 @@ module Trestle module ContainerHelper - def container(&block) + # Renders a content container with an optional sidebar, which can + # be useful when creating an admin or dashboard with a custom view. + # + # This helper accepts a block (within which the main content is provided), + # which yields a `Context` capture object. The Context object has one important + # method -- `sidebar(**attributes, &block)` which captures the sidebar content + # to be rendered after the main content. + # + # attributes - Additional HTML attributes to add to the
tag + # + # Examples + # + # <%= container do |c| %> + # This content will be wrapped in a .main-content-container > .main-content div. + # <% end %> + # + # <%= container do |c| %> + # <% c.sidebar class: "order-first" %> + # Sidebar content... + # <% end %> + # Main content... + # <% end %> + # + # Returns a HTML-safe String. + def container(**attributes, &block) context = Context.new(self) - content = capture(context, &block) + content = capture(context, &block) if block_given? - content_tag(:div, class: "main-content-container") do - concat content_tag(:div, content, class: "main-content") + tag.div(**attributes.merge(class: ["main-content-container", attributes[:class]])) do + concat tag.div(content, class: "main-content") concat context.sidebar if context.sidebar end end @@ -15,15 +39,22 @@ def initialize(template) @template = template end - def sidebar(options={}, &block) + # Captures or renders the sidebar for a container block. + # + # When passed a block, the block content is captured as the sidebar content, and nil is returned. + # When no block is provided, the sidebar tag is returned (if defined). + # + # attributes - Additional HTML attributes to add to the
tag + def sidebar(**attributes, &block) if block_given? - @sidebar = @template.content_tag(:aside, default_sidebar_options.merge(options), &block) + @sidebar = @template.tag.aside(**default_sidebar_options.merge(attributes), &block) nil else @sidebar end end + protected def default_sidebar_options Trestle::Options.new(class: ["main-content-sidebar"]) end diff --git a/app/helpers/trestle/display_helper.rb b/app/helpers/trestle/display_helper.rb index 6767b642..bf0a9e28 100644 --- a/app/helpers/trestle/display_helper.rb +++ b/app/helpers/trestle/display_helper.rb @@ -1,5 +1,16 @@ module Trestle module DisplayHelper + # Returns a plain-text representation of the given model instance, + # typically used when rendering an associated object within a table. + # + # This helper delegates to Trestle::Display, which works by checking the + # existence of each method from `Trestle.config.display_methods` in turn + # and calling the first one it finds. + # + # By default this list is set to: + # + # [:display_name, :full_name, :name, :title, :username, :login, :email] + # def display(instance) Trestle::Display.new(instance).to_s end diff --git a/app/helpers/trestle/flash_helper.rb b/app/helpers/trestle/flash_helper.rb index 063adfde..bfdb1b5f 100644 --- a/app/helpers/trestle/flash_helper.rb +++ b/app/helpers/trestle/flash_helper.rb @@ -1,4 +1,5 @@ module Trestle + # [Internal] module FlashHelper def normalize_flash_alert(flash) flash.is_a?(Hash) ? flash.with_indifferent_access : { message: flash } @@ -11,15 +12,5 @@ def debug_form_errors? def instance_has_errors? instance.errors.any? rescue false end - - def turbo_stream_update_flash - <<-EOF - - - - EOF - end end end diff --git a/app/helpers/trestle/form_helper.rb b/app/helpers/trestle/form_helper.rb index cefa1a20..52161fef 100644 --- a/app/helpers/trestle/form_helper.rb +++ b/app/helpers/trestle/form_helper.rb @@ -1,22 +1,47 @@ module Trestle module FormHelper - IDENTITY_FIELD_ERROR_PROC = Proc.new { |html_tag, instance| html_tag } DEFAULT_FORM_CONTROLLERS = %w(keyboard-submit form-loading form-error) - def trestle_form_for(instance, options={}, &block) + # Generates a form for a resource using Rails' #form_for helper. + # + # In addition to delegating to #form_for, this helper method: + # + # 1) Sets the default form builder to `Trestle::Form::Builder`. + # 2) Sets the default :as option to match the parameter name + # expected by the admin. + # 3) Sets default Stimulus controllers on the
element: + # "keyboard-submit form-loading form-error" + # 4) Sets a null/identity ActionView::Base.field_error_proc as + # errors are handled by Trestle::Form::Fields::FormGroup. + # 5) Exposes the yielded form builder instance via the `form` helper. + # + # Examples + # + # <%= trestle_form_for(article, url: admin.instance_path(instance, action: :update), + # method: :patch) do %> ... + # + # Returns a HTML-safe String. Yields the form builder instance. + def trestle_form_for(instance, **options, &block) options[:builder] ||= Form::Builder options[:as] ||= admin.parameter_name options[:data] ||= {} options[:data][:controller] = (DEFAULT_FORM_CONTROLLERS + Array(options[:data][:controller])).join(" ") - form_for(instance, options) do |f| + form_for(instance, **options) do |f| with_identity_field_error_proc do with_form(f) { yield f } end end end + # Returns the currently scoped Trestle form builder + # (a subclass of ActionView::Helpers::FormBuilder). + def form + @_trestle_form + end + + protected def with_form(form) @_trestle_form = form yield form if block_given? @@ -24,6 +49,8 @@ def with_form(form) @_trestle_form = nil end + IDENTITY_FIELD_ERROR_PROC = Proc.new { |html_tag, instance| html_tag } + def with_identity_field_error_proc original_field_error_proc = ::ActionView::Base.field_error_proc ::ActionView::Base.field_error_proc = IDENTITY_FIELD_ERROR_PROC @@ -32,17 +59,5 @@ def with_identity_field_error_proc ensure ::ActionView::Base.field_error_proc = original_field_error_proc end - - def form - @_trestle_form - end - - def sidebar(&block) - content_for(:sidebar, &block) - end - - def render_sidebar_as_tab? - modal_request? && content_for?(:sidebar) - end end end diff --git a/app/helpers/trestle/format_helper.rb b/app/helpers/trestle/format_helper.rb index 292f9b5f..20dc3fe2 100644 --- a/app/helpers/trestle/format_helper.rb +++ b/app/helpers/trestle/format_helper.rb @@ -1,30 +1,60 @@ module Trestle module FormatHelper - def format_value(value, options={}) - if options.key?(:format) - format_value_from_options(value, options) - else - autoformat_value(value, options) - end - end - - def format_value_from_options(value, options={}) - case options[:format] + # Formats a value, either with an explicit format (given the :format option) + # or automatically based on its type using the `autoformat_value` helper. + # + # This helper is most commonly called when rendering content within a + # table cell, but is available to use from any view context. + # + # value - Value to format + # format - Symbol representing format type. Currently supported: + # :auto, nil - Auto-formats (default) + # :currency - Formats as currency using `number_to_currency` + # :tags - Formats an array of Strings as a tag list + # options - Options hash to pass to `autoformat_value` helper + # + # Examples + # + # <%= format_value(123.45, format: :currency) %> + # <%= format_value(article.tags, format: :tags) %> + # + # <%= format_value(Time.current) %> + # <%= format_value(nil, blank: "None") %> + # <%= format_value(true) %> + # + # Returns a HTML-safe String. + # Raises ArgumentError if an invalid format is given. + def format_value(value, format: :auto, **options) + case format + when :auto, nil + autoformat_value(value, **options) when :currency number_to_currency(value) when :tags - tags = Array(value).map { |tag| content_tag(:span, tag, class: "tag tag-primary") } - content_tag(:div, safe_join(tags), class: "tag-list") + tags = Array(value).map { |t| tag.span(t, class: "tag tag-primary") } + tag.div(safe_join(tags), class: "tag-list") else - value + raise ArgumentError, "unknown format: #{format}" end end - def autoformat_value(value, options={}) + # Auto-formats a value based on its type. + # + # The current implementation of this helper supports Arrays, Time/Datetime, + # Date, true/false values, nil, String (with optional truncation) or model + # instance types (using the `display` helper). + # + # value - Value to format (multiple types supported) + # options - Options hash, usage dependent on value type. Currently supported: + # :blank - text to display for nil values (e.g. "N/A") + # :truncate - passed to truncate helper for String values + # + # Returns a HTML-safe String. + def autoformat_value(value, **options) case value when Array - content_tag(:ol, safe_join(value.map { |v| - content_tag(:li, v.is_a?(Array) ? v : autoformat_value(v, options)) }, + tag.ol(safe_join(value.map { |v| + tag.li(v.is_a?(Array) ? v : autoformat_value(v, **options)) }, "\n")) when Time, DateTime timestamp(value) @@ -37,7 +67,7 @@ def autoformat_value(value, options={}) if blank.respond_to?(:call) instance_exec(&blank) else - content_tag(:span, blank, class: "blank") + tag.span(blank, class: "blank") end when String if value.html_safe? || options[:truncate] == false diff --git a/app/helpers/trestle/gravatar_helper.rb b/app/helpers/trestle/gravatar_helper.rb new file mode 100644 index 00000000..e6a87a9a --- /dev/null +++ b/app/helpers/trestle/gravatar_helper.rb @@ -0,0 +1,48 @@ +module Trestle + module GravatarHelper + # Returns a Gravatar image tag for a given email address. + # See https://docs.gravatar.com/api/avatars/images/ for available options. + # + # In general, this should be wrapped in an avatar block to apply styling. + # This method is also aliased as `gravatar`. + # + # email - Email address for Gravatar image; will be MD5-hashed to create the URL + # options - Options to pass to Gravatar API (as query string params) + # + # Examples + # + # <%= avatar { gravatar_image_tag("sam@trestle.io") } %> + # + # <%= avatar { gravatar_image_tag("sam@trestle.io", size: 120, d: "retro") } %> + # + # Returns a HTML-safe String. + def gravatar_image_tag(email, **options) + image_tag(gravatar_image_url(email, **options)) + end + alias gravatar gravatar_image_tag + + # Returns a Gravatar image URL for a given email address. + # See https://docs.gravatar.com/api/avatars/images/ for available options. + # + # email - Email address for Gravatar image; will be MD5-hashed to create the URL + # options - Options to pass to Gravatar API (as query string params) + # + # Example + # + # <%= gravatar_image_url("sam@trestle.io", size: 120, d: "retro") %> + # + # Returns a HTML-safe String. + def gravatar_image_url(email, **options) + options = default_gravatar_options.merge(options) + + url = "https://www.gravatar.com/avatar/#{Digest::MD5.hexdigest(email.to_s.downcase)}.png" + url << "?#{options.to_query}" if options.any? + url + end + + protected + def default_gravatar_options + { d: "mp" } + end + end +end diff --git a/app/helpers/trestle/grid_helper.rb b/app/helpers/trestle/grid_helper.rb index c3ab2b32..08025957 100644 --- a/app/helpers/trestle/grid_helper.rb +++ b/app/helpers/trestle/grid_helper.rb @@ -1,9 +1,9 @@ module Trestle module GridHelper - # Renders a row div, one of the building blocks of Bootstrap's grid system. - # https://getbootstrap.com/docs/4.4/layout/grid/ + # Renders a row
tag, one of the building blocks of Bootstrap's grid system. + # https://getbootstrap.com/docs/5.3/layout/grid/ # - # attrs - Hash of attributes that will be passed to the tag (e.g. id, data, class). + # attributes - Hash of attributes that will be passed to the tag (e.g. id, data, class) # # Examples # @@ -14,15 +14,13 @@ module GridHelper # <%= row class: "row-cols-2", id: "my-row" do %> ... # # Returns a HTML-safe String. - def row(attrs={}) + def row(**attributes) defaults = Trestle::Options.new(class: ["row"]) - options = defaults.merge(attrs) - - content_tag(:div, options) { yield } + tag.div(**defaults.merge(attributes)) { yield } end - # Renders a column div, one of the building blocks of Bootstrap's grid system. - # https://getbootstrap.com/docs/4.4/layout/grid/ + # Renders a column
tag, one of the building blocks of Bootstrap's grid system. + # https://getbootstrap.com/docs/5.3/layout/grid/ # # Column divs should always be rendered inside of a row div. # @@ -53,16 +51,16 @@ def col(columns=nil, breakpoints={}) classes += breakpoints.map { |breakpoint, span| "col-#{breakpoint}-#{span}" } end - content_tag(:div, class: classes) { yield } + tag.div(class: classes) { yield } end - # Renders an
(horizontal rule) HTML tag. + # Renders an
(horizontal rule) HTML tag. # - # attrs - Hash of attributes that will be passed to the tag (e.g. id, data, class). + # attributes - Hash of attributes that will be passed to the
tag # # Returns a HTML-safe String. - def divider(attrs={}) - tag(:hr, attrs) + def divider(**attributes) + tag.hr(**attributes) end end end diff --git a/app/helpers/trestle/headings_helper.rb b/app/helpers/trestle/headings_helper.rb index 9540466c..8ad6b590 100644 --- a/app/helpers/trestle/headings_helper.rb +++ b/app/helpers/trestle/headings_helper.rb @@ -1,27 +1,6 @@ module Trestle module HeadingsHelper - def h1(text, options={}) - content_tag(:h1, text, options) - end - - def h2(text, options={}) - content_tag(:h2, text, options) - end - - def h3(text, options={}) - content_tag(:h3, text, options) - end - - def h4(text, options={}) - content_tag(:h4, text, options) - end - - def h5(text, options={}) - content_tag(:h5, text, options) - end - - def h6(text, options={}) - content_tag(:h6, text, options) - end + # These methods are delegated to the ActionView::Helpers::TagHelper proxy object for convenience. + delegate :h1, :h2, :h3, :h4, :h5, :h6, to: :tag end end diff --git a/app/helpers/trestle/i18n_helper.rb b/app/helpers/trestle/i18n_helper.rb index 847a8974..08f6abd3 100644 --- a/app/helpers/trestle/i18n_helper.rb +++ b/app/helpers/trestle/i18n_helper.rb @@ -1,4 +1,5 @@ module Trestle + # [Internal] module I18nHelper FLATPICKR_LOCALE_CONVERSIONS = { ca: "cat", el: "gr", nb: "no", vi: "vn" }.stringify_keys diff --git a/app/helpers/trestle/icon_helper.rb b/app/helpers/trestle/icon_helper.rb index 9607cee7..52abe54f 100644 --- a/app/helpers/trestle/icon_helper.rb +++ b/app/helpers/trestle/icon_helper.rb @@ -1,8 +1,21 @@ module Trestle module IconHelper - def icon(*classes) - options = classes.extract_options! - content_tag(:i, "", options.merge(class: classes)) + # Renders an icon (as an tag). + # + # Trestle includes the FontAwesome icon library but other font + # libraries can be included via custom CSS. + # + # classes - List of font name classes to add to the tag + # attributes - Additional HTML attributes to add to the tag + # + # Examples + # + # <%= icon("fas fa-star") %> + # <%= icon("fas", "fa-star", class: "fa-fw text-muted") + # + # Return the HTML i tag for the icon. + def icon(*classes, **attributes) + tag.i("", **attributes.merge(class: [*classes, attributes[:class]])) end end end diff --git a/app/helpers/trestle/layout_helper.rb b/app/helpers/trestle/layout_helper.rb index 7dcaa2d4..4e01fd8a 100644 --- a/app/helpers/trestle/layout_helper.rb +++ b/app/helpers/trestle/layout_helper.rb @@ -1,4 +1,5 @@ module Trestle + # [Internal] module LayoutHelper SIDEBAR_CLASSES = { "expanded" => "sidebar-expanded", diff --git a/app/helpers/trestle/modal_helper.rb b/app/helpers/trestle/modal_helper.rb index 8556c2ac..3fc3fecc 100644 --- a/app/helpers/trestle/modal_helper.rb +++ b/app/helpers/trestle/modal_helper.rb @@ -1,16 +1,34 @@ module Trestle module ModalHelper + # Merges the given options with the existing hash of defined modal options. + # + # Trestle uses Bootstrap modal markup (https://getbootstrap.com/docs/5.3/components/modal/) + # to render modals, which consist of an outer .modal wrapper div with an inner + # .modal-dialog div. + # + # The `class` attribute (e.g. `"modal-lg"`) is applied to the inner element, and all + # other attributes are applied to the outer element. A class can be applied to the + # wrapper element using the `wrapper_class` option. + # + # Examples + # + # <% modal_options! class: "modal-lg", controller: "modal-stimulus" %> + # + # <% modal_options! wrapper_class: "custom-modal-animation" %> + # def modal_options!(options) modal_options.merge!(options) end + # Returns a hash of the currently defined modal options def modal_options @_modal_options ||= {} end + # Returns the HTML attributes to apply to the modal wrapper (.modal)
def modal_wrapper_attributes { - class: ["modal", "fade", modal_options[:wrapper_class]], + class: ["modal", "fade", modal_options[:wrapper_class]].compact, tabindex: "-1", role: "dialog", data: { @@ -19,9 +37,10 @@ def modal_wrapper_attributes }.deep_merge(modal_options.except(:class, :wrapper_class, :controller)) end + # Returns the HTML attributes to apply to the inner modal dialog (.modal-dialog)
def modal_dialog_attributes { - class: ["modal-dialog", modal_options[:class]], + class: ["modal-dialog", modal_options[:class]].compact, role: "document" } end diff --git a/app/helpers/trestle/navigation_helper.rb b/app/helpers/trestle/navigation_helper.rb index 7c1ffc0b..94c96e4d 100644 --- a/app/helpers/trestle/navigation_helper.rb +++ b/app/helpers/trestle/navigation_helper.rb @@ -1,4 +1,5 @@ module Trestle + # [Internal] module NavigationHelper def current_navigation_item?(item) current_page?(item.path) || (item.admin && current_admin?(item.admin)) diff --git a/app/helpers/trestle/pagination_helper.rb b/app/helpers/trestle/pagination_helper.rb index 4764f8ee..ee6bfc55 100644 --- a/app/helpers/trestle/pagination_helper.rb +++ b/app/helpers/trestle/pagination_helper.rb @@ -1,4 +1,5 @@ module Trestle + # [Internal] module PaginationHelper # Custom version of Kaminari's page_entries_info helper to use a # Trestle-scoped I18n key and add a delimiter to the total count. diff --git a/app/helpers/trestle/params_helper.rb b/app/helpers/trestle/params_helper.rb index 35a4b293..c50386ce 100644 --- a/app/helpers/trestle/params_helper.rb +++ b/app/helpers/trestle/params_helper.rb @@ -1,5 +1,16 @@ module Trestle module ParamsHelper + # Returns a subset of the params "hash" (an instance of ActionController::Parameters) + # limited to only those keys that should be considered persistent throughout + # reordering and pagination. + # + # This could be a scope or the current order, but this may be extended by Trestle + # plugins and the application itself in `Trestle.config.persistent_params` to include + # search queries, filters, etc. + # + # By default this list is set to: [:sort, :order, :scope] + # + # Returns an instance of ActionController::Parameters. def persistent_params flat, nested = Trestle.config.persistent_params.partition { |p| !p.is_a?(Hash) } nested = nested.inject({}) { |result, param| result.merge(param) } diff --git a/app/helpers/trestle/sort_helper.rb b/app/helpers/trestle/sort_helper.rb index 940516ab..cce91d36 100644 --- a/app/helpers/trestle/sort_helper.rb +++ b/app/helpers/trestle/sort_helper.rb @@ -1,20 +1,47 @@ module Trestle module SortHelper - def sort_link(text, field, options={}) - sort_link = SortLink.new(field, persistent_params, options) + # Renders a sort link for a table column header. + # + # The `sort` and `order` params are used to determine whether the sort link is + # active, and which is the current sort direction (asc or desc). CSS classes are + # applied accordingly which are used to render the appropriate icons alongside the link. + # + # The current set of `persistent_params` is merged with the new `sort`/`order` params + # to build the target link URL. + # + # text - Text or HTML content to render as the link label + # field - The name of the current field, which should match the `sort` param + # when the collection is being sorted by this field + # options - Hash of options (default: {}): + # :default - (Boolean) Set to true if the field is considered to be active + # even when the `sort` param is blank (default: false) + # :default_order - (String/Symbol) Specify the default collection order when + # the `order` param is blank (default: "asc") + # + # Examples + # + # <%= sort_link "Title", :title, default: true %> + # + # <%= sort_link "Created", :created_at, default_order: "desc" %> + # + # Returns a HTML-safe String. + def sort_link(text, field, **options) + sort_link = SortLink.new(field, persistent_params, **options) link_to text, sort_link.params, class: sort_link.classes end class SortLink attr_reader :field - def initialize(field, params, options) - @field, @params, @options = field, params, options + def initialize(field, params, default: false, default_order: "asc") + @field, @params = field, params + + @default = default + @default_order = default_order.to_s end def active? - @params[:sort] == field.to_s || - (@options[:default] && !@params.key?(:sort)) + @params[:sort] == field.to_s || (default? && !@params.key?(:sort)) end def params @@ -33,8 +60,12 @@ def current_order @params[:order] || default_order end + def default? + @default + end + def default_order - @options.fetch(:default_order, "asc").to_s + @default_order end def classes diff --git a/app/helpers/trestle/status_helper.rb b/app/helpers/trestle/status_helper.rb index 7a961dc8..36d37d12 100644 --- a/app/helpers/trestle/status_helper.rb +++ b/app/helpers/trestle/status_helper.rb @@ -1,8 +1,24 @@ module Trestle module StatusHelper - def status_tag(label, status=:primary, options={}) - options[:class] ||= ["badge", "badge-#{status}"] - content_tag(:span, label, options) + # Renders a status indicator as a Bootstrap badge. + # (https://getbootstrap.com/docs/5.3/components/badge/) + # + # label - Status badge text or HTML content + # status - Status class (as .badge-{status}) to apply to the badge (default: :primary) + # attributes - Additional HTML attributes to add to the tag + # + # Examples + # + # <%= status_tag("Status Text") %> + # + # <%= status_tag(icon("fas fa-check"), :success) %> + # + # <%= status_tag(safe_join([icon("fas fa-warning"), "Error"], " "), :danger, + # data: { controller: "tooltip" }, title: "Full error message") %> + # + # Returns a HTML-safe String. + def status_tag(label, status=:primary, **attributes) + tag.span(label, **attributes.merge(class: ["badge", "badge-#{status}", attributes[:class]])) end end end diff --git a/app/helpers/trestle/tab_helper.rb b/app/helpers/trestle/tab_helper.rb index 9994f3dc..756dd0c0 100644 --- a/app/helpers/trestle/tab_helper.rb +++ b/app/helpers/trestle/tab_helper.rb @@ -1,13 +1,34 @@ module Trestle module TabHelper - def tabs - @_trestle_tabs ||= {} - end + # Creates a tab pane using content via the given block, :partial option or partial + # template automatically inferred from the tab name. + # + # It also appends a Trestle::Tab object to the list of declared tabs that is + # accessible via the #tabs helper (e.g. for rendering the tab links). + # + # name - (Symbol) Internal name for the tab + # options - Hash of options (default: {}): + # :label - Optional tab label. If not provided, will be inferred by the + # admin-translated tab name (`admin.tabs.{name}` i18n scope) + # :badge - Optional badge to show next to the tab label (e.g. a counter) + # :partial - Optional partial template name to use when a block is not provided + # + # Examples + # + # <%= tab :details %> + # => Automatically renders the 'details' partial (e.g. "_details.html.erb") as the tab content + # + # <%= tab :metadata, partial: "meta" %> + # => Renders the 'meta' partial (e.g. "_meta.html.erb") as the tab content + # + # <%= tab :comments do %> ... + # => Renders the given block as the tab content + # + # Returns a HTML-safe String. + def tab(name, **options) + tabs[name] = tab = Tab.new(name, **options) - def tab(name, options={}) - tabs[name] = tab = Tab.new(name, options) - - content_tag(:div, id: tab.id(("modal" if modal_request?)), class: ["tab-pane", ('active' if name == tabs.keys.first)], role: "tabpanel") do + tag.div(id: tab.id(("modal" if modal_request?)), class: ["tab-pane", ('active' if name == tabs.keys.first)], role: "tabpanel") do if block_given? yield elsif options[:partial] @@ -17,5 +38,19 @@ def tab(name, options={}) end end end + + # Returns a hash (name => Trestle::Tab) of the currently declared tabs. + def tabs + @_trestle_tabs ||= {} + end + + # Captures the given block (using `content_for`) as the sidebar content. + def sidebar(&block) + content_for(:sidebar, &block) + end + + def render_sidebar_as_tab? + modal_request? && content_for?(:sidebar) + end end end diff --git a/app/helpers/trestle/table_helper.rb b/app/helpers/trestle/table_helper.rb index 9cff5ebb..65629cff 100644 --- a/app/helpers/trestle/table_helper.rb +++ b/app/helpers/trestle/table_helper.rb @@ -2,15 +2,17 @@ module Trestle module TableHelper # Renders an existing named table or builds and renders a custom table if a block is provided. # - # name - The (optional) name of the table to render (as a Symbol), or the actual Trestle::Table instance itself. - # options - Hash of options that will be passed to the table builder (default: {}): - # :collection - The collection that should be rendered within the table. It should be an - # Array-like object, but will most likely be an ActiveRecord scope. It can - # also be a callable object (i.e. a Proc) in which case the result of calling - # the block will be used as the collection. - # See Trestle::Table::Builder for additional options. - # block - An optional block that is passed to Trestle::Table::Builder to define a custom table. - # One of either the name or block must be provided, but not both. + # One of either the name or block must be provided, but not both. + # + # name - The (optional) name of the table to render (as a Symbol), or the + # actual Trestle::Table instance itself + # collection - The collection that should be rendered within the table. + # It should be an Array-like object, but will most likely be an + # ActiveRecord scope. It can also be a callable object (i.e. a Proc) in + # which case the result of calling the block will be used as the collection + # options - Hash of options that will be passed to the table builder (default: {}). + # See Trestle::Table::Builder for additional options + # block - An optional block that is passed to Trestle::Table::Builder to define a custom table # # Examples # @@ -23,26 +25,23 @@ module TableHelper # <%= table :accounts %> # # Returns the HTML representation of the table as a HTML-safe String. - def table(name=nil, options={}, &block) + def table(name=nil, collection: nil, **options, &block) if block_given? - if name.is_a?(Hash) - options = name - else - collection = name - end - - table = Table::Builder.build(options, &block) + table = Table::Builder.build(**options, &block) else if name.is_a?(Trestle::Table) table = name else - table = admin.tables.fetch(name) { raise ArgumentError, "Unable to find table named #{name.inspect}" } + table = admin.tables.fetch(name) { + raise ArgumentError, "Unable to find table named #{name.inspect}" + } end - table = table.with_options(options.reverse_merge(sortable: false)) + table = table.with_options(sortable: false, **options) end - collection ||= options[:collection] || table.options[:collection] + collection ||= name if block_given? + collection ||= table.options[:collection] collection = collection.call if collection.respond_to?(:call) render "trestle/table/table", table: table, collection: collection @@ -50,8 +49,9 @@ def table(name=nil, options={}, &block) # Renders the pagination controls for a collection. # - # collection - The paginated Kaminari collection to render controls for (required). - # options - Hash of options that will be passed to the Kaminari #paginate method (default: {}): + # collection - The paginated Kaminari collection to render controls for (required) + # entry_name - Custom item name passed to the Kaminari #page_entries_info helper + # options - Hash of options that will be passed to the Kaminari #paginate method (default: {}) # # Examples # diff --git a/app/helpers/trestle/timestamp_helper.rb b/app/helpers/trestle/timestamp_helper.rb index 2107c6f3..f96be44b 100644 --- a/app/helpers/trestle/timestamp_helper.rb +++ b/app/helpers/trestle/timestamp_helper.rb @@ -1,13 +1,13 @@ module Trestle module TimestampHelper - # Renders a Time object as a formatted timestamp (using a