Skip to content

Commit

Permalink
feature: discreet information (#3592)
Browse files Browse the repository at this point in the history
* feature: discreet information

* wip

* wip

* lint

* refactor i18n

* wip

* remove old concern

* fix mobile cover variant

* wip

* wip

* wip

* lint

* tests

---------

Co-authored-by: Paul Bob <[email protected]>
  • Loading branch information
adrianthedev and Paul-Bob authored Feb 28, 2025
1 parent 2f2e814 commit 6a6d122
Show file tree
Hide file tree
Showing 26 changed files with 352 additions and 27 deletions.
3 changes: 0 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -445,9 +445,6 @@ GEM
prettyprint (0.2.0)
prop_initializer (0.2.0)
zeitwerk (>= 2.6.18)
psych (5.2.3)
date
stringio
public_suffix (6.0.1)
puma (6.6.0)
nio4r (~> 2.0)
Expand Down
7 changes: 7 additions & 0 deletions app/components/avo/discreet_information_component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<div class="flex gap-2 ml-2 mt-1">
<% items.each do |item| %>
<%= content_tag element_tag(item), **element_attributes(item), class: "flex gap-1 text-xs font-normal text-gray-600 hover:text-gray-900", title: item.tooltip, data: {tippy: :tooltip, **data(item)} do %>
<%= item.label if item.label.present? %> <%= helpers.svg item.icon, class: "text-2xl h-4" %>
<% end %>
<% end %>
</div>
27 changes: 27 additions & 0 deletions app/components/avo/discreet_information_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

class Avo::DiscreetInformationComponent < Avo::BaseComponent
prop :payload

def items
@payload.items.compact
end

def element_tag(item)
if item.url.present?
:a
else
:div
end
end

def element_attributes(item)
if item.url.present?
{href: item.url, target: item.url_target}
else
{}
end
end

def data(item) = item.data || {}
end
3 changes: 2 additions & 1 deletion app/components/avo/items/panel_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ def args
data: {panel_id: "main"},
cover_photo: @resource.cover_photo,
profile_photo: @resource.profile_photo,
external_link: @resource.get_external_link
external_link: @resource.get_external_link,
discreet_information: @resource.discreet_information
}
else
{name: @item.name, description: @item.description, index: @index}
Expand Down
3 changes: 2 additions & 1 deletion app/components/avo/panel_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
description: @description,
display_breadcrumbs: @display_breadcrumbs,
profile_photo: @profile_photo,
external_link: @external_link
external_link: @external_link,
discreet_information: @discreet_information
) do |header| %>
<% if name_slot.present? %>
<% header.with_name_slot do %>
Expand Down
3 changes: 2 additions & 1 deletion app/components/avo/panel_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,17 @@ class Avo::PanelComponent < Avo::BaseComponent
prop :body_classes
prop :data, default: {}.freeze
prop :display_breadcrumbs, default: false
prop :discreet_information
prop :index
prop :classes
prop :profile_photo
prop :cover_photo
prop :args, kind: :**, default: {}.freeze
prop :external_link

def after_initialize
@name = @args.dig(:name) || @args.dig(:title)
end
prop :external_link

def classes
class_names(@classes, "has-cover-photo": @cover_photo.present?, "has-profile-photo": @profile_photo.present?)
Expand Down
3 changes: 3 additions & 0 deletions app/components/avo/panel_header_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
<%= svg "heroicons/outline/arrow-top-right-on-square", class: "ml-2 text-2xl h-4" %>
<% end %>
<% end %>
<% if @discreet_information.present? %>
<%= render Avo::DiscreetInformationComponent.new(payload: @discreet_information) %>
<% end %>
<% end %>
<% end %>
<% end %>
Expand Down
1 change: 1 addition & 0 deletions app/components/avo/panel_header_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class Avo::PanelHeaderComponent < Avo::BaseComponent
prop :external_link
prop :description
prop :display_breadcrumbs, default: false
prop :discreet_information
prop :profile_photo

private
Expand Down
4 changes: 2 additions & 2 deletions app/components/avo/panel_name_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="text-2xl tracking-normal font-semibold text-gray-800 items-center flex flex-1" data-target="title">
<span class="block w-full text-center sm:text-left"><%= link_to_if @url.present?, @name, @url, target: @target, class: class_names("text-gray-800", @classes) %></span>
<div class="text-2xl tracking-normal font-semibold text-gray-800 items-center flex flex-1 text-center sm:text-left justify-center sm:justify-start" data-target="title">
<span class="block"><%= link_to_if @url.present?, @name, @url, target: @target, class: class_names("text-gray-800", @classes) %></span>
<%= body %>
</div>
4 changes: 4 additions & 0 deletions app/helpers/avo/turbo_stream_actions_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ def avo_close_modal
target: Avo::MODAL_FRAME_ID,
template: @view_context.turbo_frame_tag(Avo::MODAL_FRAME_ID)
end

def avo_turbo_reload
turbo_stream_action_tag :turbo_reload
end
end
end

Expand Down
3 changes: 2 additions & 1 deletion app/javascript/avo.base.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Mousetrap.bind('r r r', () => {
// Capture scroll position
scrollTop = document.scrollingElement.scrollTop

Turbo.visit(window.location.href, { action: 'replace' })
window.StreamActions.turbo_reload()
})

function isMac() {
Expand Down Expand Up @@ -56,6 +56,7 @@ document.addEventListener('keyup', (event) => {
function initTippy() {
tippy('[data-tippy="tooltip"]', {
theme: 'light',
allowHTML: true,
content(reference) {
const title = reference.getAttribute('title')
reference.removeAttribute('title')
Expand Down
5 changes: 5 additions & 0 deletions app/javascript/js/custom-stream-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ StreamActions.close_filters_dropdown = function () {
document.querySelector('.filters-dropdown-selector').classList.add('hidden')
}

// Uses Turbo to refresh the page
StreamActions.turbo_reload = function () {
window.Turbo.visit(window.location.href, { action: 'replace' })
}

StreamActions.open_filter = function () {
const id = this.getAttribute('unique-id')
setTimeout(() => {
Expand Down
2 changes: 1 addition & 1 deletion app/views/avo/actions/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<%= turbo_frame_tag Avo::MODAL_FRAME_ID do %>
<div
data-controller="<%= ["action", @action.get_stimulus_controllers].join(" ") %>"
data-action-no-confirmation-value="<%= @action.no_confirmation %>"
data-action-no-confirmation-value="<%= @action.no_confirmation? %>"
data-action-resource-name-value="<%= @resource.model_key %>"
data-resource-id="<%= params[:id] %>"
class="hidden text-slate-800"
Expand Down
20 changes: 17 additions & 3 deletions lib/avo/base_action.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ class BaseAction
include Avo::Concerns::HasActionStimulusControllers
include Avo::Concerns::Hydration

DATA_ATTRIBUTES = {turbo_frame: Avo::MODAL_FRAME_ID}

class_attribute :name, default: nil
class_attribute :message
class_attribute :confirm_button_label
Expand Down Expand Up @@ -59,8 +61,8 @@ def to_param
to_s
end

def link_arguments(resource:, arguments: {}, **args)
path = Avo::Services::URIService.parse(resource.record&.persisted? ? resource.record_path : resource.records_path)
def path(resource:, arguments: {}, **args)
Avo::Services::URIService.parse(resource.record&.persisted? ? resource.record_path : resource.records_path)
.append_paths("actions")
.append_query(
**{
Expand All @@ -70,8 +72,10 @@ def link_arguments(resource:, arguments: {}, **args)
}.compact
)
.to_s
end

[path, {turbo_frame: Avo::MODAL_FRAME_ID}]
def link_arguments(resource:, arguments: {}, **args)
[path(resource:, arguments:, **args), DATA_ATTRIBUTES]
end

# Encrypt the arguments so we can pass sensible data as a query param.
Expand Down Expand Up @@ -366,6 +370,16 @@ def disabled?
!enabled?
end

def no_confirmation?
Avo::ExecutionContext.new(
target: no_confirmation,
action: self,
resource: @resource,
view: @view,
arguments:
).handle
end

private

def add_message(body, type = :info)
Expand Down
15 changes: 15 additions & 0 deletions lib/avo/concerns/has_discreet_information.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module Avo
module Concerns
module HasDiscreetInformation
extend ActiveSupport::Concern

included do
class_attribute :discreet_information, instance_accessor: false
end

def discreet_information
::Avo::DiscreetInformation.new resource: self
end
end
end
end
62 changes: 62 additions & 0 deletions lib/avo/discreet_information.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
class Avo::DiscreetInformation
extend PropInitializer::Properties
include ActionView::Helpers::TagHelper

prop :resource, reader: :public

delegate :record, :view, to: :resource

def items
Array.wrap(resource.class.discreet_information).map do |item|
if item == :timestamps
timestamp_item(item)
else
parse_payload(item)
end
end
end

private

def timestamp_item(item)
return if record.created_at.blank? && record.updated_at.blank?

time_format = "%Y-%m-%d %H:%M:%S"
created_at = record.created_at.strftime(time_format)
updated_at = record.updated_at.strftime(time_format)

created_at_tag = if record.created_at.present?
I18n.t("avo.created_at_timestamp", created_at:)
end

updated_at_tag = if record.updated_at.present?
I18n.t("avo.updated_at_timestamp", updated_at:)
end

DiscreetInformationItem.new(
tooltip: tag.div([created_at_tag, updated_at_tag].compact.join(tag.br), style: "text-align: right;"),
icon: "heroicons/outline/clock"
)
end

def parse_payload(item)
return unless item.is_a?(Hash)

args = {
record:,
resource:,
view:
}

DiscreetInformationItem.new(
tooltip: Avo::ExecutionContext.new(target: item[:tooltip], **args).handle,
icon: Avo::ExecutionContext.new(target: item[:icon], **args).handle,
url: Avo::ExecutionContext.new(target: item[:url], **args).handle,
url_target: Avo::ExecutionContext.new(target: item[:url_target], **args).handle,
data: Avo::ExecutionContext.new(target: item[:data], **args).handle,
label: Avo::ExecutionContext.new(target: item[:label], **args).handle
)
end

DiscreetInformationItem = Struct.new(:tooltip, :icon, :url, :url_target, :data, :label, keyword_init: true) unless defined?(DiscreetInformationItem)
end
30 changes: 29 additions & 1 deletion lib/avo/execution_context.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
module Avo
# = Avo Execution Context
#
# The ExecutionContext class is used to evaluate blocks in isolation.
class ExecutionContext
include Avo::Concerns::HasHelpers

Expand Down Expand Up @@ -36,7 +39,32 @@ def initialize(**args)
delegate :result, to: :card
delegate :authorize, to: Avo::Services::AuthorizationService

# Return target if target is not callable, otherwise, execute target on this instance context
# Executes the target and returns the result.
# It takes in a target which usually is a block. If it's something else, it will return it.
#
# It automatically has access to the view context, current user, request, main app, avo, locale, and params.
# It also has a +delegate_missing_to+ which allows it to delegate missing methods to the view context for a more natural experience.
# You may pass extra arguments to the initialize method to have them available in the block that will be executed.
# You may pass extra modules to extend the class with.
#
# ==== Examples
#
# ===== Normal use
#
# Avo::ExecutionContext.new(target: -> { "Hello, world!" }).handle
# => "Hello, world!"
#
# ===== Providing a record
#
# Avo::ExecutionContext.new(target: -> { record.name }, record: @record).handle
# => "John Doe"
#
# ===== Providing a module
#
# This will include the SanitizeHelper module in the class and so have the +sanitize+ method available.
#
# Avo::ExecutionContext.new(target: -> { sanitize "<script>alert('be careful');</script>#{record.name}" } record: @record, include: [ActionView::Helpers::SanitizeHelper]).handle
# => "John Doe"
def handle
target.respond_to?(:call) ? instance_exec(&target) : target
end
Expand Down
1 change: 1 addition & 0 deletions lib/avo/resources/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class Base
include Avo::Concerns::HasHelpers
include Avo::Concerns::Hydration
include Avo::Concerns::Pagination
include Avo::Concerns::HasDiscreetInformation
include Avo::Concerns::RowControlsConfiguration

# Avo::Current methods
Expand Down
1 change: 1 addition & 0 deletions spec/dummy/app/avo/actions/toggle_published.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ class Avo::Actions::TogglePublished < Avo::BaseAction
self.message = "Are you sure, sure?"
self.confirm_button_label = "Toggle"
self.cancel_button_label = "Don't toggle yet"
self.no_confirmation = -> { arguments[:no_confirmation] || false }

def fields
field :notify_user, as: :boolean, default: true
Expand Down
1 change: 1 addition & 0 deletions spec/dummy/app/avo/resources/event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class Avo::Resources::Event < Avo::BaseResource
self.profile_photo = {
source: :profile_photo
}
self.discreet_information = :timestamps

self.row_controls_config = {
float: true,
Expand Down
22 changes: 22 additions & 0 deletions spec/dummy/app/avo/resources/post.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,28 @@ class Avo::Resources::Post < Avo::BaseResource
main_app.post_path(record)
}

self.discreet_information = [
:timestamps,
{
tooltip: -> { sanitize("Product is <strong>#{record.published_at ? "published" : "draft"}</strong>", tags: %w[strong]) },
icon: -> { "heroicons/outline/#{record.published_at ? "eye" : "eye-slash"}" }
},
{
label: -> { record.published_at ? "✅" : "🙄" },
tooltip: -> { "Post is #{record.published_at ? "published" : "draft"}. Click to toggle." },
url: -> {
Avo::Actions::TogglePublished.path(
resource: resource,
arguments: {
records: Array.wrap(record.id),
no_confirmation: true
}
)
},
data: Avo::BaseAction::DATA_ATTRIBUTES
}
]

def fields
field :id, as: :id
field :name, required: true, sortable: true
Expand Down
Loading

0 comments on commit 6a6d122

Please sign in to comment.