BookmarkFolderItem.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. import {
  2. FC, useCallback, useEffect, useState, useMemo,
  3. } from 'react';
  4. import { useTranslation } from 'next-i18next';
  5. import { useDrag, useDrop } from 'react-dnd';
  6. import { DropdownToggle } from 'reactstrap';
  7. import { toastError, toastSuccess } from '~/client/util/apiNotification';
  8. import {
  9. apiv3Delete, apiv3Post, apiv3Put,
  10. } from '~/client/util/apiv3-client';
  11. import CountBadge from '~/components/Common/CountBadge';
  12. import FolderIcon from '~/components/Icons/FolderIcon';
  13. import TriangleIcon from '~/components/Icons/TriangleIcon';
  14. import { BookmarkFolderItems } from '~/interfaces/bookmark-info';
  15. import { IPageToDeleteWithMeta } from '~/interfaces/page';
  16. import { OnDeletedFunction } from '~/interfaces/ui';
  17. import { useSWRBookmarkInfo } from '~/stores/bookmark';
  18. import { useSWRxBookamrkFolderAndChild } from '~/stores/bookmark-folder';
  19. import { usePageDeleteModal } from '~/stores/modal';
  20. import { useSWRxCurrentPage } from '~/stores/page';
  21. import BookmarkFolderItemControl from './BookmarkFolderItemControl';
  22. import BookmarkFolderNameInput from './BookmarkFolderNameInput';
  23. import BookmarkItem from './BookmarkItem';
  24. import DeleteBookmarkFolderModal from './DeleteBookmarkFolderModal';
  25. type BookmarkFolderItemProps = {
  26. bookmarkFolder: BookmarkFolderItems
  27. isOpen?: boolean
  28. level: number
  29. root: string
  30. }
  31. const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderItemProps) => {
  32. const {
  33. bookmarkFolder, isOpen: _isOpen = false, level, root,
  34. } = props;
  35. const { t } = useTranslation();
  36. const {
  37. name, _id: folderId, children, parent, bookmarks,
  38. } = bookmarkFolder;
  39. const [currentChildren, setCurrentChildren] = useState<BookmarkFolderItems[]>();
  40. const [targetFolder, setTargetFolder] = useState<string | null>(folderId);
  41. const [isOpen, setIsOpen] = useState(_isOpen);
  42. const { data: childBookmarkFolderData, mutate: mutateChildBookmarkData } = useSWRxBookamrkFolderAndChild(targetFolder);
  43. const { mutate: mutateParentBookmarkFolder } = useSWRxBookamrkFolderAndChild(parent);
  44. const [isRenameAction, setIsRenameAction] = useState<boolean>(false);
  45. const [isCreateAction, setIsCreateAction] = useState<boolean>(false);
  46. const [isDeleteFolderModalShown, setIsDeleteFolderModalShown] = useState<boolean>(false);
  47. const { data: currentPage } = useSWRxCurrentPage();
  48. const { mutate: mutateBookmarkInfo } = useSWRBookmarkInfo(currentPage?._id);
  49. const { open: openDeleteModal } = usePageDeleteModal();
  50. const childCount = useMemo((): number => {
  51. if (currentChildren != null && currentChildren.length > children.length) {
  52. return currentChildren.length;
  53. }
  54. return children.length;
  55. }, [children.length, currentChildren]);
  56. useEffect(() => {
  57. if (childBookmarkFolderData != null) {
  58. mutateChildBookmarkData();
  59. setCurrentChildren(childBookmarkFolderData);
  60. }
  61. }, [childBookmarkFolderData, mutateChildBookmarkData]);
  62. const hasChildren = useCallback((): boolean => {
  63. if (currentChildren != null && currentChildren.length > children.length) {
  64. return currentChildren.length > 0;
  65. }
  66. return children.length > 0;
  67. }, [children.length, currentChildren]);
  68. const loadChildFolder = useCallback(async() => {
  69. setIsOpen(!isOpen);
  70. setTargetFolder(folderId);
  71. }, [folderId, isOpen]);
  72. const loadParent = useCallback(async() => {
  73. if (!isRenameAction) {
  74. if (parent != null) {
  75. await mutateParentBookmarkFolder();
  76. }
  77. // Reload root folder structure
  78. setTargetFolder(null);
  79. }
  80. else {
  81. await mutateParentBookmarkFolder();
  82. }
  83. }, [isRenameAction, mutateParentBookmarkFolder, parent]);
  84. // Rename for bookmark folder handler
  85. const onPressEnterHandlerForRename = useCallback(async(folderName: string) => {
  86. try {
  87. await apiv3Put('/bookmark-folder', { bookmarkFolderId: folderId, name: folderName, parent });
  88. loadParent();
  89. setIsRenameAction(false);
  90. toastSuccess(t('toaster.update_successed', { target: t('bookmark_folder.bookmark_folder') }));
  91. }
  92. catch (err) {
  93. toastError(err);
  94. }
  95. }, [folderId, loadParent, parent, t]);
  96. // Create new folder / subfolder handler
  97. const onPressEnterHandlerForCreate = useCallback(async(folderName: string) => {
  98. try {
  99. await apiv3Post('/bookmark-folder', { name: folderName, parent: targetFolder });
  100. setIsOpen(true);
  101. setIsCreateAction(false);
  102. mutateChildBookmarkData();
  103. toastSuccess(t('toaster.create_succeeded', { target: t('bookmark_folder.bookmark_folder') }));
  104. }
  105. catch (err) {
  106. toastError(err);
  107. }
  108. }, [mutateChildBookmarkData, t, targetFolder]);
  109. // Delete Fodler handler
  110. const onClickDeleteButtonHandler = useCallback(async() => {
  111. try {
  112. await apiv3Delete(`/bookmark-folder/${folderId}`);
  113. setIsDeleteFolderModalShown(false);
  114. loadParent();
  115. mutateBookmarkInfo();
  116. toastSuccess(t('toaster.delete_succeeded', { target: t('bookmark_folder.bookmark_folder') }));
  117. }
  118. catch (err) {
  119. toastError(err);
  120. }
  121. }, [folderId, loadParent, mutateBookmarkInfo, t]);
  122. const onClickPlusButton = useCallback(async(e) => {
  123. e.stopPropagation();
  124. if (!isOpen && hasChildren()) {
  125. setIsOpen(true);
  126. }
  127. setIsCreateAction(true);
  128. }, [hasChildren, isOpen]);
  129. const onClickDeleteBookmarkHandler = useCallback((pageToDelete: IPageToDeleteWithMeta) => {
  130. const pageDeletedHandler : OnDeletedFunction = (pathOrPathsToDelete, _isRecursively, isCompletely) => {
  131. if (typeof pathOrPathsToDelete !== 'string') {
  132. return;
  133. }
  134. const path = pathOrPathsToDelete;
  135. if (isCompletely) {
  136. toastSuccess(t('deleted_pages_completely', { path }));
  137. }
  138. else {
  139. toastSuccess(t('deleted_pages', { path }));
  140. }
  141. mutateParentBookmarkFolder();
  142. mutateBookmarkInfo();
  143. };
  144. openDeleteModal([pageToDelete], { onDeleted: pageDeletedHandler });
  145. }, [mutateBookmarkInfo, mutateParentBookmarkFolder, openDeleteModal, t]);
  146. const onUnbookmarkHandler = useCallback(() => {
  147. mutateParentBookmarkFolder();
  148. mutateBookmarkInfo();
  149. }, [mutateBookmarkInfo, mutateParentBookmarkFolder]);
  150. const [, bookmarkFolderDragRef] = useDrag({
  151. type: 'FOLDER',
  152. item: props,
  153. end: (item, monitor) => {
  154. const dropResult = monitor.getDropResult();
  155. if (dropResult != null) {
  156. mutateParentBookmarkFolder();
  157. }
  158. },
  159. collect: monitor => ({
  160. isDragging: monitor.isDragging(),
  161. canDrag: monitor.canDrag(),
  162. }),
  163. });
  164. const folderItemDropHandler = async(item: BookmarkFolderItemProps) => {
  165. try {
  166. await apiv3Put('/bookmark-folder', { bookmarkFolderId: item.bookmarkFolder._id, name: item.bookmarkFolder.name, parent: bookmarkFolder._id });
  167. await mutateChildBookmarkData();
  168. await mutateChildBookmarkData();
  169. toastSuccess(t('toaster.update_successed', { target: t('bookmark_folder.bookmark_folder') }));
  170. }
  171. catch (err) {
  172. toastError(err);
  173. }
  174. };
  175. const isDroppable = (item: BookmarkFolderItemProps, targetRoot: string, targetLevel: number): boolean => {
  176. if (item.bookmarkFolder.parent === bookmarkFolder._id || item.bookmarkFolder._id === bookmarkFolder._id) {
  177. return false;
  178. }
  179. if (item.root === targetRoot) {
  180. if (item.level < targetLevel) {
  181. return false;
  182. }
  183. }
  184. return true;
  185. };
  186. const [, bookmarkFolderDropRef] = useDrop(() => ({
  187. accept: 'FOLDER',
  188. drop: folderItemDropHandler,
  189. canDrop: (item) => {
  190. // Implement isDropable function & improve
  191. return isDroppable(item, root, level);
  192. },
  193. collect: monitor => ({
  194. isOver: monitor.isOver(),
  195. }),
  196. }));
  197. const renderChildFolder = () => {
  198. return isOpen && currentChildren?.map((childFolder) => {
  199. return (
  200. <div key={childFolder._id} className="grw-foldertree-item-children">
  201. <BookmarkFolderItem
  202. key={childFolder._id}
  203. bookmarkFolder={childFolder}
  204. level={level + 1}
  205. root={root}
  206. />
  207. </div>
  208. );
  209. });
  210. };
  211. const renderBookmarkItem = () => {
  212. return isOpen && bookmarks?.map((bookmark) => {
  213. return (
  214. <BookmarkItem
  215. bookmarkedPage={bookmark.page}
  216. key={bookmark._id}
  217. onUnbookmarked={onUnbookmarkHandler}
  218. onRenamed={mutateParentBookmarkFolder}
  219. onClickDeleteMenuItem={onClickDeleteBookmarkHandler}
  220. />
  221. );
  222. });
  223. };
  224. const onClickRenameHandler = useCallback(() => {
  225. setIsRenameAction(true);
  226. }, []);
  227. const onClickDeleteHandler = useCallback(() => {
  228. setIsDeleteFolderModalShown(true);
  229. }, []);
  230. const onDeleteFolderModalClose = useCallback(() => {
  231. setIsDeleteFolderModalShown(false);
  232. }, []);
  233. return (
  234. <div id={`grw-bookmark-folder-item-${folderId}`} className="grw-foldertree-item-container">
  235. <li ref={(c) => { bookmarkFolderDragRef(c); bookmarkFolderDropRef(c) }}
  236. className="list-group-item list-group-item-action border-0 py-0 pr-3 d-flex align-items-center"
  237. onClick={loadChildFolder}
  238. >
  239. <div className="grw-triangle-container d-flex justify-content-center">
  240. {hasChildren() && (
  241. <button
  242. type="button"
  243. className={`grw-foldertree-triangle-btn btn ${isOpen ? 'grw-foldertree-open' : ''}`}
  244. onClick={loadChildFolder}
  245. >
  246. <div className="d-flex justify-content-center">
  247. <TriangleIcon />
  248. </div>
  249. </button>
  250. )}
  251. </div>
  252. {
  253. <div>
  254. <FolderIcon isOpen={isOpen} />
  255. </div>
  256. }
  257. { isRenameAction ? (
  258. <BookmarkFolderNameInput
  259. onClickOutside={() => setIsRenameAction(false)}
  260. onPressEnter={onPressEnterHandlerForRename}
  261. value={name}
  262. />
  263. ) : (
  264. <>
  265. <div className='grw-foldertree-title-anchor pl-2' >
  266. <p className={'text-truncate m-auto '}>{name}</p>
  267. </div>
  268. {hasChildren() && (
  269. <div className="grw-foldertree-count-wrapper">
  270. <CountBadge count={ childCount } />
  271. </div>
  272. )}
  273. </>
  274. )
  275. }
  276. <div className="grw-foldertree-control d-flex">
  277. <BookmarkFolderItemControl
  278. onClickRename={onClickRenameHandler}
  279. onClickDelete={onClickDeleteHandler}
  280. >
  281. <div onClick={e => e.stopPropagation()}>
  282. <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control p-0 grw-visible-on-hover mr-1">
  283. <i className="icon-options fa fa-rotate-90 p-1"></i>
  284. </DropdownToggle>
  285. </div>
  286. </BookmarkFolderItemControl>
  287. <button
  288. type="button"
  289. className="border-0 rounded btn btn-page-item-control p-0 grw-visible-on-hover"
  290. onClick={onClickPlusButton}
  291. >
  292. <i className="icon-plus d-block p-0" />
  293. </button>
  294. </div>
  295. </li>
  296. {isCreateAction && (
  297. <div className="flex-fill">
  298. <BookmarkFolderNameInput
  299. onClickOutside={() => setIsCreateAction(false)}
  300. onPressEnter={onPressEnterHandlerForCreate}
  301. />
  302. </div>
  303. )}
  304. {
  305. renderChildFolder()
  306. }
  307. {
  308. renderBookmarkItem()
  309. }
  310. <DeleteBookmarkFolderModal
  311. bookmarkFolder={bookmarkFolder}
  312. isOpen={isDeleteFolderModalShown}
  313. onClickDeleteButton={onClickDeleteButtonHandler}
  314. onModalClose={onDeleteFolderModalClose}/>
  315. </div>
  316. );
  317. };
  318. export default BookmarkFolderItem;