BookmarkList.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. import React, { useCallback, useState, useMemo } from 'react';
  2. import nodePath from 'path';
  3. import {
  4. IPageInfoAll, IPageToDeleteWithMeta, pathUtils,
  5. } from '@growi/core';
  6. import { useTranslation } from 'next-i18next';
  7. import { DropdownItem, DropdownToggle } from 'reactstrap';
  8. import { unbookmark } from '~/client/services/page-operation';
  9. import { apiv3Put } from '~/client/util/apiv3-client';
  10. import { addBookmarkToFolder } from '~/client/util/bookmark-utils';
  11. import { ValidationTarget } from '~/client/util/input-validator';
  12. import { toastError, toastSuccess } from '~/client/util/toastr';
  13. import { IPageHasId } from '~/interfaces/page';
  14. import { useSWRxCurrentUserBookmarks } from '~/stores/bookmark';
  15. import loggerFactory from '~/utils/logger';
  16. import ClosableTextInput from '../Common/ClosableTextInput';
  17. import { MenuItemType, PageItemControl } from '../Common/Dropdown/PageItemControl';
  18. import { PageListItemS } from './PageListItemS';
  19. const logger = loggerFactory('growi:BookmarkList');
  20. type Props = {
  21. page: IPageHasId
  22. onRenamed: () => void
  23. onUnbookmarked: () => void
  24. onClickDeleteMenuItem: (pageToDelete: IPageToDeleteWithMeta) => void
  25. }
  26. export const BookmarkList = (props:Props): JSX.Element => {
  27. const {
  28. page, onRenamed, onUnbookmarked, onClickDeleteMenuItem,
  29. } = props;
  30. const { t } = useTranslation();
  31. const [isRenameInputShown, setIsRenameInputShown] = useState(false);
  32. const pageId = page._id;
  33. const { data: userBookmarks, mutate: mutateUserBookmarks } = useSWRxCurrentUserBookmarks();
  34. const isMoveToRoot = useMemo(() => {
  35. return !userBookmarks?.map(userBookmark => userBookmark._id).includes(pageId);
  36. }, [pageId, userBookmarks]);
  37. const moveToRootClickedHandler = useCallback(async() => {
  38. try {
  39. await addBookmarkToFolder(pageId, null);
  40. await mutateUserBookmarks();
  41. }
  42. catch (err) {
  43. toastError(err);
  44. }
  45. }, [mutateUserBookmarks, pageId]);
  46. const additionalMenuItemOnTopRenderer = useMemo(() => {
  47. return (
  48. <DropdownItem
  49. onClick={moveToRootClickedHandler}
  50. className="grw-page-control-dropdown-item"
  51. data-testid="add-remove-bookmark-btn"
  52. >
  53. <i className="fa fa-fw fa-bookmark-o grw-page-control-dropdown-icon"></i>
  54. {t('bookmark_folder.move_to_root')}
  55. </DropdownItem>
  56. );
  57. }, [moveToRootClickedHandler, t]);
  58. const bookmarkMenuItemClickHandler = useCallback(async() => {
  59. await unbookmark(page._id);
  60. onUnbookmarked();
  61. }, [page._id, onUnbookmarked]);
  62. const deleteMenuItemClickHandler = useCallback(async(_pageId: string, pageInfo: IPageInfoAll | undefined): Promise<void> => {
  63. if (page._id == null || page.path == null) {
  64. throw Error('_id and path must not be null.');
  65. }
  66. const pageToDelete: IPageToDeleteWithMeta = {
  67. data: {
  68. _id: page._id,
  69. revision: page.revision as string,
  70. path: page.path,
  71. },
  72. meta: pageInfo,
  73. };
  74. onClickDeleteMenuItem(pageToDelete);
  75. }, [onClickDeleteMenuItem, page]);
  76. const pressEnterForRenameHandler = useCallback(async(inputText: string) => {
  77. const parentPath = pathUtils.addTrailingSlash(nodePath.dirname(page.path ?? ''));
  78. const newPagePath = nodePath.resolve(parentPath, inputText);
  79. if (newPagePath === page.path) {
  80. setIsRenameInputShown(false);
  81. return;
  82. }
  83. try {
  84. setIsRenameInputShown(false);
  85. await apiv3Put('/pages/rename', {
  86. pageId: page._id,
  87. revisionId: page.revision,
  88. newPagePath,
  89. });
  90. onRenamed();
  91. toastSuccess(t('renamed_pages', { path: page.path }));
  92. }
  93. catch (err) {
  94. setIsRenameInputShown(true);
  95. logger.error('failed to fetch data', err);
  96. toastError(err);
  97. }
  98. }, [onRenamed, page, t]);
  99. return (
  100. <li key={`my-bookmarks:${page?._id}`} className="list-group-item list-group-item-action border-0 py-0 pl-3 d-flex align-items-center">
  101. { isRenameInputShown ? (
  102. <ClosableTextInput
  103. value={nodePath.basename(page.path ?? '')}
  104. placeholder={t('Input page name')}
  105. onClickOutside={() => { setIsRenameInputShown(false) }}
  106. onPressEnter={pressEnterForRenameHandler}
  107. validationTarget={ValidationTarget.PAGE}
  108. />
  109. ) : (
  110. <PageListItemS page={page} />
  111. )}
  112. <PageItemControl
  113. pageId={page._id}
  114. isEnableActions
  115. forceHideMenuItems={[MenuItemType.DUPLICATE]}
  116. onClickBookmarkMenuItem={bookmarkMenuItemClickHandler}
  117. onClickRenameMenuItem={() => setIsRenameInputShown(true)}
  118. onClickDeleteMenuItem={deleteMenuItemClickHandler}
  119. additionalMenuItemOnTopRenderer={isMoveToRoot ? (() => additionalMenuItemOnTopRenderer) : undefined}
  120. >
  121. <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control p-0 grw-visible-on-hover mr-1">
  122. <i className="icon-options fa fa-rotate-90 p-1"></i>
  123. </DropdownToggle>
  124. </PageItemControl>
  125. </li>
  126. );
  127. };