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

個人の日報一覧をプラクティスで絞り込めるようにした #6044

Merged
merged 16 commits into from
Feb 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions app/javascript/components/CommentUserIcon.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react'

export default function CommentUserIcon({comment}) {
return (
<a className="card-list-item__user-icons-icon" href={`/users/${comment.user_id}`}>
<img className="a-user-icon" src={comment.user_icon} />
</a>
)
}
37 changes: 37 additions & 0 deletions app/javascript/components/ListComment.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react'
import CommentUserIcon from './CommentUserIcon'

export default function ListComment({ report }) {
return (
<>
<hr className="card-list-item__row-separator"></hr>
<div className="card-list-item__row">
<div className="card-list-item-meta">
<div className="card-list-item-meta__items">
<div className="card-list-item-meta__item">
<div className="a-meta">
コメント({report.numberOfComments})
</div>
</div>
<div className="card-list-item-meta__item">
<div className="card-list-item__user-icons">
{report.comments.map((comment) => {
return (
<CommentUserIcon comment={comment} key={comment.user_id} />
)
})}
</div>
</div>
<div className="card-list-item-meta__item">
<time
className="a-meta"
dateTime={report.lastCommentDatetime}
pubdate="'pubdate'">{`〜 ${report.lastCommentDate}`}</time>
</div>
<div className="card-list-item-meta__item"></div>
</div>
</div>
</div>
</>
)
}
66 changes: 66 additions & 0 deletions app/javascript/components/PracticeFilterDropdown.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React, { useState, useEffect, useRef } from 'react'
import Choices from 'choices.js'

export default function PracticeFilterDropdown({
practices,
setPracticeId,
practiceId
}) {
const [selectedId, setSelectedId] = useState(practiceId)

const onChange = (event) => {
const value = event.target.value
setSelectedId(value)
setPracticeId(value)
}

const selectRef = useRef(null)

useEffect(() => {
const selectElement = selectRef.current
const choicesInstance = new Choices(selectElement, {
searchEnabled: true,
allowHTML: true,
searchResultLimit: 20,
searchPlaceholderValue: '検索ワード',
noResultsText: '一致する情報は見つかりません',
itemSelectText: '選択',
shouldSort: false
})

return () => {
choicesInstance.destroy()
}
}, [])

return (
<>
<nav className="page-filter form">
<div className="container is-md">
<div className="form-item is-inline-md-up">
<label className="a-form-label" htmlFor="js-choices-single-select">
プラクティスで絞り込む
</label>
<select
className="a-form-select"
onChange={onChange}
ref={selectRef}
value={selectedId}
id="js-choices-single-select">
<option key="" value="">
全ての日報を表示
</option>
{practices.map((practice) => {
return (
<option key={practice.id} value={practice.id}>
{practice.title}
</option>
)
})}
</select>
</div>
</div>
</nav>
</>
)
}
109 changes: 109 additions & 0 deletions app/javascript/components/Report.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React from 'react'
import ListComment from './ListComment'

export default function Report({ report, currentUser }) {
return (
<div className={`card-list-item ${report.wip ? 'is-wip' : ''}`}>
<div className="card-list-item__inner">
<div className="card-list-item__user">
<a href={report.user.url} className="card-list-item__user-link">
<span className='["a-user-role", roleClass]'>
<img
className="card-list-item__user-icon a-user-icon"
src={report.user.avatar_url}
title={report.user.login_name}
alt={report.user.login_name}
/>
</span>
</a>
</div>
<div className="card-list-item__rows">
<div className="card-list-item__row">
<header className="card-list-item-title">
<div className="card-list-item-title__start">
{report.wip && (
<div className="a-list-item-badge is-wip">
<span>WIP</span>
</div>
)}
<h2 className="card-list-item-title__title">
<a
className="card-list-item-title__link a-text-link js-unconfirmed-link"
href={report.url}>
<img
className="card-list-item-title__emotion-image"
src={`/images/emotion/${report.emotion}.svg`}
alt={report.emotion}
/>
{report.title}
</a>
</h2>
{currentUser.id === report.user.id && (
<ReportListItemActions report={report} />
)}
</div>
</header>
</div>
<div className="card-list-item__row">
<div className="card-list-item-meta">
<div className="card-list-item-meta__items">
<div className="card-list-item-meta__item">
<a className="a-user-name" href={report.user.url}>
{report.user.long_name}
</a>
</div>
<div className="card-list-item-meta__item">
<time className="a-meta">{`${report.reportedOn}の日報`}</time>
</div>
</div>
</div>
{report.hasAnyComments && <ListComment report={report} />}
</div>
</div>
{report.hasCheck && (
<div className="stamp stamp-approve">
<h2 className="stamp__content is-title">確認済</h2>
<time className="stamp__content is-created-at">
{report.checkDate}
</time>
<div className="stamp__content is-user-name">
<div className="stamp__content-inner">{report.checkUserName}</div>
</div>
</div>
)}
</div>
</div>
)
}

const ReportListItemActions = ({ report }) => {
return (
<div className="card-list-item-title__end">
<label className="card-list-item-actions__trigger" htmlFor={report.id}>
<i className="fa-solid fa-ellipsis-h"></i>
</label>
<div className="card-list-item-actions">
<input className="a-toggle-checkbox" type="checkbox" id={report.id} />
<div className="card-list-item-actions__inner">
<ul className="card-list-item-actions__items">
<li className="card-list-item-actions__item">
<a
className="card-list-item-actions__action"
href={report.editURL}>
<i className="fa-solid fa-pen">内容変更</i>
</a>
</li>
<li className="card-list-item-actions__item">
<a
className="card-list-item-actions__action"
href={report.newURL}>
<i className="fa-solid fa-copy">コピー</i>
</a>
</li>
</ul>
<label className="a-overlay" htmlFor={report.id}></label>
</div>
</div>
</div>
)
}
106 changes: 106 additions & 0 deletions app/javascript/components/Reports.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React, { useState, useEffect } from 'react'
import useSWR from 'swr'
import queryString from 'query-string'
import fetcher from '../fetcher'
import LoadingListPlaceholder from './LoadingListPlaceholder'
import Report from './Report'
import Pagination from './Pagination'
import PracticeFilterDropdown from './PracticeFilterDropdown'

export default function Reports({ user, currentUser, practices }) {
const per = 20
const neighbours = 4
const defaultPage = parseInt(queryString.parse(location.search).page) || 1
const [page, setPage] = useState(defaultPage)
const [practiceId, setPracticeId] = useState('')

useEffect(() => {
setPage(page)
}, [page])

useEffect(() => {
setPracticeId(practiceId)
}, [practiceId])

const { data, error } = useSWR(
`/api/reports.json?user_id=${user.id}&page=${page}&practice_id=${practiceId}`,
fetcher
)

if (error) return <>エラーが発生しました。</>
if (!data) {
return (
<div className="container is-md">
<LoadingListPlaceholder />
</div>
)
}

return (
<>
{data.totalPages === 0 && (
<div className="container is-md">
<PracticeFilterDropdown
practices={practices}
setPracticeId={setPracticeId}
practiceId={practiceId}
/>
<NoReports />
</div>
)}
{data.totalPages > 0 && (
<div className="container is-md">
<PracticeFilterDropdown
practices={practices}
setPracticeId={setPracticeId}
practiceId={practiceId}
/>
<div className="page-content reports">
{data.totalPages > 1 && (
<Pagination
sum={data.totalPages * per}
per={per}
neighbours={neighbours}
page={page}
onChange={(e) => setPage(e.page)}
/>
)}
<ul className="card-list a-card">
<div className="card-list__items">
{data.reports.map((report) => {
return (
<Report
key={report.id}
report={report}
currentUser={currentUser}
/>
)
})}
</div>
</ul>
{data.totalPages > 1 && (
<Pagination
sum={data.totalPages * per}
per={per}
neighbours={neighbours}
page={page}
onChange={(e) => setPage(e.page)}
/>
)}
</div>
</div>
)}
</>
)
}

const NoReports = () => {
return (
<div className="o-empty-message">
<div className="o-empty-message__icon">
<i className="fa-regular fa-face-sad-tear" />
<p className="o-empty-message__text">日報はまだありません。</p>
</div>
</div>
)
}
4 changes: 2 additions & 2 deletions app/javascript/components/report.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@
.card-list-item-actions__inner
ul.card-list-item-actions__items
li.card-list-item-actions__item
a.card-list-item-actions__action(:href='report.editURL')
a.card-list-item-actions__action(:href='report.editPath')
i.fa-solid.fa-pen
| 内容変更
li.card-list-item-actions__item
a.card-list-item-actions__action(:href='report.newURL')
a.card-list-item-actions__action(:href='report.newPath')
i.fa-solid.fa-copy
| コピー
label.a-overlay(:for='report.id')
Expand Down
6 changes: 4 additions & 2 deletions app/views/api/reports/_report.json.jbuilder
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ json.id report.id
json.title report.title
json.reportedOn l(report.reported_on)
json.url report_url(report)
json.editURL edit_report_path(report)
json.newURL new_report_path(id: report)
json.editPath edit_report_path(report)
json.newPath new_report_path(id: report)
json.editURL edit_report_url(report)
json.newURL new_report_url(id: report)
json.wip report.wip?
json.emotion report.emotion
3 changes: 1 addition & 2 deletions app/views/users/reports/index.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,4 @@
= render 'users/page_tabs', user: @user

.page-body
.container.is-md
div(data-vue="Reports" data-vue-user-id:number="#{params[:user_id]}")
= react_component('Reports', user: @user, currentUser: current_user, practices: @user.practices)
18 changes: 18 additions & 0 deletions test/system/user/reports_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@ class User::ReportsTest < ApplicationSystemTestCase
assert_equal 'hatsuno 日報 | FBC', title
end

test 'can use select box to filter user reports by practice' do
visit_with_auth "/users/#{users(:hajime).id}/reports", 'kimura'

expected_reports = users(:hajime).reports
displayed_reports_count = all('.card-list-item-title__link').count
assert_equal expected_reports.count, displayed_reports_count

find('.choices__inner').click
find('#choices--js-choices-single-select-item-choice-3', text: 'Terminalの基礎を覚える').click
assert_text 'Terminalの基礎を覚える'

expected_reports = Practice.find_by(title: 'Terminalの基礎を覚える').reports.where(user: users(:hajime))
displayed_reports_count = all('.card-list-item-title__link').count
assert_equal expected_reports.count, displayed_reports_count

assert_text expected_reports.first.title
end

test 'cannot access other users download reports' do
visit_with_auth "/users/#{users(:hatsuno).id}/reports.md", 'kimura'
assert_text '自分以外の日報はダウンロードすることができません'
Expand Down