-
- <%= render Avo::PaginatorComponent.new pagy: @pagy, turbo_frame: turbo_frame || 'none', index_params: @index_params, resource: @resource, parent_record: parent_record, discreet_pagination: field&.discreet_pagination %>
+ <%= render Avo::PaginatorComponent.new pagy: @pagy, turbo_frame: turbo_frame || "none", index_params: @index_params, resource: @resource, parent_record: parent_record, discreet_pagination: field&.discreet_pagination %>
<% end %>
<% end %>
diff --git a/lib/avo/base_resource.rb b/lib/avo/base_resource.rb
index 6c8591182d..47dbf23f64 100644
--- a/lib/avo/base_resource.rb
+++ b/lib/avo/base_resource.rb
@@ -9,6 +9,8 @@ class BaseResource
include Avo::Concerns::HasResourceStimulusControllers
include Avo::Concerns::ModelClassConstantized
include Avo::Concerns::HasDescription
+ include Avo::Concerns::HasCoverPhoto
+ include Avo::Concerns::HasProfilePhoto
include Avo::Concerns::HasHelpers
include Avo::Concerns::Hydration
include Avo::Concerns::Pagination
diff --git a/lib/avo/concerns/has_cover_photo.rb b/lib/avo/concerns/has_cover_photo.rb
new file mode 100644
index 0000000000..9d29cd2a49
--- /dev/null
+++ b/lib/avo/concerns/has_cover_photo.rb
@@ -0,0 +1,18 @@
+# Adds the ability to set the visibility of an item in the execution context.
+module Avo
+ module Concerns
+ module HasCoverPhoto
+ extend ActiveSupport::Concern
+
+ class_methods do
+ # Add class property to capture the settings
+ attr_accessor :cover_photo
+ end
+
+ # Add instance property to compute the options
+ def cover_photo
+ Avo::CoverPhoto.new resource: self
+ end
+ end
+ end
+end
diff --git a/lib/avo/concerns/has_profile_photo.rb b/lib/avo/concerns/has_profile_photo.rb
new file mode 100644
index 0000000000..38b524939a
--- /dev/null
+++ b/lib/avo/concerns/has_profile_photo.rb
@@ -0,0 +1,18 @@
+# Adds the ability to set the visibility of an item in the execution context.
+module Avo
+ module Concerns
+ module HasProfilePhoto
+ extend ActiveSupport::Concern
+
+ class_methods do
+ # Add class property to capture the settings
+ attr_accessor :profile_photo
+ end
+
+ # Add instance property to compute the options
+ def profile_photo
+ ProfilePhoto.new resource: self
+ end
+ end
+ end
+end
diff --git a/lib/avo/configuration/branding.rb b/lib/avo/configuration/branding.rb
index 16493d1373..1d12ea0c50 100644
--- a/lib/avo/configuration/branding.rb
+++ b/lib/avo/configuration/branding.rb
@@ -8,7 +8,7 @@ def initialize(colors: nil, chart_colors: nil, logo: nil, logomark: nil, placeho
@favicon = favicon
@default_colors = {
- background: "#F6F6F7",
+ :background => "#F6F6F7",
100 => "206 231 248",
400 => "57 158 229",
500 => "8 134 222",
diff --git a/lib/avo/cover_photo.rb b/lib/avo/cover_photo.rb
new file mode 100644
index 0000000000..7ea7c5888a
--- /dev/null
+++ b/lib/avo/cover_photo.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Avo
+ class CoverPhoto < PhotoObject
+ def key = :cover_photo
+
+ def size
+ options.fetch(:size, :md)
+ end
+ end
+end
diff --git a/lib/avo/photo_object.rb b/lib/avo/photo_object.rb
new file mode 100644
index 0000000000..4a8590f7e4
--- /dev/null
+++ b/lib/avo/photo_object.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+class Avo::PhotoObject
+ def initialize(resource:)
+ @resource = resource
+ end
+
+ delegate :record, to: :@resource
+ delegate :view, to: :@resource
+
+ def options
+ @options ||= if @resource.class&.send(key).present?
+ @resource.class&.send(key)
+ else
+ {}
+ end
+ end
+
+ def value
+ return unless options.fetch(:source, nil).present?
+
+ if options[:source].is_a?(Symbol)
+ record.send(options[:source])
+ elsif options[:source].respond_to?(:call)
+ Avo::ExecutionContext.new(target: options[:source], record:, resource: @resource, view:).handle
+ end
+ end
+
+ def visible_on
+ @visible_on ||= Array.wrap(options[:visible_on] || [:show, :forms])
+ end
+
+ def visible_in_current_view?
+ send(:"visible_on_#{view}?")
+ end
+
+ def present?
+ value.present?
+ end
+
+ def visible_on_index? = visible_in_either?(:index, :display)
+
+ def visible_on_show? = visible_in_either?(:show, :display)
+
+ def visible_on_edit? = visible_in_either?(:edit, :forms)
+
+ def visible_on_new? = visible_in_either?(:new, :forms)
+
+ private
+
+ def visible_in_either?(*options)
+ options.map do |option|
+ visible_on.include?(option)
+ end.uniq.first.eql?(true)
+ end
+end
diff --git a/lib/avo/profile_photo.rb b/lib/avo/profile_photo.rb
new file mode 100644
index 0000000000..85c00d32f6
--- /dev/null
+++ b/lib/avo/profile_photo.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Avo
+ class ProfilePhoto < PhotoObject
+ def key = :profile_photo
+ end
+end
diff --git a/lib/avo/view_inquirer.rb b/lib/avo/view_inquirer.rb
index 389a97a56e..88efc72af0 100644
--- a/lib/avo/view_inquirer.rb
+++ b/lib/avo/view_inquirer.rb
@@ -30,7 +30,7 @@ def ==(other)
end
def in?(another_object)
- super another_object.map(&:to_s)
+ super(another_object.map(&:to_s))
end
end
end
diff --git a/spec/components/avo/cover_photo_component_spec.rb b/spec/components/avo/cover_photo_component_spec.rb
new file mode 100644
index 0000000000..2b9324ecf5
--- /dev/null
+++ b/spec/components/avo/cover_photo_component_spec.rb
@@ -0,0 +1,64 @@
+require "rails_helper"
+
+RSpec.describe Avo::CoverPhotoComponent, type: :component do
+ let(:cover_photo) { double("cover_photo") }
+ let(:resource) { double("resource", cover_photo: cover_photo) }
+
+ describe "rendering" do
+ context "when cover photo is present and visible in the current view" do
+ before do
+ allow(cover_photo).to receive(:present?).and_return(true)
+ allow(cover_photo).to receive(:visible_in_current_view?).and_return(true)
+ allow(cover_photo).to receive(:value).and_return("cover_photo_value")
+ allow(cover_photo).to receive(:size).and_return(:md)
+ end
+
+ it "renders the component with the correct size and visibility settings" do
+ render_inline(described_class.new(cover_photo: cover_photo))
+
+ expect(rendered_component).to have_css(".aspect-cover-md")
+ expect(rendered_component).to have_selector("img[src='cover_photo_value']")
+ end
+ end
+
+ context "when cover photo is not present" do
+ before do
+ allow(cover_photo).to receive(:present?).and_return(false)
+ end
+
+ it "does not render the component" do
+ render_inline(described_class.new(cover_photo: cover_photo))
+
+ expect(rendered_component).to be_blank
+ end
+ end
+
+ context "when cover photo is not visible in the current view" do
+ before do
+ allow(cover_photo).to receive(:visible_in_current_view?).and_return(false)
+ end
+
+ it "does not render the component" do
+ render_inline(described_class.new(cover_photo: cover_photo))
+
+ expect(rendered_component).to be_blank
+ end
+ end
+
+ context "when cover photo source is a lambda" do
+ let(:lambda_source) { -> { "dynamic_source_value" } }
+
+ before do
+ allow(cover_photo).to receive(:present?).and_return(true)
+ allow(cover_photo).to receive(:visible_in_current_view?).and_return(true)
+ allow(cover_photo).to receive(:value).and_return(lambda_source.call)
+ end
+
+ it "correctly handles dynamic sources" do
+ render_inline(described_class.new(cover_photo: cover_photo))
+
+ expect(rendered_component).to have_selector("img[src='dynamic_source_value']")
+ end
+ end
+ end
+end
diff --git a/spec/dummy/app/avo/resources/event.rb b/spec/dummy/app/avo/resources/event.rb
index ef2e90ed18..368ccffff5 100644
--- a/spec/dummy/app/avo/resources/event.rb
+++ b/spec/dummy/app/avo/resources/event.rb
@@ -3,6 +3,21 @@ class Avo::Resources::Event < Avo::BaseResource
self.description = "An event that happened at a certain time."
self.includes = [:location]
+ self.cover_photo = {
+ # size: :sm,
+ visible_on: [:show, :index],
+ source: -> {
+ if record.present?
+ record.cover_photo
+ else
+ Event.first&.cover_photo
+ end
+ }
+ }
+ self.profile_photo = {
+ source: :profile_photo
+ }
+
def fields
field :name, as: :text, link_to_record: true, sortable: true, stacked: true
field :first_user,
@@ -17,6 +32,9 @@ def fields
foo: :bar,
}
+ field :profile_photo, as: :file, is_image: true
+ field :cover_photo, as: :file, is_image: true
+
if params[:show_location_field] == "1"
# Example for error message when resource is missing
field :location, as: :belongs_to
diff --git a/spec/dummy/app/avo/resources/store.rb b/spec/dummy/app/avo/resources/store.rb
index 239983dd0a..370ee655e1 100644
--- a/spec/dummy/app/avo/resources/store.rb
+++ b/spec/dummy/app/avo/resources/store.rb
@@ -6,7 +6,7 @@ def fields
field :name, as: :text
field :size, as: :text
- if params[:show_location_field] == '1'
+ if params[:show_location_field] == "1"
# Example for error message when resource is missing
field :location, as: :has_one
end
diff --git a/spec/dummy/app/avo/resources/team.rb b/spec/dummy/app/avo/resources/team.rb
index 8001fbf5a9..7498f9021c 100644
--- a/spec/dummy/app/avo/resources/team.rb
+++ b/spec/dummy/app/avo/resources/team.rb
@@ -14,9 +14,9 @@ class Avo::Resources::Team < Avo::BaseResource
}
def fields
- field :preview, as: :preview
-
main_panel do
+ field :preview, as: :preview
+
unless params[:hide_id]
field :id, as: :id, filterable: true
end
diff --git a/spec/dummy/app/models/event.rb b/spec/dummy/app/models/event.rb
index 5fb719e5c8..9b9ec60bd2 100644
--- a/spec/dummy/app/models/event.rb
+++ b/spec/dummy/app/models/event.rb
@@ -15,6 +15,9 @@ class Event < ApplicationRecord
belongs_to :location, optional: true
+ has_one_attached :profile_photo
+ has_one_attached :cover_photo
+
def first_user
User.first
end
diff --git a/spec/dummy/db/seed_files/dummy-image.jpg b/spec/dummy/db/seed_files/dummy-image.jpg
new file mode 100644
index 0000000000..2a09202174
Binary files /dev/null and b/spec/dummy/db/seed_files/dummy-image.jpg differ
diff --git a/spec/features/avo/app_spec.rb b/spec/features/avo/app_spec.rb
index e75bc6ac25..2d5630f44c 100644
--- a/spec/features/avo/app_spec.rb
+++ b/spec/features/avo/app_spec.rb
@@ -13,7 +13,7 @@
end
describe "Current.user is set" do
- it "displayes the current user" do
+ it "displays the current user" do
visit "/admin/custom_tool"
# Label on the menu builder
diff --git a/spec/features/avo/cover_profile_photos_spec.rb b/spec/features/avo/cover_profile_photos_spec.rb
new file mode 100644
index 0000000000..cabf3a3f44
--- /dev/null
+++ b/spec/features/avo/cover_profile_photos_spec.rb
@@ -0,0 +1,50 @@
+require "rails_helper"
+
+RSpec.describe "Cover and profile photos", type: :feature do
+ let!(:event) {
+ create(:event).tap do |event|
+ event.profile_photo.attach(io: Avo::Engine.root.join("spec", "dummy", "db", "seed_files", "dummy-image.jpg").open, filename: "dummy-image.jpg")
+ event.cover_photo.attach(io: Avo::Engine.root.join("spec", "dummy", "db", "seed_files", "dummy-image.jpg").open, filename: "dummy-image.jpg")
+ end
+ }
+
+ describe "event page without cover and profile photo" do
+ it "does not display the cover and profile photo components" do
+ event.profile_photo.purge
+ event.cover_photo.purge
+ visit avo.resources_event_path(event)
+
+ expect(page).not_to have_selector '[data-component="avo/cover_photo_component"]'
+ expect(page).not_to have_selector '[data-component="avo/profile_photo_component"]'
+ end
+ end
+
+ describe "event page with only cover photo" do
+ it "displays only the cover photo component" do
+ event.profile_photo.purge
+ visit avo.resources_event_path(event)
+
+ expect(page).to have_selector '[data-component="avo/cover_photo_component"]'
+ expect(page).not_to have_selector '[data-component="avo/profile_photo_component"]'
+ end
+ end
+
+ describe "event page with only profile photo" do
+ it "displays only the profile photo component" do
+ event.cover_photo.purge
+ visit avo.resources_event_path(event)
+
+ expect(page).not_to have_selector '[data-component="avo/cover_photo_component"]'
+ expect(page).to have_selector '[data-component="avo/profile_photo_component"]'
+ end
+ end
+
+ describe "event page with both cover and profile photos" do
+ it "displays both the cover and profile photo components" do
+ visit avo.resources_event_path(event)
+
+ expect(page).to have_selector '[data-component="avo/cover_photo_component"]'
+ expect(page).to have_selector '[data-component="avo/profile_photo_component"]'
+ end
+ end
+end
diff --git a/tailwind.preset.js b/tailwind.preset.js
index 40eaa621dc..3f8fd4ce6f 100644
--- a/tailwind.preset.js
+++ b/tailwind.preset.js
@@ -27,6 +27,11 @@ module.exports = {
],
theme: {
extend: {
+ aspectRatio: {
+ 'cover-sm': '9/2',
+ 'cover-md': '9/3',
+ 'cover-lg': '9/4',
+ },
colors: {
blue,
gray,
@@ -152,6 +157,8 @@ module.exports = {
// Add has-sidebar variant to make it easier to target fields in panels and use the full-width
addVariant('has-sidebar', '.has-sidebar & ')
addVariant('has-record-selector', '.has-record-selector & ')
+ addVariant('has-profile-photo', '.has-profile-photo & ')
+ addVariant('has-cover-photo', '.has-cover-photo & ')
addVariant('index-grid-view', '.index-grid-view & ')
addVariant('index-table-view', '.index-table-view & ')
}),