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