BookmarkFolderItem.tsx 12 KB

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