From 2b5da72cf88cf148706f167ce657e92d1323dbf1 Mon Sep 17 00:00:00 2001 From: yocchan-git Date: Thu, 22 Feb 2024 11:45:46 +0900 Subject: [PATCH] =?UTF-8?q?=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E4=B8=80?= =?UTF-8?q?=E8=A6=A7=E3=82=92=E9=9D=9Evue=E5=8C=96=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/api/followings_controller.rb | 6 +- app/controllers/api/users_controller.rb | 5 + app/controllers/generations_controller.rb | 4 + app/controllers/users_controller.rb | 35 ++- app/helpers/users_helper.rb | 13 ++ app/javascript/components/users.vue | 212 ------------------ app/javascript/debounce.js | 13 -- app/javascript/following.js | 76 +++++++ app/javascript/generation.vue | 86 ------- app/javascript/generations.js | 18 -- app/javascript/generations.vue | 101 --------- .../loading-users-list-placeholder.vue | 52 ----- app/javascript/packs/application.js | 4 +- app/javascript/search-user.js | 104 +++++++++ app/views/generations/index.html.slim | 33 ++- app/views/users/_activity_counts.html.slim | 65 +++--- app/views/users/_following.html.slim | 61 +++++ app/views/users/_no_match_user.html.slim | 8 + app/views/users/_user.html.slim | 73 ++++++ app/views/users/_user_list.html.slim | 9 + app/views/users/index.html.slim | 15 +- .../practices/_completed_practices.html.slim | 2 +- .../_completed_practices_progress.html.slim | 2 +- test/system/users_test.rb | 16 -- 24 files changed, 463 insertions(+), 550 deletions(-) delete mode 100644 app/javascript/components/users.vue delete mode 100644 app/javascript/debounce.js delete mode 100644 app/javascript/generation.vue delete mode 100644 app/javascript/generations.js delete mode 100644 app/javascript/generations.vue delete mode 100644 app/javascript/loading-users-list-placeholder.vue create mode 100644 app/javascript/search-user.js create mode 100644 app/views/users/_following.html.slim create mode 100644 app/views/users/_no_match_user.html.slim create mode 100644 app/views/users/_user.html.slim create mode 100644 app/views/users/_user_list.html.slim diff --git a/app/controllers/api/followings_controller.rb b/app/controllers/api/followings_controller.rb index 05ba55daa81..df2deba1b48 100644 --- a/app/controllers/api/followings_controller.rb +++ b/app/controllers/api/followings_controller.rb @@ -7,7 +7,7 @@ def create user = User.find(params[:id]) watch = params[:watch] == 'true' if current_user.follow(user, watch:) - render json: { id: user.id } + render json: { html: render_to_string(partial: 'users/following', locals: { user: }) } else head :bad_request end @@ -17,7 +17,7 @@ def update user = User.find(params[:id]) watch = params[:watch] == 'true' if current_user.change_watching(user, watch) - head :no_content + render json: { html: render_to_string(partial: 'users/following', locals: { user: }) } else head :bad_request end @@ -26,7 +26,7 @@ def update def destroy user = User.find(params[:id]) if current_user.unfollow(user) - head :no_content + render json: { html: render_to_string(partial: 'users/following', locals: { user: }) } else head :bad_request end diff --git a/app/controllers/api/users_controller.rb b/app/controllers/api/users_controller.rb index 6dea361fe24..825707a8a76 100644 --- a/app/controllers/api/users_controller.rb +++ b/app/controllers/api/users_controller.rb @@ -24,6 +24,11 @@ def index .page(params[:page]) .per(PAGER_NUMBER) end + + return unless params[:require_html] + + @users = @users.page(params[:page]).per(PAGER_NUMBER) if params[:search_word] + render json: { html: render_to_string(partial: 'users/user_list', locals: { users: @users, is_search: true }, formats: :html) } end def show; end diff --git a/app/controllers/generations_controller.rb b/app/controllers/generations_controller.rb index 50f37b85a74..f6a37398d66 100644 --- a/app/controllers/generations_controller.rb +++ b/app/controllers/generations_controller.rb @@ -2,6 +2,7 @@ class GenerationsController < ApplicationController ALLOWED_TARGETS = %w[all trainee adviser graduate mentor retired].freeze + PAGER_NUMBER = 24 def show @generation = params[:id].to_i @@ -10,5 +11,8 @@ def show def index @target = ALLOWED_TARGETS.include?(params[:target]) ? params[:target] : ALLOWED_TARGETS.first redirect_to root_path, alert: '管理者としてログインしてください' if @target == 'retired' && !current_user.admin? + + result = Generation.generations(@target).reverse + @generations = Kaminari.paginate_array(result).page(params[:page]).per(PAGER_NUMBER) end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index a43953d8838..5dd76788c33 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -5,22 +5,15 @@ class UsersController < ApplicationController before_action :require_token, only: %i[new] if Rails.env.production? before_action :set_user, only: %w[show] - PAGER_NUMBER = 20 + PAGER_NUMBER = 24 def index @target = params[:target] @target = 'student_and_trainee' unless target_allowlist.include?(@target) + @entered_tag = params[:tag] @watch = params[:watch] - target_users = - if @target == 'followings' - current_user.followees_list(watch: @watch) - elsif params[:tag] - User.tagged_with(params[:tag]) - else - users = User.users_role(@target, allowed_targets: target_allowlist) - @target == 'inactive' ? users.order(:last_activity_at) : users - end + target_users = fetch_target_users @users = target_users .page(params[:page]).per(PAGER_NUMBER) @@ -29,6 +22,10 @@ def index @users = @users.unhibernated.unretired unless @target.in? %w[hibernated retired] + if params[:search_word] + @users = search_for_users(@target, users, params[:search_word]).page(params[:page]).per(PAGER_NUMBER) + @search_word = params[:search_word] + end @random_tags = User.tags.sample(20) @top3_tags_counts = User.tags.limit(3).map(&:count).uniq @tag = ActsAsTaggableOn::Tag.find_by(name: params[:tag]) @@ -81,6 +78,24 @@ def create private + def search_for_users(target, target_users, search_word) + users = target_users.search_by_keywords({ word: search_word }) + # search_by_keywords内では { unretired } というスコープが設定されている + # 退会したユーザーに対しキーワード検索を行う場合は、一旦 unscope(where: :retired_on) で { unretired } スコープを削除し、その後で retired スコープを設定する必要がある + target == 'retired' ? users.unscope(where: :retired_on).retired : users + end + + def fetch_target_users + if @target == 'followings' + current_user.followees_list(watch: @watch) + elsif @entered_tag + User.tagged_with(@entered_tag) + else + users = User.users_role(@target, allowed_targets: target_allowlist) + @target == 'inactive' ? users.order(:last_activity_at) : users + end + end + def target_allowlist target_allowlist = %w[student_and_trainee followings mentor graduate adviser trainee year_end_party] target_allowlist.push('job_seeking') if current_user.adviser? diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 7da9fa5f830..84fdd3cb52e 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -55,6 +55,19 @@ def users_name User.pluck(:login_name, :id).sort end + def button_label(user) + if current_user.following?(user) + current_user.watching?(user) ? 'コメントあり' : 'コメントなし' + else + 'フォローする' + end + end + + def desc_paragraphs(user) + max_description = user.description.length <= 200 ? user.description : "#{user.description[0...200]}..." + max_description.split(/\n|\r\n/).map.with_index { |text, i| { id: i, text: } } + end + def all_countries_with_subdivisions ISO3166::Country.all .map { |country| [country.alpha2, country.subdivision_names_with_codes(I18n.locale.to_s)] } diff --git a/app/javascript/components/users.vue b/app/javascript/components/users.vue deleted file mode 100644 index b93977f8af6..00000000000 --- a/app/javascript/components/users.vue +++ /dev/null @@ -1,212 +0,0 @@ - - diff --git a/app/javascript/debounce.js b/app/javascript/debounce.js deleted file mode 100644 index 22ddf3db86a..00000000000 --- a/app/javascript/debounce.js +++ /dev/null @@ -1,13 +0,0 @@ -const debounce = (func, wait) => { - let timerId - return function (...args) { - if (timerId) { - clearTimeout(timerId) - } - timerId = setTimeout(() => { - func.apply(this, args) - }, wait) - } -} - -export default debounce diff --git a/app/javascript/following.js b/app/javascript/following.js index 66577ece977..2446657fb6f 100644 --- a/app/javascript/following.js +++ b/app/javascript/following.js @@ -1,5 +1,79 @@ import Vue from 'vue' import Following from 'following.vue' +import 'whatwg-fetch' +import CSRF from 'csrf' + +const usersIndex = { + getUrl(userId, isFollow, isWatch) { + return isFollow + ? `/api/followings/${userId}?watch=${isWatch}` + : `/api/followings?watch=${isWatch}` + }, + + getVerb(isFollow) { + return isFollow ? 'PATCH' : 'POST'; + }, + + followOrChangeFollow(userId, isFollow, isWatch) { + const params = { + id: userId + }; + fetch(usersIndex.getUrl(userId, isFollow, isWatch), { + method: usersIndex.getVerb(isFollow), + headers: { + 'Content-Type': 'application/json; charset=utf-8', + 'X-Requested-With': 'XMLHttpRequest', + 'X-CSRF-Token': CSRF.getToken() + }, + credentials: 'same-origin', + redirect: 'manual', + body: JSON.stringify(params) + }) + .then(function(response) { + if (response.ok) { + return response.json(); + } else { + alert('フォロー処理に失敗しました'); + } + }) + .then(function(data) { + document.getElementById(`follow_${userId}`).innerHTML = data.html; + }) + .catch(function(error) { + console.warn(error); + }); + }, + + unfollow(userId, isWatch) { + const params = { + id: userId + }; + fetch(usersIndex.getUrl(userId, true, isWatch), { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json; charset=utf-8', + 'X-Requested-With': 'XMLHttpRequest', + 'X-CSRF-Token': CSRF.getToken() + }, + credentials: 'same-origin', + redirect: 'manual', + body: JSON.stringify(params) + }) + .then(function(response) { + if (response.ok) { + return response.json(); + } else { + alert('フォロー処理に失敗しました'); + } + }) + .then(function(data) { + document.getElementById(`follow_${userId}`).innerHTML = data.html; + }) + .catch(function(error) { + console.warn(error); + }); + } +} document.addEventListener('DOMContentLoaded', () => { const followings = document.querySelectorAll('.js-following') @@ -23,3 +97,5 @@ document.addEventListener('DOMContentLoaded', () => { } } }) + +window.usersIndex = usersIndex; diff --git a/app/javascript/generation.vue b/app/javascript/generation.vue deleted file mode 100644 index 11856048c8d..00000000000 --- a/app/javascript/generation.vue +++ /dev/null @@ -1,86 +0,0 @@ - - diff --git a/app/javascript/generations.js b/app/javascript/generations.js deleted file mode 100644 index 88c91b7bfe1..00000000000 --- a/app/javascript/generations.js +++ /dev/null @@ -1,18 +0,0 @@ -import Vue from 'vue' -import Generations from 'generations.vue' - -document.addEventListener('DOMContentLoaded', () => { - const selector = '#js-generations' - const generations = document.querySelector(selector) - if (generations) { - const target = generations.getAttribute('data-target') - new Vue({ - render: (h) => - h(Generations, { - props: { - target: target - } - }) - }).$mount(selector) - } -}) diff --git a/app/javascript/generations.vue b/app/javascript/generations.vue deleted file mode 100644 index dc950d3333d..00000000000 --- a/app/javascript/generations.vue +++ /dev/null @@ -1,101 +0,0 @@ - - diff --git a/app/javascript/loading-users-list-placeholder.vue b/app/javascript/loading-users-list-placeholder.vue deleted file mode 100644 index bc8ec5d03f4..00000000000 --- a/app/javascript/loading-users-list-placeholder.vue +++ /dev/null @@ -1,52 +0,0 @@ - diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index aae0897a035..cc09b67d102 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -39,7 +39,6 @@ import '../mentor-mode.js' import '../bookmark.js' import '../agreements.js' import '../book-select.js' -import '../generations.js' import '../subscription-status.js' import '../new-event-date-set.js' import '../company-users.js' @@ -54,6 +53,7 @@ import '../hibernation_agreements.js' import '../current-date-time-setter.js' import '../modal-switcher.js' import '../survey-question-listings.js' +import '../search-user.js' import '../change-subdivisions.js' import '../register-address.js' import '../upload-image-to-article.js' @@ -65,7 +65,6 @@ import ExternalEntries from '../components/external-entries.vue' import Pages from '../components/pages.vue' import Questions from '../components/questions.vue' import WorriedUsers from '../components/worried-users.vue' -import Users from '../components/users.vue' import UsersAnswers from '../components/users-answers.vue' import User from '../components/user.vue' import Watches from '../components/watches.vue' @@ -91,7 +90,6 @@ mounter.addComponent(ExternalEntries) mounter.addComponent(Pages) mounter.addComponent(Questions) mounter.addComponent(WorriedUsers) -mounter.addComponent(Users) mounter.addComponent(UsersAnswers) mounter.addComponent(User) mounter.addComponent(Watches) diff --git a/app/javascript/search-user.js b/app/javascript/search-user.js new file mode 100644 index 00000000000..df2ee8fd670 --- /dev/null +++ b/app/javascript/search-user.js @@ -0,0 +1,104 @@ +import CSRF from 'csrf' + +const SearchUser = { + controller: null, + timeoutId: null, + + validateSearchUsersWord(searchUsersWord) { + if (searchUsersWord.match(/^[\w-]+$/)) + return searchUsersWord.length >= 3 + return searchUsersWord.length >= 2 + }, + addParams(searchUsersWord) { + if (!SearchUser.validateSearchUsersWord(searchUsersWord)) return + const params = new URL(location.origin).searchParams + params.set('search_word', searchUsersWord) + return params + }, + + url(searchUsersWord) { + const params = SearchUser.getParams() + const currentPage = Number(this.getParams().page) || 1 + const searchWordParams = SearchUser.addParams(searchUsersWord) + return ( + '/api/users/' + + (params.tag ? `tags/${params.tag}` : '') + + `?page=${currentPage}` + + (params.target ? `&target=${params.target}` : '') + + (params.watch ? `&watch=${params.watch}` : '') + + "&require_html=true" + + (searchWordParams ? `&${searchWordParams}` : '') + ) + }, + getParams() { + const params = {} + location.search + .slice(1) + .split('&') + .forEach((query) => { + const queryArr = query.split('=') + params[queryArr[0]] = queryArr[1] + }) + if (location.pathname.match(/tags/)) { + const tag = location.pathname.split('/').pop() + params.tag = tag + } + return params + }, + + async fetchUsersResource(searchUsersWord) { + if (this.controller) { + this.controller.abort(); + } + + this.controller = new AbortController(); + + const usersResource = await fetch(SearchUser.url(searchUsersWord), { + method: 'GET', + headers: { + 'Content-Type': 'application/json; charset=utf-8', + 'X-Requested-With': 'XMLHttpRequest', + 'X-CSRF-Token': CSRF.getToken() + }, + credentials: 'same-origin', + redirect: 'manual', + signal: this.controller.signal + }) + return usersResource.json() + }, + setupSearchedUsers(searchUsersWord) { + clearTimeout(this.timeoutId); + + this.timeoutId = setTimeout(() => { + SearchUser.fetchUsersResource(searchUsersWord) + .then((response) => { + document.getElementById(`user_lists`).innerHTML = response.html; + SearchUser.fixPaginationLinks(); + }) + .catch((error) => { + if (error.name === 'AbortError') { + // 非同期通信がキャンセルされた場合は何もしない + } else { + console.warn(error); + } + }); + }, 300); + }, + fixPaginationLinks() { + const paginationLinks = document.querySelectorAll('.pagination a'); + paginationLinks.forEach((link) => { + link.href = link.href.replace('/api/users', '/users'); + }); + } +} + +document.addEventListener('DOMContentLoaded', () => { + const inputElement = document.getElementById('js-user-search-input'); + + if (inputElement) { + inputElement.addEventListener('input', function() { + const inputValue = this.value.trim(); + SearchUser.setupSearchedUsers(inputValue); + }); + } +}) diff --git a/app/views/generations/index.html.slim b/app/views/generations/index.html.slim index 59a22a6610b..319d572e2fc 100644 --- a/app/views/generations/index.html.slim +++ b/app/views/generations/index.html.slim @@ -19,4 +19,35 @@ main.page-main = '期生別' | (#{t("target.#{@target}")}) hr.a-border - #js-generations(data-target="#{@target}") + .page-body + = paginate @generations + - if @generations.length == 0 + .container.is-lg + .o-empty-message + .o-empty-message__icon + i.fa-regular.fa-smile + p.o-empty-message__text + | ユーザーはありません + - else + .container.is-lg + .card-list.a-card + - @generations.each do |generation| + - users = Generation.new(generation.number).target_users(@target) + - if users.length != 0 + .user-group + header.user-group__header + h2.user-group__title + = link_to generation_path(generation.number), class: "user-group__title-link" do + span.user-group__title-label + = "#{generation.number}期生" + .user-group__date + = "#{l generation.start_date, format: :year_and_date} ~ #{l generation.end_date, format: :year_and_date}" + .a-user-icons + .a-user-icons__items + - users.each do |user| + - decorated_user = ActiveDecorator::Decorator.instance.decorate(user) + .a-user-icons__item + = link_to user_path(user), class: 'a-user-icons__item-link' do + span class=["a-user-role", "is-#{user.primary_role}"] + img.a-user-icons__item-icon.a-user-icon src=user.avatar_url title=user.icon_title data-login-name=user.login_name + = paginate @generations diff --git a/app/views/users/_activity_counts.html.slim b/app/views/users/_activity_counts.html.slim index 076b8e63cf8..71b6c6c7ca1 100644 --- a/app/views/users/_activity_counts.html.slim +++ b/app/views/users/_activity_counts.html.slim @@ -1,32 +1,33 @@ -.card-counts.is-user - dl.card-counts__items - .card-counts__item - .card-counts__item-inner - dt.card-counts__item-label - | 日報 - dd.card-counts__item-value - = link_to user.reports.count, user_reports_path(user) - .card-counts__item - .card-counts__item-inner - dt.card-counts__item-label - | 提出物 - dd.card-counts__item-value - = link_to user.products.count, user_products_path(user) - .card-counts__item - .card-counts__item-inner - dt.card-counts__item-label - | コメント - dd.card-counts__item-value - = link_to user.comments.count, user_comments_path(user) - .card-counts__item - .card-counts__item-inner - dt.card-counts__item-label - | 質問 - dd.card-counts__item-value - = link_to user.questions.count, user_questions_path(user) - .card-counts__item - .card-counts__item-inner - dt.card-counts__item-label - | 回答 - dd.card-counts__item-value - = link_to user.answers.count, user_answers_path(user) +- if user.student_or_trainee? + .card-counts.is-user + dl.card-counts__items + .card-counts__item + .card-counts__item-inner + dt.card-counts__item-label + | 日報 + dd.card-counts__item-value + = link_to user.reports.count, user_reports_path(user) + .card-counts__item + .card-counts__item-inner + dt.card-counts__item-label + | 提出物 + dd.card-counts__item-value + = link_to user.products.count, user_products_path(user) + .card-counts__item + .card-counts__item-inner + dt.card-counts__item-label + | コメント + dd.card-counts__item-value + = link_to user.comments.count, user_comments_path(user) + .card-counts__item + .card-counts__item-inner + dt.card-counts__item-label + | 質問 + dd.card-counts__item-value + = link_to user.questions.count, user_questions_path(user) + .card-counts__item + .card-counts__item-inner + dt.card-counts__item-label + | 回答 + dd.card-counts__item-value + = link_to user.answers.count, user_answers_path(user) diff --git a/app/views/users/_following.html.slim b/app/views/users/_following.html.slim new file mode 100644 index 00000000000..97e1b9cfde9 --- /dev/null +++ b/app/views/users/_following.html.slim @@ -0,0 +1,61 @@ +details.following ref='followingDetailsRef' + - is_following = current_user.following?(user) + - is_watching = current_user.watching?(user) + summary.following__summary + - if is_following && is_watching + span.a-button.is-warning.is-sm.is-block + i.fa-solid.fa-check + span + | コメントあり + - elsif is_following && !is_watching + span.a-button.is-warning.is-sm.is-block + i.fa-solid.fa-check + span + | コメントなし + - else + span.a-button.is-secondary.is-sm.is-block + | #{button_label(user)} + .following__dropdown.a-dropdown + ul.a-dropdown__items + li.following__dropdown-item.a-dropdown__item + - if is_following && is_watching + button.following-option.a-dropdown__item-inner.is-active + .following-option__inner + .following-option__label + | コメントあり + .following-option__desciption + | フォローしたユーザーの日報を自動でWatch状態にします。日報投稿時の通知と日報にコメントが来た際に通知を受け取ります。 + - else + = puts user.id + button.following-option.a-dropdown__item-inner onclick='usersIndex.followOrChangeFollow(#{user.id}, #{is_following}, true)' + .following-option__inner + .following-option__label + | コメントあり + .following-option__desciption + | フォローしたユーザーの日報を自動でWatch状態にします。日報投稿時の通知と日報にコメントが来た際に通知を受け取ります。 + li.following__dropdown-item.a-dropdown__item + - if is_following && !is_watching + button.following-option.a-dropdown__item-inner.is-active + .following-option__inner + .following-option__label + | コメントなし + .following-option__desciption + | フォローしたユーザーの日報はWatch状態にしません。日報投稿時の通知だけ通知を受けとります。 + - else + button.following-option.a-dropdown__item-inner onclick='usersIndex.followOrChangeFollow(#{user.id}, #{is_following}, false)' + .following-option__inner + .following-option__label + | コメントなし + .following-option__desciption + | フォローしたユーザーの日報はWatch状態にしません。日報投稿時の通知だけ通知を受けとります。 + li.following__dropdown-item.a-dropdown__item + - if !is_following + button.following-option.a-dropdown__item-inner.is-active + .following-option__inner + .following-option__label + | フォローしない + - else + button.following-option.a-dropdown__item-inner onclick='usersIndex.unfollow(#{user.id}, true)' + .following-option__inner + .following-option__label + | フォローしない diff --git a/app/views/users/_no_match_user.html.slim b/app/views/users/_no_match_user.html.slim new file mode 100644 index 00000000000..d9234af3243 --- /dev/null +++ b/app/views/users/_no_match_user.html.slim @@ -0,0 +1,8 @@ +- tag_or_target_name = @entered_tag ? @entered_tag : t("target.#{@target}") +- title = is_search ? "一致する" : "#{tag_or_target_name}の" +.row + .o-empty-message + .o-empty-message__icon + i.fa-regular.fa-sad-tear + p.o-empty-message__text + = "#{title}ユーザーはいません" diff --git a/app/views/users/_user.html.slim b/app/views/users/_user.html.slim new file mode 100644 index 00000000000..a815e5923bf --- /dev/null +++ b/app/views/users/_user.html.slim @@ -0,0 +1,73 @@ +.col-xxl-3.col-xl-4.col-lg-4.col-md-6.col-xs-12 + .users-item + .users-item__inner.a-card + .users-item__inactive-message-container.is-only-mentor + - if (current_user.mentor || current_user.admin) && user.student_or_trainee? + - if user.roles.include?("retired") + .users-item__inactive-message + | 退会しました + - elsif user.roles.include?("hibernationed") + .users-item__inactive-message + | 休会中: #{l user.hibernated_at, format: :year_and_date}〜(#{user.hibernation_elapsed_days}日経過) + - elsif !user.active? + .users-item__inactive-message + | 1ヶ月以上ログインがありません + header.users-item__header + .users-item__header-inner + .users-item__header-start + .users-item__icon + = link_to user.url + span class="a-user-role" + img.users-item__user-icon-image.a-user-icon title=user.icon_title alt=user.icon_title src=user.avatar_url + .users-item__header-end + .card-list-item__rows + .card-list-item__row + .card-list-item-title + = link_to user.url, class: 'card-list-item-title__title is-lg a-text-link' do + | #{user.login_name} + - if user.company && user.company.logo_url + = link_to company_path(user.company) do + img.user-item__company-logo src=user.company.logo_url + .card-list-item__row + .card-list-item-meta + .card-list-item-meta__items + .card-list-item-meta__item + .a-meta + | #{ user.name } + .card-list-item-meta__item + - if user.discord_profile.times_url + = link_to user.discord_profile.times_url, class: 'a-meta' do + .a-meta__icon + i.fa-brands.fa-discord + | #{ user.discord_profile.account_name } + - else + .a-meta + .a-meta__icon + i.fa-brands.fa-discord + | #{ user.discord_profile.account_name } + = render 'users/sns', user: + = render 'users/activity_counts', user: + .users-item__body + .users-item__description.a-short-text + - desc_paragraphs(user).each do |desc_paragraph| + p + | #{ desc_paragraph[:text] } + .users-item__tags + = render 'users/tags', user: + = render 'users/practices/completed_practices_progress', user:, is_progress: false + hr.a-border-tint + footer.card-footer + .card-main-actions + ul.card-main-actions__items + - if current_user.id != user.id && current_user.adviser && user.company && current_user.company_id == user.company.id + li.card-main-actions__item + .a-button.is-disabled.is-sm.is-block + i.fa-solid.fa-check + span + | 自社研修生 + - elsif current_user.id != user.id + li.card-main-actions__item id="follow_#{user.id}" + = render 'users/following', user: + - if current_user.admin && user.talk + li.card-main-actions__item.is-only-mentor + = link_to '相談部屋', user.talk, class: "a-button is-secondary is-sm is-block" diff --git a/app/views/users/_user_list.html.slim b/app/views/users/_user_list.html.slim new file mode 100644 index 00000000000..3a6a2f8002d --- /dev/null +++ b/app/views/users/_user_list.html.slim @@ -0,0 +1,9 @@ +- if users.length != 0 + .user-list + = paginate users, params: { controller: 'users', action: 'index' } + .row + - users.each do |user| + = render 'users/user', user: + = paginate users, params: { controller: 'users', action: 'index' } +- else + = render 'users/no_match_user', is_search: diff --git a/app/views/users/index.html.slim b/app/views/users/index.html.slim index 54dfac9b95f..60a1d0edcfd 100644 --- a/app/views/users/index.html.slim +++ b/app/views/users/index.html.slim @@ -48,7 +48,20 @@ main.page-main // TODO 暫定的な対応 .page-body__inner.has-side-nav .container - div(data-vue='Users') + .users + - if @users.length != 0 && request.path == '/users' && @target != "followings" + .page-filter.form.pt-0 + .container.is-md.has-no-x-padding + .form__items + .form-item.is-inline-md-up + label.a-form-label + | 絞り込み + input#js-user-search-input.a-text-input type="text" value="#{@search_word}" placeholder="ユーザー名、読み方、Discord ID、GitHub ID など" + .page-content.is-users + .users__items + .row id="user_lists" + = render 'users/user_list', users: @users, is_search: false + = render '/users/random_tags' - unless params[:tag] .sticky-message diff --git a/app/views/users/practices/_completed_practices.html.slim b/app/views/users/practices/_completed_practices.html.slim index b7cdd88d912..96442660241 100644 --- a/app/views/users/practices/_completed_practices.html.slim +++ b/app/views/users/practices/_completed_practices.html.slim @@ -4,5 +4,5 @@ .card-header__sub-title = render 'users/learning_status', user: user hr.a-border-tint - = render 'users/practices/completed_practices_progress', user: user + = render 'users/practices/completed_practices_progress', user: user, is_progress: true = render 'users/practices/completed_practices_list', user:, completed_learnings: diff --git a/app/views/users/practices/_completed_practices_progress.html.slim b/app/views/users/practices/_completed_practices_progress.html.slim index 5694443fa1e..5d0180058ce 100644 --- a/app/views/users/practices/_completed_practices_progress.html.slim +++ b/app/views/users/practices/_completed_practices_progress.html.slim @@ -1,6 +1,6 @@ - if user.graduated? - percentage = 100 - - fraction = "卒業(#{user.completed_practices.size})" + - fraction = "卒業#{"(#{user.completed_practices.size})" if is_progress}" - else - percentage = user.cached_completed_percentage - fraction = user.completed_fraction diff --git a/test/system/users_test.rb b/test/system/users_test.rb index 5cd191b74a8..be499abc66c 100644 --- a/test/system/users_test.rb +++ b/test/system/users_test.rb @@ -379,7 +379,6 @@ class UsersTest < ApplicationSystemTestCase test 'incremental search by login_name' do visit_with_auth '/users', 'komagata' - find('.users .loaded', wait: 60) assert_selector '.users-item', count: 24 fill_in 'js-user-search-input', with: 'kimura' assert_text 'Kimura Tadasi', count: 1 @@ -387,7 +386,6 @@ class UsersTest < ApplicationSystemTestCase test 'incremental search by name' do visit_with_auth '/users', 'kimura' - find('.users .loaded', wait: 60) assert_selector '.users-item', count: 24 fill_in 'js-user-search-input', with: 'Shinji' assert_text 'Hatsuno Shinji', count: 1 @@ -395,7 +393,6 @@ class UsersTest < ApplicationSystemTestCase test 'incremental search by name_kana' do visit_with_auth '/users', 'mentormentaro' - find('.users .loaded', wait: 60) assert_selector '.users-item', count: 24 fill_in 'js-user-search-input', with: 'キムラ ミタイ' assert_text 'Kimura Mitai', count: 1 @@ -403,7 +400,6 @@ class UsersTest < ApplicationSystemTestCase test 'incremental search by twitter_account' do visit_with_auth '/users', 'komagata' - find('.users .loaded', wait: 60) assert_selector '.users-item', count: 24 fill_in 'js-user-search-input', with: 'hatsuno' assert_text 'Hatsuno Shinji', count: 1 @@ -411,7 +407,6 @@ class UsersTest < ApplicationSystemTestCase test 'incremental search by blog_url' do visit_with_auth '/users', 'komagata' - find('.users .loaded', wait: 60) assert_selector '.users-item', count: 24 fill_in 'js-user-search-input', with: 'hatsuno.org' assert_text 'Hatsuno Shinji', count: 1 @@ -419,7 +414,6 @@ class UsersTest < ApplicationSystemTestCase test 'incremental search by github_account' do visit_with_auth '/users', 'komagata' - find('.users .loaded', wait: 60) assert_selector '.users-item', count: 24 fill_in 'js-user-search-input', with: 'kananashi' assert_text 'ユーザーです 読み方のカナが無い', count: 1 @@ -427,7 +421,6 @@ class UsersTest < ApplicationSystemTestCase test 'incremental search by facebook_url' do visit_with_auth '/users', 'komagata' - find('.users .loaded', wait: 60) assert_selector '.users-item', count: 24 fill_in 'js-user-search-input', with: 'kimurafacebook' assert_text 'Kimura Mitai', count: 1 @@ -435,7 +428,6 @@ class UsersTest < ApplicationSystemTestCase test 'incremental search by description' do visit_with_auth '/users', 'komagata' - find('.users .loaded', wait: 60) assert_selector '.users-item', count: 24 fill_in 'js-user-search-input', with: '木村です' assert_text 'Kimura Tadasi', count: 1 @@ -443,7 +435,6 @@ class UsersTest < ApplicationSystemTestCase test 'search only mentor when target is mentor' do visit_with_auth '/users?target=mentor', 'komagata' - find('.users .loaded', wait: 60) assert_selector '.users-item', count: 4 fill_in 'js-user-search-input', with: 'machida' assert_text 'Machida Teppei', count: 1 @@ -454,7 +445,6 @@ class UsersTest < ApplicationSystemTestCase test 'search only graduated students when target is graduate' do visit_with_auth '/users?target=graduate', 'komagata' - find('.users .loaded', wait: 60) assert_selector '.users-item', count: 3 fill_in 'js-user-search-input', with: '卒業 就職済美' assert_text '卒業 就職済美', count: 1 @@ -465,7 +455,6 @@ class UsersTest < ApplicationSystemTestCase test 'search only adviser when target is adviser' do visit_with_auth '/users?target=adviser', 'komagata' - find('.users .loaded', wait: 60) assert_selector '.users-item', count: 3 fill_in 'js-user-search-input', with: 'advijirou' assert_text 'アドバイ 次郎', count: 1 @@ -476,7 +465,6 @@ class UsersTest < ApplicationSystemTestCase test 'search only retired when target is retired' do visit_with_auth '/users?target=retired', 'komagata' - find('.users .loaded', wait: 60) assert_selector '.users-item', count: 4 fill_in 'js-user-search-input', with: 'yameo' assert_text '辞目 辞目夫', count: 1 @@ -487,7 +475,6 @@ class UsersTest < ApplicationSystemTestCase test 'search only trainee when target is trainee' do visit_with_auth '/users?target=trainee', 'komagata' - find('.users .loaded', wait: 60) assert_selector '.users-item', count: 2 fill_in 'js-user-search-input', with: 'Kensyu Seiko' assert_text 'Kensyu Seiko', count: 1 @@ -498,7 +485,6 @@ class UsersTest < ApplicationSystemTestCase test 'search users from all users when target is all' do visit_with_auth '/users?target=all', 'komagata' - find('.users .loaded', wait: 60) assert_selector '.users-item', count: 24 fill_in 'js-user-search-input', with: 'hajime' assert_text 'Hajime Tayo', count: 1 @@ -515,7 +501,6 @@ class UsersTest < ApplicationSystemTestCase test 'only show incremental search in all tab' do visit_with_auth '/users', 'komagata' - find('.users .loaded', wait: 60) assert_selector '#js-user-search-input' visit '/generations' @@ -536,7 +521,6 @@ class UsersTest < ApplicationSystemTestCase test 'incremental search needs more than two characters for Japanese and three for others' do visit_with_auth '/users', 'komagata' - find('.users .loaded', wait: 60) assert_selector '.users-item', count: 24 fill_in 'js-user-search-input', with: 'ki' assert_selector '.users-item', count: 24