BookmarkFolderItem.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. import {
  2. FC, useCallback, useEffect, useState,
  3. } from 'react';
  4. import { useTranslation } from 'next-i18next';
  5. import { DropdownToggle } from 'reactstrap';
  6. import { toastError, toastSuccess } from '~/client/util/apiNotification';
  7. import { apiv3Delete, apiv3Post, apiv3Put } from '~/client/util/apiv3-client';
  8. import CountBadge from '~/components/Common/CountBadge';
  9. import FolderIcon from '~/components/Icons/FolderIcon';
  10. import TriangleIcon from '~/components/Icons/TriangleIcon';
  11. import { BookmarkFolderItems } from '~/interfaces/bookmark-info';
  12. import { useSWRxBookamrkFolderAndChild } from '~/stores/bookmark-folder';
  13. import BookmarkFolderItemControl from './BookmarkFolderItemControl';
  14. import BookmarkFolderNameInput from './BookmarkFolderNameInput';
  15. import DeleteBookmarkFolderModal from './DeleteBookmarkFolderModal';
  16. type BookmarkFolderItemProps = {
  17. bookmarkFolder: BookmarkFolderItems
  18. isOpen?: boolean
  19. }
  20. const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderItemProps) => {
  21. const { bookmarkFolder, isOpen: _isOpen = false } = props;
  22. const { t } = useTranslation();
  23. const {
  24. name, _id: folderId, children, parent,
  25. } = bookmarkFolder;
  26. const [currentChildren, setCurrentChildren] = useState<BookmarkFolderItems[]>();
  27. const [targetFolder, setTargetFolder] = useState<string | null>(folderId);
  28. const [isOpen, setIsOpen] = useState(_isOpen);
  29. const { data: childBookmarkFolderData, mutate: mutateChildBookmarkData } = useSWRxBookamrkFolderAndChild(targetFolder);
  30. const { mutate: mutateParentBookmarkFolder } = useSWRxBookamrkFolderAndChild(parent);
  31. const [isRenameAction, setIsRenameAction] = useState<boolean>(false);
  32. const [isCreateAction, setIsCreateAction] = useState<boolean>(false);
  33. const [isDeleteFolderModalShown, setIsDeleteFolderModalShown] = useState<boolean>(false);
  34. const getChildCount = useCallback((): number => {
  35. if (currentChildren != null && currentChildren.length > children.length) {
  36. return currentChildren.length;
  37. }
  38. return children.length;
  39. }, [children.length, currentChildren]);
  40. useEffect(() => {
  41. if (childBookmarkFolderData != null) {
  42. mutateChildBookmarkData();
  43. setCurrentChildren(childBookmarkFolderData);
  44. }
  45. }, [childBookmarkFolderData, mutateChildBookmarkData]);
  46. const hasChildren = useCallback((): boolean => {
  47. if (currentChildren != null && currentChildren.length > children.length) {
  48. return currentChildren.length > 0;
  49. }
  50. return children.length > 0;
  51. }, [children.length, currentChildren]);
  52. const loadChildFolder = useCallback(async() => {
  53. setIsOpen(!isOpen);
  54. setTargetFolder(folderId);
  55. }, [folderId, isOpen]);
  56. const loadParent = useCallback(async() => {
  57. if (!isRenameAction) {
  58. if (parent != null) {
  59. await mutateParentBookmarkFolder();
  60. }
  61. // Reload root folder structure
  62. setTargetFolder(null);
  63. }
  64. else {
  65. await mutateParentBookmarkFolder();
  66. }
  67. }, [isRenameAction, mutateParentBookmarkFolder, parent]);
  68. // Rename for bookmark folder handler
  69. const onPressEnterHandlerForRename = useCallback(async(folderName: string) => {
  70. try {
  71. await apiv3Put('/bookmark-folder', { bookmarkFolderId: folderId, name: folderName, parent });
  72. loadParent();
  73. setIsRenameAction(false);
  74. toastSuccess(t('toaster.update_successed', { target: t('bookmark_folder.bookmark_folder') }));
  75. }
  76. catch (err) {
  77. toastError(err);
  78. }
  79. }, [folderId, loadParent, parent, t]);
  80. // Create new folder / subfolder handler
  81. const onPressEnterHandlerForCreate = useCallback(async(folderName: string) => {
  82. try {
  83. await apiv3Post('/bookmark-folder', { name: folderName, parent: targetFolder });
  84. setIsOpen(true);
  85. setIsCreateAction(false);
  86. mutateChildBookmarkData();
  87. toastSuccess(t('toaster.create_succeeded', { target: t('bookmark_folder.bookmark_folder') }));
  88. }
  89. catch (err) {
  90. toastError(err);
  91. }
  92. }, [mutateChildBookmarkData, t, targetFolder]);
  93. // Delete Fodler handler
  94. const onClickDeleteButtonHandler = useCallback(async() => {
  95. try {
  96. await apiv3Delete(`/bookmark-folder/${folderId}`);
  97. setIsDeleteFolderModalShown(false);
  98. loadParent();
  99. toastSuccess(t('toaster.delete_succeeded', { target: t('bookmark_folder.bookmark_folder') }));
  100. }
  101. catch (err) {
  102. toastError(err);
  103. }
  104. }, [folderId, loadParent, t]);
  105. const onClickPlusButton = useCallback(async(e) => {
  106. e.stopPropagation();
  107. if (!isOpen && hasChildren()) {
  108. setIsOpen(true);
  109. }
  110. setIsCreateAction(true);
  111. }, [hasChildren, isOpen]);
  112. const renderChildFolder = () => {
  113. return isOpen && currentChildren?.map((childFolder) => {
  114. return (
  115. <div key={childFolder._id} className="grw-foldertree-item-children">
  116. <BookmarkFolderItem
  117. key={childFolder._id}
  118. bookmarkFolder={childFolder}
  119. />
  120. </div>
  121. );
  122. });
  123. };
  124. const onClickRenameHandler = useCallback(() => {
  125. setIsRenameAction(true);
  126. }, []);
  127. const onClickDeleteHandler = useCallback(() => {
  128. setIsDeleteFolderModalShown(true);
  129. }, []);
  130. const onDeleteFolderModalClose = useCallback(() => {
  131. setIsDeleteFolderModalShown(false);
  132. }, []);
  133. return (
  134. <div id={`grw-bookmark-folder-item-${folderId}`} className="grw-foldertree-item-container">
  135. <li className="list-group-item list-group-item-action border-0 py-0 pr-3 d-flex align-items-center" onClick={loadChildFolder}>
  136. <div className="grw-triangle-container d-flex justify-content-center">
  137. {hasChildren() && (
  138. <button
  139. type="button"
  140. className={`grw-foldertree-triangle-btn btn ${isOpen ? 'grw-foldertree-open' : ''}`}
  141. onClick={loadChildFolder}
  142. >
  143. <div className="d-flex justify-content-center">
  144. <TriangleIcon />
  145. </div>
  146. </button>
  147. )}
  148. </div>
  149. {
  150. <div>
  151. <FolderIcon isOpen={isOpen} />
  152. </div>
  153. }
  154. { isRenameAction ? (
  155. <BookmarkFolderNameInput
  156. onClickOutside={() => setIsRenameAction(false)}
  157. onPressEnter={onPressEnterHandlerForRename}
  158. value={name}
  159. />
  160. ) : (
  161. <>
  162. <div className='grw-foldertree-title-anchor pl-2' >
  163. <p className={'text-truncate m-auto '}>{name}</p>
  164. </div>
  165. {hasChildren() && (
  166. <div className="grw-foldertree-count-wrapper">
  167. <CountBadge count={ getChildCount() } />
  168. </div>
  169. )}
  170. </>
  171. )
  172. }
  173. <div className="grw-foldertree-control d-flex">
  174. <BookmarkFolderItemControl
  175. onClickRename={onClickRenameHandler}
  176. onClickDelete={onClickDeleteHandler}
  177. >
  178. <div onClick={e => e.stopPropagation()}>
  179. <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control p-0 grw-visible-on-hover mr-1">
  180. <i className="icon-options fa fa-rotate-90 p-1"></i>
  181. </DropdownToggle>
  182. </div>
  183. </BookmarkFolderItemControl>
  184. <button
  185. type="button"
  186. className="border-0 rounded btn btn-page-item-control p-0 grw-visible-on-hover"
  187. onClick={onClickPlusButton}
  188. >
  189. <i className="icon-plus d-block p-0" />
  190. </button>
  191. </div>
  192. </li>
  193. {isCreateAction && (
  194. <div className="flex-fill">
  195. <BookmarkFolderNameInput
  196. onClickOutside={() => setIsCreateAction(false)}
  197. onPressEnter={onPressEnterHandlerForCreate}
  198. />
  199. </div>
  200. )}
  201. {
  202. renderChildFolder()
  203. }
  204. <DeleteBookmarkFolderModal
  205. bookmarkFolder={bookmarkFolder}
  206. isOpen={isDeleteFolderModalShown}
  207. onClickDeleteButton={onClickDeleteButtonHandler}
  208. onModalClose={onDeleteFolderModalClose}/>
  209. </div>
  210. );
  211. };
  212. export default BookmarkFolderItem;