diff --git a/lib/tiny_admin/raw_html.rb b/lib/tiny_admin/raw_html.rb new file mode 100644 index 0000000..7aed38e --- /dev/null +++ b/lib/tiny_admin/raw_html.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module TinyAdmin + class RawHtml + attr_reader :to_s + + def initialize(value) + @to_s = value.to_s + end + end +end diff --git a/lib/tiny_admin/support.rb b/lib/tiny_admin/support.rb index 4eda149..5914f42 100644 --- a/lib/tiny_admin/support.rb +++ b/lib/tiny_admin/support.rb @@ -3,6 +3,10 @@ module TinyAdmin class Support class << self + def raw_html(value) + TinyAdmin::RawHtml.new(value) + end + def call(value, options: []) options.inject(value) { |result, message| result&.send(message) } if value && options&.any? end diff --git a/lib/tiny_admin/views/components/field_value.rb b/lib/tiny_admin/views/components/field_value.rb index 8d0695e..62e69e0 100644 --- a/lib/tiny_admin/views/components/field_value.rb +++ b/lib/tiny_admin/views/components/field_value.rb @@ -18,15 +18,25 @@ def view_template if field.options[:link_to] a(href: TinyAdmin.route_for(field.options[:link_to], reference: translated_value)) { span(class: value_class) { - field.apply_call_option(record) || translated_value + render_value(field.apply_call_option(record) || translated_value) } } else span(class: value_class) { - translated_value + render_value(translated_value) } end end + + private + + def render_value(val) + if val.is_a?(TinyAdmin::RawHtml) + unsafe_raw(val.to_s) + else + val + end + end end end end diff --git a/spec/dummy_rails/app/models/author.rb b/spec/dummy_rails/app/models/author.rb index 5d5ed59..564752b 100644 --- a/spec/dummy_rails/app/models/author.rb +++ b/spec/dummy_rails/app/models/author.rb @@ -31,4 +31,12 @@ class Author < ApplicationRecord def to_s "#{name} (#{age})" end + + def stats + [ + "Posts: #{posts.count}", + "Published: #{published_posts.count}", + "Recent: #{recent_posts.count}" + ] + end end diff --git a/spec/dummy_rails/app/models/post.rb b/spec/dummy_rails/app/models/post.rb index dce62ba..6f1b89f 100644 --- a/spec/dummy_rails/app/models/post.rb +++ b/spec/dummy_rails/app/models/post.rb @@ -17,7 +17,7 @@ class Post < ApplicationRecord validates :title, allow_blank: false, presence: true scope :published, -> { where(published: true) } - scope :recents, -> { where('created_at > ?', Date.current - 8.months) } + scope :recents, -> { where('created_at > ?', Time.current - 1.week) } # # override a field - can be dangerous # def title diff --git a/spec/dummy_rails/app/tiny_admin/admin_helper.rb b/spec/dummy_rails/app/tiny_admin/admin_helper.rb index 36e8d6d..381a2ed 100644 --- a/spec/dummy_rails/app/tiny_admin/admin_helper.rb +++ b/spec/dummy_rails/app/tiny_admin/admin_helper.rb @@ -1,2 +1,7 @@ class AdminHelper < TinyAdmin::Support + class << self + def multiline(array, options: []) + raw_html array.join("
") + end + end end diff --git a/spec/dummy_rails/config/tiny_admin.yml b/spec/dummy_rails/config/tiny_admin.yml index 3b87de9..bebe75c 100644 --- a/spec/dummy_rails/config/tiny_admin.yml +++ b/spec/dummy_rails/config/tiny_admin.yml @@ -42,6 +42,23 @@ sections: - id - name - email + - field: stats + header: Some stats + method: multiline + links: + - show + - sample_mem + show: + attributes: + - id + - name + - email + - age + - field: stats + header: Some stats + method: multiline + - created_at + - updated_at links: - show - sample_mem diff --git a/spec/lib/tiny_admin/views/components/field_value_spec.rb b/spec/lib/tiny_admin/views/components/field_value_spec.rb index ada0cd3..d58ee0a 100644 --- a/spec/lib/tiny_admin/views/components/field_value_spec.rb +++ b/spec/lib/tiny_admin/views/components/field_value_spec.rb @@ -31,6 +31,39 @@ end end + describe "with a RawHtml value" do + let(:field) { TinyAdmin::Field.new(name: "items", type: :string, title: "Items", options: { method: "multiline" }) } + let(:record) { double("record", id: 1) } # rubocop:disable RSpec/VerifiedDoubles + + before do + allow(TinyAdmin.settings.helper_class).to receive(:multiline) + .and_return(TinyAdmin::RawHtml.new("1
2
3")) + end + + it "renders the value as raw HTML without escaping", :aggregate_failures do + html = described_class.new(field, [1, 2, 3], record: record).call + expect(html).to include("") + expect(html).to include("1
2
3") + expect(html).not_to include("<br/>") + end + end + + describe "with a RawHtml value inside a link" do + let(:field) { TinyAdmin::Field.new(name: "items", type: :string, title: "Items", options: { method: "multiline", link_to: "posts" }) } + let(:record) { double("record", id: 1) } # rubocop:disable RSpec/VerifiedDoubles + + before do + allow(TinyAdmin.settings.helper_class).to receive(:multiline) + .and_return(TinyAdmin::RawHtml.new("1
2
3")) + end + + it "renders the value as raw HTML inside a link without escaping", :aggregate_failures do + html = described_class.new(field, [1, 2, 3], record: record).call + expect(html).to include("1
2
3
") + end + end + describe "with value_class option" do let(:field) { TinyAdmin::Field.new(name: "status", type: :string, title: "Status", options: { options: ["value_class"] }) } let(:record) { double("record", id: 1) } # rubocop:disable RSpec/VerifiedDoubles