From 4b107c1eb7a951fe4e990054284731a1d2739a97 Mon Sep 17 00:00:00 2001 From: aidewoode Date: Mon, 22 Jul 2024 21:33:28 +0800 Subject: [PATCH 1/3] Add basic dropdown component --- Gemfile | 3 ++ Gemfile.lock | 6 ++++ .../stylesheets/components/_dropdown.scss | 20 ++--------- app/components/application_component.rb | 5 +++ app/components/base_component.rb | 15 ++++++++ app/components/dropdown/item_component.rb | 22 ++++++++++++ app/components/dropdown_component.html.erb | 8 +++++ app/components/dropdown_component.rb | 15 ++++++++ app/helpers/component_helper.rb | 7 ++++ app/views/shared/_search_bar.html.erb | 19 +++++----- app/views/shared/_sort_select.html.erb | 35 +++++++++---------- .../dropdown/item_component_test.rb | 12 +++++++ test/components/dropdown_component_test.rb | 12 +++++++ 13 files changed, 134 insertions(+), 45 deletions(-) create mode 100644 app/components/application_component.rb create mode 100644 app/components/base_component.rb create mode 100644 app/components/dropdown/item_component.rb create mode 100644 app/components/dropdown_component.html.erb create mode 100644 app/components/dropdown_component.rb create mode 100644 app/helpers/component_helper.rb create mode 100644 test/components/dropdown/item_component_test.rb create mode 100644 test/components/dropdown_component_test.rb diff --git a/Gemfile b/Gemfile index 296896f8..a1addedc 100644 --- a/Gemfile +++ b/Gemfile @@ -13,6 +13,9 @@ gem "turbo-rails", "~> 1.5.0" # Install Stimulus on Rails gem "stimulus-rails", "~> 1.2.1" +# View components for Rails +gem "view_component", "~> 3.12.0" + # Bundle and process CSS in Rails gem "cssbundling-rails", "~> 1.4.0" diff --git a/Gemfile.lock b/Gemfile.lock index b638047f..8359bcec 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -209,6 +209,7 @@ GEM marcel (1.0.4) matrix (0.4.2) memory_profiler (0.9.14) + method_source (1.1.0) mini_magick (4.12.0) mini_mime (1.1.5) mini_portile2 (2.8.6) @@ -374,6 +375,10 @@ GEM uniform_notifier (1.16.0) url_mount (0.2.1) rack + view_component (3.12.1) + activesupport (>= 5.2.0, < 8.0) + concurrent-ruby (~> 1.0) + method_source (~> 1.0) wahwah (1.6.2) web-console (4.2.1) actionview (>= 6.0.0) @@ -433,6 +438,7 @@ DEPENDENCIES stimulus-rails (~> 1.2.1) turbo-rails (~> 1.5.0) tzinfo-data + view_component (~> 3.12.0) wahwah (~> 1.6.0) web-console (>= 3.3.0) webmock (~> 3.18.0) diff --git a/app/assets/stylesheets/components/_dropdown.scss b/app/assets/stylesheets/components/_dropdown.scss index cfbd85c0..562955a4 100644 --- a/app/assets/stylesheets/components/_dropdown.scss +++ b/app/assets/stylesheets/components/_dropdown.scss @@ -65,31 +65,15 @@ a.c-dropdown__item { color: var(--dropdown-color); } -form.c-dropdown__item input[type="submit"], -form.c-dropdown__item button[type="submit"] { +input.c-dropdown__item, +button.c-dropdown__item { width: 100%; background: var(--dropdown-bg-color); border: none; cursor: pointer; } -.c-dropdown__item:first-child, -form.c-dropdown__item:first-child input[type="submit"], -form.c-dropdown__item:first-child button[type="submit"] { - border-top-left-radius: border-radius("medium"); - border-top-right-radius: border-radius("medium"); -} - -.c-dropdown__item:last-child, -form.c-dropdown__item:last-child input[type="submit"], -form.c-dropdown__item:last-child button[type="submit"] { - border-bottom-left-radius: border-radius("medium"); - border-bottom-right-radius: border-radius("medium"); -} - a.c-dropdown__item:hover, -form.c-dropdown__item:hover input[type="submit"], -form.c-dropdown__item:hover button[type="submit"], .c-dropdown__item:hover { background: var(--dropdown-active-color); color: var(--dropdown-color); diff --git a/app/components/application_component.rb b/app/components/application_component.rb new file mode 100644 index 00000000..25d42055 --- /dev/null +++ b/app/components/application_component.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class ApplicationComponent < ViewComponent::Base + include ComponentHelper +end diff --git a/app/components/base_component.rb b/app/components/base_component.rb new file mode 100644 index 00000000..3152f057 --- /dev/null +++ b/app/components/base_component.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class BaseComponent < ApplicationComponent + def initialize(tag = :div, **attributes) + @tag = tag + @tag_args = attributes + + test_id = @tag_args.delete(:test_id) + @tag_args[:data] = merge_attributes(@tag_args[:data], {test_id: test_id}) if test_id + end + + def call + content? ? content_tag(@tag, content, @tag_args) : tag(@tag, @tag_args) + end +end diff --git a/app/components/dropdown/item_component.rb b/app/components/dropdown/item_component.rb new file mode 100644 index 00000000..d42c0f58 --- /dev/null +++ b/app/components/dropdown/item_component.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Dropdown + class ItemComponent < ApplicationComponent + def initialize(type, *attributes) + @type = type + @attributes = attributes + end + + def call + html_options_index = content? ? 1 : 2 + @attributes[html_options_index] = merge_attributes(@attributes[html_options_index], {class: "c-dropdown__item"}) + + case @type + when :link + content? ? link_to(*@attributes) { content } : link_to(*@attributes) + when :button + content? ? button_to(*@attributes) { content } : button_to(*@attributes) + end + end + end +end diff --git a/app/components/dropdown_component.html.erb b/app/components/dropdown_component.html.erb new file mode 100644 index 00000000..fb351973 --- /dev/null +++ b/app/components/dropdown_component.html.erb @@ -0,0 +1,8 @@ +<%= render(BaseComponent.new(:details, **@attributes)) do %> + <%= toggle %> +
+ <% menu.each do |item| %> + <%= item %> + <% end %> +
+<% end %> diff --git a/app/components/dropdown_component.rb b/app/components/dropdown_component.rb new file mode 100644 index 00000000..9d8d332d --- /dev/null +++ b/app/components/dropdown_component.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class DropdownComponent < ApplicationComponent + renders_one :toggle + renders_many :menu, types: { + item: Dropdown::ItemComponent, + divider: -> { tag :hr } + } + + def initialize(**attributes) + @attributes = attributes + @attributes[:class] = class_names(@attributes[:class], "c-dropdown") + @attributes[:data] = merge_attributes(@attributes[:data], {controller: "dropdown"}) + end +end diff --git a/app/helpers/component_helper.rb b/app/helpers/component_helper.rb new file mode 100644 index 00000000..8400e290 --- /dev/null +++ b/app/helpers/component_helper.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module ComponentHelper + def merge_attributes(*hashes) + hashes.compact.reduce({}, :merge) + end +end diff --git a/app/views/shared/_search_bar.html.erb b/app/views/shared/_search_bar.html.erb index c73419da..2e8efc3c 100644 --- a/app/views/shared/_search_bar.html.erb +++ b/app/views/shared/_search_bar.html.erb @@ -14,14 +14,15 @@ <% end %>
-
- <%= avatar_tag current_user %> -
- <%= link_to t("label.settings"), setting_path, class: "c-dropdown__item" %> - <%= link_to t("label.manage_users"), users_path, class: "c-dropdown__item" if is_admin? %> - <%= link_to t("label.update_profile"), edit_user_path(current_user), class: "c-dropdown__item" %> - <%= button_to t("button.logout"), current_session, method: :delete, form_class: "c-dropdown__item" %> -
-
+ <%= render DropdownComponent.new(class: "u-ml-auto", test_id: "search_bar_menu") do |component| %> + <% component.with_toggle do %> + <%= avatar_tag current_user %> + <% end %> + + <%= component.with_menu_item(:link, t("label.settings"), setting_path) %> + <%= component.with_menu_item(:link, t("label.manage_users"), users_path) if is_admin? %> + <%= component.with_menu_item(:link, t("label.update_profile"), edit_user_path(current_user)) %> + <%= component.with_menu_item(:button, t("button.logout"), current_session, method: :delete) %> + <% end %>
diff --git a/app/views/shared/_sort_select.html.erb b/app/views/shared/_sort_select.html.erb index 5b7cf5fb..7a63200b 100644 --- a/app/views/shared/_sort_select.html.erb +++ b/app/views/shared/_sort_select.html.erb @@ -1,30 +1,29 @@ -
- - <%= icon_tag "sort", title: t("label.sort") %> - -
- <% option.values.each do |sort_value| %> - <%= link_to url_for(controller: sort_controller, action: :index, **filter_sort_presenter.params(sort: sort_value)), class: "c-dropdown__item", data: {"turbo-action" => ("replace" if native_app?)} do %> - +<%= render DropdownComponent.new do |component| %> + <% component.with_toggle do %> + <%= icon_tag "sort", title: t("label.sort"), class: "c-button c-button--icon" %> + <% end %> + + <% option.values.each do |sort_value| %> + <% component.with_menu_item(:link, url_for(controller: sort_controller, action: :index, **filter_sort_presenter.params(sort: sort_value)), data: {"turbo-action" => ("replace" if native_app?)}) do %> + <%= t("field.#{sort_value}") %> - <% if filter_sort_presenter.sort_value == sort_value || (filter_sort_presenter.sort_value.blank? && sort_value == option.default.name) %> + <% if filter_sort_presenter.sort_value == sort_value || (filter_sort_presenter.sort_value.blank? && sort_value == option.default.name) %> <%= icon_tag("check", size: "small", class: "u-ml-narrow") %> <% end %> - <% end %> <% end %> + <% end %> -
+ <%= component.with_menu_divider %> - <% %w[asc desc].each do |sort_direction| %> - <%= link_to url_for(controller: sort_controller, action: :index, **filter_sort_presenter.params(sort: filter_sort_presenter.sort_value || option.default.name, sort_direction: sort_direction)), class: "c-dropdown__item", data: {"turbo-action" => ("replace" if native_app?)} do %> - + <% %w[asc desc].each do |sort_direction| %> + <% component.with_menu_item(:link, url_for(controller: sort_controller, action: :index, **filter_sort_presenter.params(sort: filter_sort_presenter.sort_value || option.default.name, sort_direction: sort_direction)), data: {"turbo-action" => ("replace" if native_app?)}) do %> + <%= t("label.#{sort_direction}") %> - <% if filter_sort_presenter.sort_direction_value == sort_direction || (filter_sort_presenter.sort_direction_value.blank? && option.default.direction == sort_direction) %> + <% if filter_sort_presenter.sort_direction_value == sort_direction || (filter_sort_presenter.sort_direction_value.blank? && option.default.direction == sort_direction) %> <%= icon_tag("check", size: "small", class: "u-ml-narrow") %> <% end %> - <% end %> <% end %> -
-
+ <% end %> +<% end %> diff --git a/test/components/dropdown/item_component_test.rb b/test/components/dropdown/item_component_test.rb new file mode 100644 index 00000000..a6ee4c77 --- /dev/null +++ b/test/components/dropdown/item_component_test.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require "test_helper" + +class Dropdown::ItemComponentTest < ViewComponent::TestCase + def test_component_renders_something_useful + # assert_equal( + # %(Hello, components!), + # render_inline(Dropdown::ItemComponent.new(message: "Hello, components!")).css("span").to_html + # ) + end +end diff --git a/test/components/dropdown_component_test.rb b/test/components/dropdown_component_test.rb new file mode 100644 index 00000000..46440d8a --- /dev/null +++ b/test/components/dropdown_component_test.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require "test_helper" + +class DropdownComponentTest < ViewComponent::TestCase + def test_component_renders_something_useful + # assert_equal( + # %(Hello, components!), + # render_inline(DropdownComponent.new(message: "Hello, components!")).css("span").to_html + # ) + end +end From 8a1a25503c8e392ec30476eddddcdf659b842496 Mon Sep 17 00:00:00 2001 From: aidewoode Date: Mon, 29 Jul 2024 21:48:56 +0800 Subject: [PATCH 2/3] Replace all dropdown with dropdown component --- .../stylesheets/components/_dropdown.scss | 1 - app/components/dropdown_component.html.erb | 12 +- app/components/dropdown_component.rb | 4 +- app/views/albums/_filters.html.erb | 54 ++++----- app/views/albums/show.html.erb | 103 ++++++++-------- .../current_playlist/songs/_song.html.erb | 16 +-- .../current_playlist/songs/index.html.erb | 14 ++- app/views/playlists/songs/_song.html.erb | 113 +++++++++--------- app/views/playlists/songs/index.html.erb | 21 ++-- app/views/shared/_filter_options.html.erb | 26 ++-- app/views/shared/_sort_select.html.erb | 4 +- app/views/songs/_filters.html.erb | 46 +++---- app/views/songs/_song.html.erb | 73 +++++------ 13 files changed, 245 insertions(+), 242 deletions(-) diff --git a/app/assets/stylesheets/components/_dropdown.scss b/app/assets/stylesheets/components/_dropdown.scss index 562955a4..596c0d12 100644 --- a/app/assets/stylesheets/components/_dropdown.scss +++ b/app/assets/stylesheets/components/_dropdown.scss @@ -67,7 +67,6 @@ a.c-dropdown__item { input.c-dropdown__item, button.c-dropdown__item { - width: 100%; background: var(--dropdown-bg-color); border: none; cursor: pointer; diff --git a/app/components/dropdown_component.html.erb b/app/components/dropdown_component.html.erb index fb351973..45804548 100644 --- a/app/components/dropdown_component.html.erb +++ b/app/components/dropdown_component.html.erb @@ -1,8 +1,16 @@ <%= render(BaseComponent.new(:details, **@attributes)) do %> <%= toggle %>
- <% menu.each do |item| %> - <%= item %> + <% if @src.present? && @frame_id.present? %> + <%= tag.turbo_frame(id: @frame_id, src: @src, loading: "lazy") do %> +
+ <%= helpers.loader_tag size: "small" %> +
+ <% end %> + <% else %> + <% menu.each do |item| %> + <%= item %> + <% end %> <% end %>
<% end %> diff --git a/app/components/dropdown_component.rb b/app/components/dropdown_component.rb index 9d8d332d..244b1ea5 100644 --- a/app/components/dropdown_component.rb +++ b/app/components/dropdown_component.rb @@ -7,7 +7,9 @@ class DropdownComponent < ApplicationComponent divider: -> { tag :hr } } - def initialize(**attributes) + def initialize(src: nil, frame_id: nil, **attributes) + @src = src + @frame_id = frame_id @attributes = attributes @attributes[:class] = class_names(@attributes[:class], "c-dropdown") @attributes[:data] = merge_attributes(@attributes[:data], {controller: "dropdown"}) diff --git a/app/views/albums/_filters.html.erb b/app/views/albums/_filters.html.erb index ba901f0e..62b80dd9 100644 --- a/app/views/albums/_filters.html.erb +++ b/app/views/albums/_filters.html.erb @@ -7,34 +7,32 @@ <% end %> <% end %> -
- - - <%= t("field.genre") %> - <%= icon_tag "expand-more" %> +<%= render DropdownComponent.new( + src: albums_filter_genres_path(**filter_sort_presenter.params), + frame_id: "turbo-genre-filter", + class: "u-mr-small" +) do |component| %> + <% component.with_toggle do %> + + + <%= t("field.genre") %> + <%= icon_tag "expand-more" %> + - -
- <%= turbo_frame_tag "turbo-genre-filter", src: albums_filter_genres_path(**filter_sort_presenter.params), loading: "lazy" do %> -
- <%= loader_tag size: "small" %> -
- <% end %> -
-
+ <% end %> +<% end %> -
- - - <%= t("field.year") %> - <%= icon_tag "expand-more" %> +<%= render DropdownComponent.new( + src: albums_filter_years_path(**filter_sort_presenter.params), + frame_id: "turbo-year-filter", + class: "u-mr-small" +) do |component| %> + <% component.with_toggle do %> + + + <%= t("field.year") %> + <%= icon_tag "expand-more" %> + - -
- <%= turbo_frame_tag "turbo-year-filter", src: albums_filter_years_path(**filter_sort_presenter.params), loading: "lazy" do %> -
- <%= loader_tag size: "small" %> -
- <% end %> -
-
+ <% end %> +<% end %> diff --git a/app/views/albums/show.html.erb b/app/views/albums/show.html.erb index c782ee1c..293a17fd 100644 --- a/app/views/albums/show.html.erb +++ b/app/views/albums/show.html.erb @@ -69,60 +69,55 @@ <% end %> -
- - <%= icon_tag "more-vertical", size: "small", title: t("label.more") %> - -
- <%= button_to( - t("button.play_now"), - current_playlist_songs_path(song_id: song.id, should_play: true), - form_class: "c-dropdown__item", - form: { - data: { - "turbo-frame" => "turbo-playlist", - "delegated-action" => "turbo:submit-start->songs#checkBeforePlay click->songs-bridge#playNow", - "disabled-on-native" => "true" - } - } - ) %> - <%= button_to( - t("button.play_next"), - current_playlist_songs_path(song_id: song.id), - form_class: "c-dropdown__item", - form: { - data: { - "turbo-frame" => "turbo-playlist", - "delegated-action" => "turbo:submit-start->songs#checkBeforePlayNext click->songs-bridge#playNext", - "disabled-on-native" => "true" - } - } - ) %> - <%= button_to( - t("button.play_last"), - current_playlist_songs_path(song_id: song.id, location: "last"), - form_class: "c-dropdown__item", - form: { - data: { - "turbo-frame" => "turbo-playlist", - "delegated-action" => "click->songs-bridge#playLast", - "disabled-on-native" => "true" - } - } - ) %> - <%= link_to( - t("label.add_to_playlist"), - dialog_playlists_path(song_id: song.id, referer_url: current_url), - data: {"turbo-frame" => ("turbo-dialog" unless native_app?)}, - class: "c-dropdown__item" - ) %> - <%= link_to( - t("label.go_to_artist"), - artist_path(song.artist), - class: "c-dropdown__item" - ) %> -
-
+ <%= render DropdownComponent.new(test_id: "album_song_menu") do |component| %> + <% component.with_toggle do %> + + <%= icon_tag "more-vertical", size: "small", title: t("label.more") %> + + <% end %> + + <%= component.with_menu_item(:button, + t("button.play_now"), + current_playlist_songs_path(song_id: song.id, should_play: true), + form: { + data: { + "turbo-frame" => "turbo-playlist", + "delegated-action" => "turbo:submit-start->songs#checkBeforePlay click->songs-bridge#playNow", + "disabled-on-native" => "true" + } + }) %> + + <%= component.with_menu_item(:button, + t("button.play_next"), + current_playlist_songs_path(song_id: song.id), + form: { + data: { + "turbo-frame" => "turbo-playlist", + "delegated-action" => "turbo:submit-start->songs#checkBeforePlayNext click->songs-bridge#playNext", + "disabled-on-native" => "true" + } + }) %> + + <%= component.with_menu_item(:button, + t("button.play_last"), + current_playlist_songs_path(song_id: song.id, location: "last"), + form: { + data: { + "turbo-frame" => "turbo-playlist", + "delegated-action" => "click->songs-bridge#playLast", + "disabled-on-native" => "true" + } + }) %> + + <%= component.with_menu_item(:link, + t("label.add_to_playlist"), + dialog_playlists_path(song_id: song.id, referer_url: current_url), + data: {"turbo-frame" => ("turbo-dialog" unless native_app?)}) %> + + <%= component.with_menu_item(:link, + t("label.go_to_artist"), + artist_path(song.artist)) %> + <% end %> <% end %> diff --git a/app/views/current_playlist/songs/_song.html.erb b/app/views/current_playlist/songs/_song.html.erb index 38e09afb..f9b92204 100644 --- a/app/views/current_playlist/songs/_song.html.erb +++ b/app/views/current_playlist/songs/_song.html.erb @@ -22,13 +22,15 @@ -
- <%= icon_tag "more-vertical", size: "small", title: t("label.more") %> -
- <%= link_to t("label.add_to_playlist"), dialog_playlists_path(song_id: song.id, referer_url: current_url), data: {"turbo-frame" => ("turbo-dialog" unless native_app?)}, class: "c-dropdown__item" %> + <%= render DropdownComponent.new(test_id: "current_playlist_song_menu") do |component| %> + <% component.with_toggle do %> + + <%= icon_tag "more-vertical", size: "small", title: t("label.more") %> + + <% end %> - <%= button_to t("button.delete"), current_playlist_song_path(song), method: :delete, form_class: "c-dropdown__item", form: {data: {"turbo-frame" => "turbo-playlist"}} %> -
-
+ <%= component.with_menu_item(:link, t("label.add_to_playlist"), dialog_playlists_path(song_id: song.id, referer_url: current_url), data: {"turbo-frame" => ("turbo-dialog" unless native_app?)}) %> + <%= component.with_menu_item(:button, t("button.delete"), current_playlist_song_path(song), method: :delete, form: {data: {"turbo-frame" => "turbo-playlist"}}) %> + <% end %> diff --git a/app/views/current_playlist/songs/index.html.erb b/app/views/current_playlist/songs/index.html.erb index 9d62fb99..52207f51 100644 --- a/app/views/current_playlist/songs/index.html.erb +++ b/app/views/current_playlist/songs/index.html.erb @@ -10,12 +10,14 @@ <%= @playlist.songs.count %> <%= t("label.tracks") %>

-
- <%= icon_tag "more-vertical", title: t("label.more") %> -
- <%= button_to t("button.clear"), current_playlist_songs_path, method: :delete, form_class: "c-dropdown__item" %> -
-
+ <%= render DropdownComponent.new(test_id: "playlist_menu") do |component| %> + <% component.with_toggle do %> + + <%= icon_tag "more-vertical", title: t("label.more") %> + + <% end %> + <%= component.with_menu_item(:button, t("button.clear"), current_playlist_songs_path, method: :delete) %> + <% end %>