Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: copyable field option #3468

Merged
merged 28 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions app/components/avo/clipboard_component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<div class="flex flex-row justify-between opacity-0 group-hover:opacity-100 transition-opacity duration-200" data-controller="clipboard">
<div class="hidden" data-clipboard-target="source"><%= @value %></div>
<div class="text-xs xl:text-sm">
<button type="button" data-action="clipboard#copy" data-clipboard-target="icon">
<a href="javascript:void(0)">
<%= helpers.svg "heroicons/outline/clipboard", class: "h-4 inline" %>
</a>
</button>
<div class="hidden" data-clipboard-target="iconCopied">
<a href="javascript:void(0)">
<%= helpers.svg "heroicons/outline/clipboard-document-check", class: "h-4 inline" %>
</a>
</div>
</div>
</div>
9 changes: 9 additions & 0 deletions app/components/avo/clipboard_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

class Avo::ClipboardComponent < Avo::BaseComponent
prop :value

def render?
@value.present?
end
end
15 changes: 10 additions & 5 deletions app/components/avo/field_wrapper_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,16 @@
}), data: {slot: "value"} do %>
<div class="self-center w-full <% unless full_width? || compact? || stacked? %> md:w-8/12 has-sidebar:w-full <% end %>">
<% if on_show? %>
<% if render_dash? %>
<% else %>
<%= content %>
<% end %>
<div class="flex flex-row items-center justify-between space-x-1 group">
<% if render_dash? %>
<% else %>
<%= content %>
<% end %>
<% if @field.copyable %>
<%= render Avo::ClipboardComponent.new(value: @field.value) %>
<% end %>
</div>
<% elsif on_edit? %>
<%= content %>
<% if record.present? and record.errors.include? @field.id %>
Expand Down
17 changes: 11 additions & 6 deletions app/components/avo/index/field_wrapper_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@
<% if render_dash? %>
<% else %>
<% if @center_content %>
<div class="flex items-center justify-center">
<div class="flex flex-row items-center justify-between space-x-1 group">
<% if @center_content %>
<div class="flex items-center justify-center">
<%= content %>
</div>
<% else %>
<%= content %>
</div>
<% else %>
<%= content %>
<% end %>
<% end %>
<% if @field.copyable %>
<%= render Avo::ClipboardComponent.new(value: @field.value) %>
<% end %>
</div>
<% end %>
<% if params[:avo_debug].present? %>
<!-- Raw value: -->
Expand Down
6 changes: 4 additions & 2 deletions app/javascript/js/application.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { Alert, Popover } from 'tailwindcss-stimulus-components'
import { Application } from '@hotwired/stimulus'
import TextareaAutogrow from 'stimulus-textarea-autogrow'
import { Alert, Popover } from 'tailwindcss-stimulus-components'
import ClipboardController from './controllers/clipboard_controller'
import PasswordVisibility from '@stimulus-components/password-visibility'
import TextareaAutogrow from 'stimulus-textarea-autogrow'
import TurboPower from 'turbo_power'

TurboPower.initialize(window.Turbo.StreamActions)

const application = Application.start()
application.register('textarea-autogrow', TextareaAutogrow)
application.register('password-visibility', PasswordVisibility)
application.register('clipboard', ClipboardController)

// Configure Stimulus development experience
application.debug = window?.localStorage.getItem('avo.debug')
Expand Down
20 changes: 20 additions & 0 deletions app/javascript/js/controllers/clipboard_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Clipboard from '@stimulus-components/clipboard'

export default class extends Clipboard {
static targets = ['icon', 'iconCopied']

connect() {
super.connect()
}

copied() {
this.iconTarget.classList.add('hidden')
this.iconCopiedTarget.classList.remove('hidden')

// Reset the icon after a 2 seconds delay
setTimeout(() => {
this.iconTarget.classList.remove('hidden')
this.iconCopiedTarget.classList.add('hidden')
}, 2000)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be configured with data-clipboard-success-duration-value

From: https://www.stimulus-components.com/docs/stimulus-clipboard

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not that easy, It works with data-clipboard-success-content-value (this work only with plain text) , we want to show another object so we can not probably use data-clipboard-success-duration-value. I tried use that but it do not work.

}
}
2 changes: 2 additions & 0 deletions lib/avo/fields/base_field.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class BaseField
attr_reader :computable # if allowed to be computable
attr_reader :computed # if block is present
attr_reader :computed_value # the value after computation
attr_reader :copyable # if allowed to be copyable

# Hydrated payload
attr_accessor :record
Expand Down Expand Up @@ -87,6 +88,7 @@ def initialize(id, **args, &block)
@components = args[:components] || {}
@for_attribute = args[:for_attribute]
@meta = args[:meta]
@copyable = args[:copyable] || false

@args = args

Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@
"@algolia/autocomplete-theme-classic": "^1.0.0-alpha.46",
"@babel/plugin-proposal-class-properties": "^7.12.1",
"@hotwired/stimulus": "^3.2.2",
"@hotwired/turbo-rails": "^8.0.12",
"@rails/activestorage": "^6.1.710",
"@hotwired/turbo-rails": "^8.0.10",
"@rails/activestorage": "^6.1.7",
"@stimulus-components/clipboard": "^5.0.0",
"@stimulus-components/password-visibility": "^3.0.0",
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
Expand Down Expand Up @@ -75,6 +76,7 @@
"postcss-preset-env": "^8.5.1",
"regenerator-runtime": "^0.13.11",
"sortablejs": "^1.15.3",
"stimulus-clipboard": "^4.0.1",
"stimulus-rails-nested-form": "^4.1.0",
"stimulus-textarea-autogrow": "^4.1.0",
"stimulus-use": "^0.50.0",
Expand Down
2 changes: 1 addition & 1 deletion spec/dummy/app/avo/resources/city.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def base_fields
# This is because we want to be able to edit them using the tool.
# When submitting the form, we need this fields declared on the resource in order to know how to process them and fill the record.
def tool_fields
field :name, as: :text, hide_on: [:index, :forms]
field :name, as: :text, hide_on: [:index, :forms], copyable: true
with_options hide_on: :forms do
field :name, as: :text, filterable: true, name: "name (click to edit)", only_on: :index do
path, data = Avo::Actions::City::Update.link_arguments(
Expand Down
2 changes: 1 addition & 1 deletion spec/dummy/app/avo/resources/comment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class Avo::Resources::Comment < Avo::BaseResource

def fields
field :id, as: :id
field :body, as: :textarea
field :body, as: :textarea, copyable: true
field :tiny_name, as: :text, only_on: :index
field :posted_at,
as: :date_time,
Expand Down
4 changes: 2 additions & 2 deletions spec/dummy/app/avo/resources/project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def fields
nullable: true,
filterable: true,
summarizable: true
field :name, as: :text, required: true, sortable: true, default: "New project default name"
field :name, as: :text, required: true, sortable: true, default: "New project default name", copyable: true
field :progress,
as: :progress_bar,
value_suffix: "%",
Expand Down Expand Up @@ -51,7 +51,7 @@ def fields
sortable: true,
summarizable: true
field :country,
as: :country,
as: :country, copyable: true,
include_blank: "No country",
filterable: true,
summarizable: true
Expand Down
2 changes: 1 addition & 1 deletion spec/dummy/app/avo/resources/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def main_panel_fields
field :first_name, placeholder: "John"
field :last_name, placeholder: "Doe", filterable: true
end
field :email, as: :text, name: "User Email", required: true, protocol: :mailto
field :email, as: :text, name: "User Email", required: true, protocol: :mailto, copyable: true
field :active, as: :boolean, name: "Is active", only_on: :index
field :cv, as: :file, name: "CV"
field :is_admin?, as: :boolean, name: "Is admin", only_on: :index
Expand Down
2 changes: 1 addition & 1 deletion spec/dummy/db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "price_cents", default: 0, null: false
t.string "price_currency", default: "'USD'::character varying", null: false
t.string "price_currency", default: "USD", null: false
end

create_table "projects", force: :cascade do |t|
Expand Down
49 changes: 49 additions & 0 deletions spec/system/avo/copy_field_content_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
require "rails_helper"

RSpec.describe "CopyFieldContent", type: :system do
let!(:user) { User.first }

def test_copy_to_clipboard(path)
visit path

element = find('div[data-controller="clipboard"]', visible: :all)
expect(element).to be_present
element.hover

copy_button = element.find('button[data-action="clipboard#copy"]', visible: :visible)
copy_button.click

expect(element).to have_css('div[data-clipboard-target="iconCopied"]', visible: :all, wait: 1)
end

def test_button_visability(path)
visit path

element = find('div[data-controller="clipboard"]', visible: :all)
expect(element).to be_present
end

describe "index view" do
let(:path) { "/admin/resources/users" }

it "shows copy to clipboard icon" do
test_button_visability(path)
end

it "copies to clipboard after clicking button" do
test_copy_to_clipboard(path)
end
end

describe "show view" do
let(:path) { "/admin/resources/users/#{user.id}" }

it "shows copy to clipboard icon" do
test_button_visability(path)
end

it "copies to clipboard after clicking button" do
test_copy_to_clipboard(path)
end
end
end
14 changes: 12 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1360,7 +1360,7 @@
resolved "https://registry.yarnpkg.com/@hotwired/stimulus/-/stimulus-3.2.2.tgz#071aab59c600fed95b97939e605ff261a4251608"
integrity sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A==

"@hotwired/turbo-rails@^8.0.12":
"@hotwired/turbo-rails@^8.0.10":
version "8.0.12"
resolved "https://registry.yarnpkg.com/@hotwired/turbo-rails/-/turbo-rails-8.0.12.tgz#6f1a2661122c0a2bf717f3bc68b5106638798c89"
integrity sha512-ZXwu9ez+Gd4RQNeHIitqOQgi/LyqY8J4JqsUN0nnYiZDBRq7IreeFdMbz29VdJpIsmYqwooE4cFzPU7QvJkQkA==
Expand Down Expand Up @@ -1491,7 +1491,7 @@
resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-7.0.4.tgz#70a3ca56809f7aaabb80af2f9c01ae51e1a8ed41"
integrity sha512-tz4oM+Zn9CYsvtyicsa/AwzKZKL+ITHWkhiu7x+xF77clh2b4Rm+s6xnOgY/sGDWoFWZmtKsE95hxBPkgQQNnQ==

"@rails/activestorage@^6.1.710":
"@rails/activestorage@^6.1.7":
version "6.1.710"
resolved "https://registry.yarnpkg.com/@rails/activestorage/-/activestorage-6.1.710.tgz#bc914716f642ba233f033b7765a44dc3bfb626f5"
integrity sha512-hLXxAtn7hSWXkTzMGOmQmjuzJ0FzVw8j/Zi8DGS+DG9x4uPQjl+ZEVdty7pFcsBCjkpejtk0TChcBQlLW8sgOg==
Expand All @@ -1508,6 +1508,11 @@
resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8"
integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==

"@stimulus-components/clipboard@^5.0.0":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@stimulus-components/clipboard/-/clipboard-5.0.0.tgz#1d119c4ba8827c6c11c2e1b9214d0de79bc0cc87"
integrity sha512-gbwU1sVBiKfMGGCt6oyXx9mGD+cEcHBSqaz//5UVepoQqzl2jYEUWQGFIO0f48LMOamEQwR1azQKfHq6llv6oA==

"@stimulus-components/password-visibility@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@stimulus-components/password-visibility/-/password-visibility-3.0.0.tgz#314c1bae571ba13b9a4da6014f3e5c5b776fe4cc"
Expand Down Expand Up @@ -4953,6 +4958,11 @@ spark-md5@^3.0.0:
resolved "https://registry.yarnpkg.com/spark-md5/-/spark-md5-3.0.1.tgz#83a0e255734f2ab4e5c466e5a2cfc9ba2aa2124d"
integrity sha512-0tF3AGSD1ppQeuffsLDIOWlKUd3lS92tFxcsrh5Pe3ZphhnoK+oXIBTzOAThZCiuINZLvpiLH/1VS1/ANEJVig==

stimulus-clipboard@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/stimulus-clipboard/-/stimulus-clipboard-4.0.1.tgz#acc9212b479fedc633ecdec8f191a28c1d826a6b"
integrity sha512-dem+ihC3Q8+gbyGINdd+dK+9d5vUTnOwoH+n3KcDJvbxrFcq9lV8mWjyhEaDquGxYy3MmqSdz9FHQbG88TBqGg==

stimulus-rails-nested-form@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/stimulus-rails-nested-form/-/stimulus-rails-nested-form-4.1.0.tgz#bfce185cff908170a4eb9973875b72517c3bc83a"
Expand Down
Loading