diff --git a/app/controllers/api/bookmarks_controller.rb b/app/controllers/api/bookmarks_controller.rb index ff6830baf51..1767fd7c066 100644 --- a/app/controllers/api/bookmarks_controller.rb +++ b/app/controllers/api/bookmarks_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class API::BookmarksController < API::BaseController - PAGER_NUMBER = 25 + PAGER_NUMBER = 20 def index per = params[:per] || PAGER_NUMBER diff --git a/app/controllers/current_user/bookmarks_controller.rb b/app/controllers/current_user/bookmarks_controller.rb index 6e89ba1a89c..d24d54732e6 100644 --- a/app/controllers/current_user/bookmarks_controller.rb +++ b/app/controllers/current_user/bookmarks_controller.rb @@ -1,7 +1,20 @@ # frozen_string_literal: true class CurrentUser::BookmarksController < ApplicationController - def index - @user = current_user + PAGER_NUMBER = 20 + + before_action :set_bookmarks, only: [:index] + + def index; end + + def destroy + current_user.bookmarks.find(params[:id]).destroy + head :no_content + end + + private + + def set_bookmarks + @bookmarks = current_user.bookmarks.preload(bookmarkable: :user).order(created_at: :desc, id: :desc).page(params[:page]).per(PAGER_NUMBER) end end diff --git a/app/javascript/bookmarks-delete-button-visibility.js b/app/javascript/bookmarks-delete-button-visibility.js new file mode 100644 index 00000000000..47ad0e9f8a5 --- /dev/null +++ b/app/javascript/bookmarks-delete-button-visibility.js @@ -0,0 +1,6 @@ +export function toggleDeleteButtonVisibility(editToggle, deleteButtons) { + const displayStyle = editToggle.checked ? 'block' : 'none' + for (const button of deleteButtons) { + button.style.display = displayStyle + } +} diff --git a/app/javascript/bookmarks-edit-button.js b/app/javascript/bookmarks-edit-button.js deleted file mode 100644 index 76407116d92..00000000000 --- a/app/javascript/bookmarks-edit-button.js +++ /dev/null @@ -1,23 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - const bookMarksEditButton = document.getElementById('bookmark_edit') - const bookmarkDeleteButton = document.getElementsByClassName( - 'js-bookmark-delete-button' - ) - if (bookMarksEditButton && bookmarkDeleteButton) { - for (let i = 0; i < bookmarkDeleteButton.length; i++) { - bookmarkDeleteButton[i].style.display = 'none' - } - - bookMarksEditButton.addEventListener('click', () => { - if (bookMarksEditButton.checked) { - for (let i = 0; i < bookmarkDeleteButton.length; i++) { - bookmarkDeleteButton[i].style.display = 'block' - } - } else { - for (let i = 0; i < bookmarkDeleteButton.length; i++) { - bookmarkDeleteButton[i].style.display = 'none' - } - } - }) - } -}) diff --git a/app/javascript/bookmarks.js b/app/javascript/bookmarks.js new file mode 100644 index 00000000000..7033ef8c2f9 --- /dev/null +++ b/app/javascript/bookmarks.js @@ -0,0 +1,83 @@ +import { get, destroy } from '@rails/request.js' +import { toggleDeleteButtonVisibility } from './bookmarks-delete-button-visibility' + +const EDIT_MODE_KEY = 'bookmark_edit_mode' +const DELETE_BUTTON_CLASS = 'js-bookmark-delete-button' + +document.addEventListener('DOMContentLoaded', () => { + const editButton = document.getElementById('bookmark_edit') + const pageBody = document.querySelector('.page-body') + if (!pageBody) return + + const savedValue = sessionStorage.getItem(EDIT_MODE_KEY) + const savedMode = savedValue === 'true' + initialize(savedMode) + + if (editButton) { + editButton.addEventListener('change', () => { + const deleteButtons = document.getElementsByClassName(DELETE_BUTTON_CLASS) + toggleDeleteButtonVisibility(editButton, deleteButtons) + sessionStorage.setItem(EDIT_MODE_KEY, editButton.checked) + }) + } + + document.addEventListener('click', async (event) => { + const deleteButton = event.target.closest('.bookmark-delete-button') + if (!deleteButton) return + + deleteButton.disabled = true + + try { + const url = deleteButton.dataset.url + const response = await destroy(url) + + if (!response.ok) { + throw new Error(`削除に失敗しました。(ステータス: ${response.status})`) + } + + const params = new URLSearchParams(location.search) + const currentPage = parseInt(params.get('page') || '1', 10) + const newPageMain = await fetchPageMain(currentPage) + + // 空ページの場合は1ページ前にフォールバック + let pageToShow = newPageMain + if (currentPage > 1 && newPageMain.querySelector('.o-empty-message')) { + pageToShow = await fetchPageMain(currentPage - 1) + } + document.querySelector('.page-body').replaceWith(pageToShow) + + const savedModeAfterDelete = + sessionStorage.getItem(EDIT_MODE_KEY) === 'true' + initialize(savedModeAfterDelete) + } catch (error) { + console.warn(error) + deleteButton.disabled = false + } + }) +}) + +window.addEventListener('beforeunload', () => { + const isBookmarkPage = location.pathname.includes('/current_user/bookmarks') + if (!isBookmarkPage) { + sessionStorage.removeItem(EDIT_MODE_KEY) + } +}) + +const fetchPageMain = async (page) => { + const bookmarkUrl = `/current_user/bookmarks?page=${page}` + const response = await get(bookmarkUrl, { responseKind: 'html' }) + const html = await response.text + const parser = new DOMParser() + const parsedDocument = parser.parseFromString(html, 'text/html') + return parsedDocument.querySelector('.page-body') +} + +const initialize = (deleteMode = false) => { + const editButton = document.getElementById('bookmark_edit') + const deleteButtons = document.getElementsByClassName(DELETE_BUTTON_CLASS) + + if (editButton && deleteButtons.length > 0) { + editButton.checked = deleteMode + toggleDeleteButtonVisibility(editButton, deleteButtons) + } +} diff --git a/app/javascript/components/Bookmarks.jsx b/app/javascript/components/Bookmarks.jsx deleted file mode 100644 index db737cd07f3..00000000000 --- a/app/javascript/components/Bookmarks.jsx +++ /dev/null @@ -1,240 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react' -import useSWR, { useSWRConfig } from 'swr' -import fetcher from '../fetcher' -import { destroy } from '@rails/request.js' -import userIcon from '../user-icon.js' -import Pagination from './Pagination' -import usePage from './hooks/usePage' -import { formatDateToJapanese } from '../dateFormatter' - -export default function Bookmarks() { - const [editable, setEditable] = useState(false) - const per = 20 - const { page, setPage } = usePage() - const bookmarksUrl = `/api/bookmarks.json?page=${page}&per=${per}` - - const { data, error } = useSWR(bookmarksUrl, fetcher) - if (error) return <>エラーが発生しました。 - if (!data) return <>ロード中… - - if (data.totalPages === 0) { - return - } else { - return ( -
-
-
-
-
-
-

ブックマーク

-
-
-
-
-
-
- -
-
-
-
-
-
-
-
- {/* .page-main-header */} -
-
-
- {data.totalPages > 1 && ( - - )} -
-
- {data.bookmarks.map((bookmark) => { - return ( - - ) - })} -
-
- {data.totalPages > 1 && ( - - )} -
- {/* .container */} -
- {/* .page-body */} -
- {/* .page-main */} -
- ) - } -} - -const NoBookmarks = () => { - return ( -
-
-
-
-
-

ブックマーク

-
-
-
-
- {/* .page-main-header */} -
-
-
-
- -

- ブックマークはまだありません。 -

-
-
-
-
- ) -} - -const EditButton = ({ editable, setEditable }) => { - return ( - <> - - - - ) -} - -const Bookmark = ({ bookmark, editable, bookmarksUrl }) => { - // userIconの非React化により、useRef,useEffectを導入している。 - const userIconRef = useRef(null) - useEffect(() => { - const linkClass = 'card-list-item__user-link' - const imgClasses = ['card-list-item__user-icon', 'a-user-icon'] - - const userIconElement = userIcon({ - user: bookmark.user, - linkClass, - imgClasses - }) - - if (userIconRef.current) { - userIconRef.current.innerHTML = '' - userIconRef.current.appendChild(userIconElement) - } - }, [bookmark.user]) - - const date = bookmark.reported_on || bookmark.created_at - const createdAt = formatDateToJapanese(date) - const { mutate } = useSWRConfig() - const afterDelete = async (id) => { - try { - const response = await destroy(`/api/bookmarks/${id}.json`) - if (response.ok) { - mutate(bookmarksUrl) - } else { - console.warn('削除に失敗しました。') - } - } catch (error) { - console.warn(error) - } - } - - return ( -
-
- {bookmark.modelName === 'Talk' ? ( -
- ) : ( -
{bookmark.modelNameI18n}
- )} -
- - {bookmark.modelName !== 'Talk' && ( -
-
-
-

{bookmark.summary}

-
-
- -
- )} -
- {editable && ( - - )} -
-
- ) -} - -const DeleteButton = ({ id, afterDelete }) => { - return ( -
-
afterDelete(id)}> - 削除 -
-
- ) -} diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 42c89b21ff5..c8de1feb0c6 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -44,7 +44,7 @@ import '../learning-completion-message.js' import '../choices-ui.js' import '../training-info-toggler.js' import '../welcome_message_for_adviser.js' -import '../bookmarks-edit-button.js' +import '../bookmarks.js' import '../hibernation_agreements.js' import '../current-date-time-setter.js' import '../modal-switcher.js' diff --git a/app/views/current_user/bookmarks/_empty.html.slim b/app/views/current_user/bookmarks/_empty.html.slim new file mode 100644 index 00000000000..2cf4dcbc87b --- /dev/null +++ b/app/views/current_user/bookmarks/_empty.html.slim @@ -0,0 +1,11 @@ +.page-main-header + .container + .page-main-header__inner + .page-main-header__start + h1.page-main-header__title ブックマーク +hr.a-border +.page-body + .o-empty-message + .o-empty-message__icon + i.fa-regular.fa-face-sad-tear + p.o-empty-message__text ブックマークはまだありません。 diff --git a/app/views/current_user/bookmarks/_list.html.slim b/app/views/current_user/bookmarks/_list.html.slim new file mode 100644 index 00000000000..6d6d8a817c8 --- /dev/null +++ b/app/views/current_user/bookmarks/_list.html.slim @@ -0,0 +1,59 @@ +.page-main-header + .container + .page-main-header__inner + .page-main-header__start + h1.page-main-header__title ブックマーク + .page-main-header__end + .page-header-actions + .page-header-actions__items + .page-header-actions__item.js-bookmark-edit-toggle + .form-item.is-inline + label.a-form-label for="card-list-tools__action" + | 編集 + label.a-on-off-checkbox.is-sm + input#bookmark_edit[name="card-list-tools__action" type="checkbox"] + span#spec-edit-mode +hr.a-border +.page-body + .container.is-md + - if bookmarks.total_pages > 1 + = paginate bookmarks + + .card-list.a-card + .card-list__items + - bookmarks.each do |bookmark| + .card-list-item class="is-#{bookmark.bookmarkable_type.downcase}" id="bookmark-#{bookmark.id}" + .card-list-item__inner + - if bookmark.bookmarkable_type == 'Talk' + = image_tag bookmark.bookmarkable.user.avatar_url, class: 'card-list-item__user-icon a-user-icon' + - else + .card-list-item__label = bookmark.bookmarkable.model_name.human + + .card-list-item__rows + .card-list-item__row + .card-list-item-title + .card-list-item-title__title + = link_to polymorphic_path(bookmark.bookmarkable), class: 'card-list-item-title__link a-text-link' do + = bookmark.bookmarkable_type == 'Talk' ? "#{bookmark.bookmarkable.user.long_name} さんの相談部屋" : bookmark.bookmarkable.title + + - if bookmark.bookmarkable_type != 'Talk' + .card-list-item__row + .card-list-item__summary + p = truncate(search_summary(bookmark.bookmarkable, ''), length: 100) + + .card-list-item__row + .card-list-item-meta + .card-list-item-meta__item + = link_to bookmark.bookmarkable.user.url, class: 'a-user-name' do + = "#{bookmark.bookmarkable.user.login_name}(#{bookmark.bookmarkable.user.name_kana})" + .card-list-item-meta__item + - date = bookmark.bookmarkable_type == 'Report' ? bookmark.bookmarkable.reported_on.to_time : bookmark.bookmarkable.created_at + time.a-meta datetime=date + = l(date, format: :default) + + .card-list-item__option + .js-bookmark-delete-button + button.bookmark-delete-button.a-bookmark-button.a-button.is-sm.is-block.is-main[type="button" data-url=current_user_bookmark_path(bookmark)] 削除 + + - if bookmarks.total_pages > 1 + = paginate bookmarks diff --git a/app/views/current_user/bookmarks/index.html.slim b/app/views/current_user/bookmarks/index.html.slim index e054472fc3c..94670f816d6 100644 --- a/app/views/current_user/bookmarks/index.html.slim +++ b/app/views/current_user/bookmarks/index.html.slim @@ -4,4 +4,8 @@ = render 'home/page_header' = dashboard_page_tabs(active_tab: 'ブックマーク') -= react_component 'Bookmarks' +.page-main + - if @bookmarks.empty? + = render 'current_user/bookmarks/empty' + - else + = render 'current_user/bookmarks/list', bookmarks: @bookmarks diff --git a/config/routes/current_user.rb b/config/routes/current_user.rb index dda7d75dc03..ac7e86d9f7e 100644 --- a/config/routes/current_user.rb +++ b/config/routes/current_user.rb @@ -5,6 +5,6 @@ resources :reports, only: %i(index) resources :products, only: %i(index) resources :watches, only: %i(index) - resources :bookmarks, only: %i(index) + resources :bookmarks, only: %i(index destroy) end end diff --git a/test/system/bookmark/talks_test.rb b/test/system/bookmark/talks_test.rb index 43aa50aee15..69de2e18c2f 100644 --- a/test/system/bookmark/talks_test.rb +++ b/test/system/bookmark/talks_test.rb @@ -11,6 +11,8 @@ class Bookmark::TalkTest < ApplicationSystemTestCase test 'show talk bookmark on lists' do visit_with_auth '/current_user/bookmarks', 'komagata' + find_all('a.pagination__item-link', text: '2').first.click if !page.has_text?("#{@decorated_user.long_name} さんの相談部屋") + assert_text "#{@decorated_user.long_name} さんの相談部屋" end diff --git a/test/system/current_user/bookmarks_test.rb b/test/system/current_user/bookmarks_test.rb index e8b56d01d97..b702c462b1d 100644 --- a/test/system/current_user/bookmarks_test.rb +++ b/test/system/current_user/bookmarks_test.rb @@ -26,7 +26,7 @@ class CurrentUser::BookmarksTest < ApplicationSystemTestCase visit_with_auth '/current_user/bookmarks', 'kimura' assert_selector 'label', text: '編集' - assert_selector 'input#card-list-tools__action', visible: false + assert_selector 'input#bookmark_edit', visible: false assert_text '作業週1日目' assert_selector '.card-list-item__label', text: '日報' @@ -104,8 +104,7 @@ class CurrentUser::BookmarksTest < ApplicationSystemTestCase user_with_some_bookmarks.bookmarks.create!(bookmarkable_id: reports("report#{n}".to_sym).id, bookmarkable_type: 'Report') end visit_with_auth '/current_user/bookmarks', user_with_some_bookmarks.login_name - # ページ遷移直後なのでreactコンポーネントが表示されるまで待つ - within "[data-testid='bookmarks']" do + within '.page-main' do assert_no_selector 'nav.pagination' end end @@ -127,8 +126,7 @@ class CurrentUser::BookmarksTest < ApplicationSystemTestCase user_with_many_bookmarks.bookmarks.create!(bookmarkable_id: reports("report#{n}".to_sym).id, bookmarkable_type: 'Report') end visit_with_auth '/current_user/bookmarks', user_with_many_bookmarks.login_name - # ページ遷移直後なのでreactコンポーネントが表示されるまで待つ - within "[data-testid='bookmarks']" do + within '.page-main' do assert_selector 'nav.pagination', count: 2 end