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

通知一覧をVue.jsで非同期にした #2260

Merged
merged 21 commits into from
Feb 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
13efe69
viewからkaminariの記述を削除
sano11o1 Dec 29, 2020
e935ae2
vuejs-paginateを導入
sano11o1 Dec 31, 2020
9e3b271
既存のNotificationsをNotificationsBellに変更
sano11o1 Dec 31, 2020
2e98d3f
notificationsのJSONを返すようにした
sano11o1 Jan 4, 2021
fbcc055
通知一覧をviewからvueに移行
sano11o1 Jan 5, 2021
b8c14c4
未読の通知一覧を非同期で読み込むようにした
sano11o1 Jan 5, 2021
b2b4cd1
通知一覧にページャーを追加
sano11o1 Jan 5, 2021
d0fc1ac
vuejs-paginateで生成されるページャーのデザインを追記
sano11o1 Jan 12, 2021
e630bac
コントローラーの不要な記述を削除
sano11o1 Jan 12, 2021
c006442
通知一覧のページャーのテストを追記
sano11o1 Jan 18, 2021
2b45c9c
未読の通知を一括で開くボタンをVue化
sano11o1 Jan 28, 2021
cb5a6de
未読の通知を一括で開くボタンが表示されることを確認するテストを修正
sano11o1 Jan 28, 2021
266afea
通知一覧のページャーのテストを修正
sano11o1 Feb 6, 2021
d8da559
通知のvueファイル、jsファイルの細かな点を修正
sano11o1 Feb 6, 2021
526bf4b
notifications/index.json.jbuilderに書いてあったcreated_at系の記述を減らした
sano11o1 Feb 6, 2021
e20315e
_notification.html.slim はVueに置き換えたので削除
sano11o1 Feb 6, 2021
4742dd8
mentorLoginプロパティをisMentorプロパティに変更
sano11o1 Feb 6, 2021
0ee7afc
ページャーの位置調整、disabledのぺージャーリンクを目立たない表示に変更
machida Feb 22, 2021
68fccd4
ページャーが表示されないときは、そのDOMが表示されないようにした
machida Feb 22, 2021
37f5471
ページャーのデザイン変更に合わせてテストを修正
sano11o1 Feb 22, 2021
91f9f6d
Notificationsのintegrationテストを書きました
sano11o1 Feb 23, 2021
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
7 changes: 7 additions & 0 deletions app/assets/stylesheets/blocks/shared/_pagination.sass
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@

.pagination__item
+padding(horizontal, .125rem)
&.is-disabled
opacity: .4
pointer-events: none

.pagination__item-link
+size(2.5rem)
border-radius: 50%
outline: none
+text-block(.875rem 1, $muted-text)
+flex-link
justify-content: center
Expand All @@ -33,3 +37,6 @@
&.is-active
background-color: $main
color: $reversal-text
.pagination__item.is-active &
background-color: $main
color: $reversal-text
15 changes: 15 additions & 0 deletions app/controllers/api/notifications/unread_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

class API::Notifications::UnreadController < API::BaseController
Copy link
Member

Choose a reason for hiding this comment

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

すいません、他のレビューで気づいた点をひとつ。

下記のようなAPIのテストがあるとよりよりかもです。

https://github.com/fjordllc/bootcamp/blob/master/test/integration/api/practices_test.rb

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@komagata
APIのテストを書きましたので確認お願いします! 🙏

def index
@notifications = if params[:page]
current_user.notifications
.unreads_with_avatar
.order(created_at: :desc)
.page(params[:page])
else
current_user.notifications.unreads_with_avatar
end
render template: 'api/notifications/index'
end
end
5 changes: 4 additions & 1 deletion app/controllers/api/notifications_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

class API::NotificationsController < API::BaseController
def index
@notifications = current_user.notifications.unreads_with_avatar
@notifications = current_user.notifications
.reads_with_avatar
.order(created_at: :desc)
.page(params[:page])
end
end
7 changes: 1 addition & 6 deletions app/controllers/notifications/unread_controller.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
# frozen_string_literal: true

class Notifications::UnreadController < ApplicationController
def index
@notifications = current_user.notifications
.unreads_with_avatar
.order(created_at: :desc)
.page(params[:page])
end
def index; end
end
7 changes: 1 addition & 6 deletions app/controllers/notifications_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@ class NotificationsController < ApplicationController
before_action :require_login, only: %i[index show]
before_action :set_my_notification, only: %i[show]

def index
@notifications = current_user.notifications
.reads_with_avatar
.order(created_at: :desc)
.page(params[:page])
end
def index; end

def show
path = @notification.read_attribute :path
Expand Down
35 changes: 35 additions & 0 deletions app/javascript/notification.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<template lang="pug">
.thread-list-item(:class="notification.read ? 'is-read' : 'is-unread'")
.thread-list-item__inner
.thread-list-item__author
img.thread-list-item__author-icon.a-user-icon(:title="notification.sender.icon_title" :src="notification.sender.avatar_url" :class="[roleClass, daimyoClass]")
header.thread-list-item__header
.thread-list-item__header-title-container
.thread-list-item__header-icon.is-unread(v-if='notification.read === false')
| 未読
h2.thread-list-item__title(itemprop='name')
a.thread-list-item__title-link.js-unconfirmed-link(:href="notification.path" itemprop='url')
span.thread-list-item__title-link-label {{ notification.message }}
.thread-list-item-meta
time.thread-list-item-meta__created-at(:datetime='notification.created_at') {{ formattedCreatedAtInJapanese }}
</template>
<script>
import dayjs from 'dayjs'
import ja from 'dayjs/locale/ja'
dayjs.locale(ja)

export default {
props: ['notification'],
computed: {
formattedCreatedAtInJapanese() {
return dayjs(this.notification.created_at).format('YYYY年MM月DD日(ddd) HH:mm')
},
roleClass() {
return `is-${this.notification.sender.role}`
},
daimyoClass() {
return { 'is-daimyo': this.notification.sender.daimyo }
}
}
}
</script>
38 changes: 12 additions & 26 deletions app/javascript/notifications.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,18 @@
import Vue from 'vue'
import Notifications from './notifications.vue'
import NotificationsMobile from './notifications_mobile.vue'
import isMobile from 'ismobilejs'

document.addEventListener('DOMContentLoaded', () => {
if (isMobile(window.navigator).any) {
const notifications = document.querySelector('#js-notifications')
if (notifications) {
notifications.style.display = 'none'
}

const notificationsMobile = document.querySelector('#js-notifications-mobile')
if (notificationsMobile) {
new Vue({
render: h => h(NotificationsMobile)
}).$mount('#js-notifications-mobile')
}
} else {
const notificationsMobile = document.querySelector('#js-notifications-mobile')
if (notificationsMobile) {
notificationsMobile.style.display = 'none'
}

const notifications = document.querySelector('#js-notifications')
if (notifications) {
new Vue({
render: h => h(Notifications)
}).$mount('#js-notifications')
}
const selector = '#js-notifications'
const notifications = document.querySelector(selector)
if (notifications) {
const isMentor = notifications.getAttribute('data-is-mentor')
new Vue({
render: h => h(Notifications, {
props: {
// 文字列を真偽値に変換して渡す
isMentor: isMentor === 'true'
}
})
}).$mount(selector)
}
})
187 changes: 134 additions & 53 deletions app/javascript/notifications.vue
Original file line number Diff line number Diff line change
@@ -1,71 +1,152 @@
<template lang="pug">
li.header-links__item(v-bind:class="hasCountClass")
label.header-links__link.test-show-notifications(for="header-notification-pc" @click="clickBell")
.header-links__link.test-bell
.header-notification-icon
.header-notification-count.a-notification-count.test-notification-count(v-show="notificationExist") {{ this.notificationCount }}
i.fas.fa-bell
.header-links__link-label 通知
input.a-toggle-checkbox(v-if="notificationExist" type="checkbox" id="header-notification-pc")
.header-dropdown
label.header-dropdown__background(for="header-notification-pc")
.header-dropdown__inner.is-notification
ul.header-dropdown__items
li.header-dropdown__item(v-for="notification in notifications")
a.header-dropdown__item-link(:href="notification.path")
.header-notifications-item__body
img.header-notifications-item__user-icon.a-user-icon(:src="notification.avatar_url")
.header-notifications-item__message
p.test-notification-message {{ notification.message }}
time.header-notifications-item_created-at {{ notification.created_at }}
footer.header-dropdown__footer
a.header-dropdown__footer-link(href="/notifications/unread") 全ての未読通知
a.header-dropdown__footer-link(href="/notifications") 全ての通知
a.header-dropdown__footer-link(href="/notifications/allmarks" ref="nofollow" data-method="post") 全て既読にする
.container(v-if="loaded && notifications.length > 0")
nav.pagination(v-if="totalPages > 1")
pager-top(
v-model="currentPage"
:page-count="totalPages"
:page-range="5"
:prev-text="`<i class='fas fa-angle-left'></i>`"
:next-text="`<i class='fas fa-angle-right'></i>`"
:first-button-text="`<i class='fas fa-angle-double-left'></i>`"
:last-button-text="`<i class='fas fa-angle-double-right'></i>`"
:click-handler="paginateClickCallback"
:container-class="'pagination__items'"
:page-class="'pagination__item'"
:page-link-class="'pagination__item-link'"
:disabled-class="'is-disabled'"
:active-class="'is-active'"
:prev-class="'is-prev pagination__item'"
:prev-link-class="'is-prev pagination__item-link'"
:next-class="'is-next pagination__item'"
:next-link-class="'is-next pagination__item-link'"
:first-last-button="true"
:hide-prev-next="true"
:margin-pages="0"
:break-view-text=null
)
.thread-list.a-card
notification(v-for="notification in notifications"
:key="notification.id"
:notification="notification")
unconfirmed-links-open-button(v-if="isMentor && isUnreadPage" label="未読の通知を一括で開く")
nav.pagination(v-if="totalPages > 1")
pager-bottom(
v-model="currentPage"
:page-count="totalPages"
:page-range="5"
:prev-text="`<i class='fas fa-angle-left'></i>`"
:next-text="`<i class='fas fa-angle-right'></i>`"
:first-button-text="`<i class='fas fa-angle-double-left'></i>`"
:last-button-text="`<i class='fas fa-angle-double-right'></i>`"
:click-handler="paginateClickCallback"
:container-class="'pagination__items'"
:page-class="'pagination__item'"
:page-link-class="'pagination__item-link'"
:disabled-class="'is-disabled'"
:active-class="'is-active'"
:prev-class="'is-prev pagination__item'"
:prev-link-class="'is-prev pagination__item-link'"
:next-class="'is-next pagination__item'"
:next-link-class="'is-next pagination__item-link'"
:first-last-button="true"
:hide-prev-next="true"
:margin-pages="0"
:break-view-text=null
)
.container(v-else-if="loaded")
.o-empty-massage
.o-empty-massage__icon
i.far.fa-smile
p.o-empty-massage__text(v-if="isUnreadPage")
| 未読の通知はありません
p.o-empty-massage__text(v-else)
| 通知はありません
.container(v-else)
| ロード中
</template>

<script>
import Notification from './notification.vue'
import VueJsPaginate from 'vuejs-paginate'
import UnconfirmedLinksOpenButton from './unconfirmed_links_open_button'

export default {
props: {
isMentor: {
type: Boolean
}
},
components: {
'notification': Notification,
'pager-top': VueJsPaginate,
'pager-bottom': VueJsPaginate,
'unconfirmed-links-open-button': UnconfirmedLinksOpenButton
},
data: () => {
return {
notifications: []
notifications: [],
totalPages: 0,
currentPage: null,
loaded: false
}
},
created() {
fetch(`/api/notifications.json`, {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
},
credentials: 'same-origin',
redirect: 'manual'
})
.then(response => {
return response.json()
})
.then(json => {
json.forEach(n => { this.notifications.push(n) })
})
.catch(error => {
console.warn('Failed to parsing', error)
})
// ブラウザバック・フォワードした時に画面を読み込ませる
window.onpopstate = function() {
location.href = location.href
}
this.currentPage = Number(this.getPageValueFromParameter()) || 1
this.getNotificationsPerPage()
},
methods: {
clickBell() {
if (!this.notificationExist) {
location.href = '/notifications'
computed: {
url() {
if (this.isUnreadPage) {
return `/api/notifications/unread.json?page=${this.currentPage}`
} else {
return `/api/notifications.json?page=${this.currentPage}`
}
},
isUnreadPage() {
return location.pathname.includes('unread')
}
},
computed: {
notificationCount() {
const count = this.notifications.length
return count > 99 ? '99+' : String(count)
methods: {
getNotificationsPerPage: function() {
fetch(this.url, {
method: 'GET',
headers: { 'X-Requested-With': 'XMLHttpRequest'},
credentials: 'same-origin',
redirect: 'manual'
})
.then(response => {
return response.json()
})
.then(json => {
this.totalPages = json['total_pages']
this.notifications = []
json['notifications'].forEach(n => { this.notifications.push(n) })
this.loaded = true
})
.catch(error => {
console.warn('Failed to parsing', error)
})
},
updateCurrentUrl: function() {
let url = location.pathname
if (this.currentPage !== 1) {
url += `?page=${this.currentPage}`
}
history.pushState(null, null, url)
},
notificationExist() {
return this.notifications.length > 0
paginateClickCallback: function() {
this.getNotificationsPerPage()
this.updateCurrentUrl()
},
hasCountClass() {
return this.notificationExist ? 'has-count' : 'has-no-count'
getPageValueFromParameter: function() {
let url = location.href
let results = url.match(/\?page=(\d+)/)
if (!results) return null;
return results[1]
}
}
}
Expand Down
32 changes: 32 additions & 0 deletions app/javascript/notifications_bell.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Vue from 'vue'
import NotificationsBell from './notifications_bell.vue'
import NotificationsBellMobile from './notifications_bell_mobile.vue'
import isMobile from 'ismobilejs'

Copy link
Member

Choose a reason for hiding this comment

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

notifications_bellいい名前だと思います!

document.addEventListener('DOMContentLoaded', () => {
if (isMobile(window.navigator).any) {
const notifications = document.querySelector('#js-notifications-bell')
if (notifications) {
notifications.style.display = 'none'
}

const notificationsBellMobile = document.querySelector('#js-notifications-bell-mobile')
if (notificationsBellMobile) {
new Vue({
render: h => h(NotificationsBellMobile)
}).$mount('#js-notifications-bell-mobile')
}
} else {
const notificationsBellMobile = document.querySelector('#js-notifications-bell-mobile')
if (notificationsBellMobile) {
notificationsBellMobile.style.display = 'none'
}

const notificationsBell = document.querySelector('#js-notifications-bell')
if (notificationsBell) {
new Vue({
render: h => h(NotificationsBell)
}).$mount('#js-notifications-bell')
}
}
})
Loading