BookmarkFolderItem.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  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'), ns: 'commons' }));
  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(t('toaster.add_successed', { target: t('bookmark_folder.bookmark'), ns: 'commons' }));
  177. }
  178. catch (err) {
  179. toastError(err);
  180. }
  181. }, [bookmarkFolder._id, mutateParentBookmarkFolder, t]);
  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. return item.root !== targetRoot || item.level >= targetLevel;
  187. };
  188. const [, bookmarkFolderDropRef] = useDrop(() => ({
  189. accept: 'FOLDER',
  190. drop: folderItemDropHandler,
  191. canDrop: (item) => {
  192. // Implement isDropable function & improve
  193. return isDroppable(item, root, level);
  194. },
  195. collect: monitor => ({
  196. isOver: monitor.isOver(),
  197. }),
  198. }));
  199. const [, bookmarkItemDropRef] = useDrop(() => ({
  200. accept: 'BOOKMARK',
  201. drop: bookmarkItemDropHandler,
  202. collect: monitor => ({
  203. isOver: monitor.isOver(),
  204. }),
  205. }));
  206. const renderChildFolder = () => {
  207. return isOpen && currentChildren?.map((childFolder) => {
  208. return (
  209. <div key={childFolder._id} className="grw-foldertree-item-children">
  210. <BookmarkFolderItem
  211. key={childFolder._id}
  212. bookmarkFolder={childFolder}
  213. level={level + 1}
  214. root={root}
  215. />
  216. </div>
  217. );
  218. });
  219. };
  220. const renderBookmarkItem = () => {
  221. return isOpen && bookmarks?.map((bookmark) => {
  222. return (
  223. <BookmarkItem
  224. bookmarkedPage={bookmark.page}
  225. key={bookmark._id}
  226. onUnbookmarked={onUnbookmarkHandler}
  227. onRenamed={mutateParentBookmarkFolder}
  228. onClickDeleteMenuItem={onClickDeleteBookmarkHandler}
  229. parentFolder={bookmarkFolder}
  230. />
  231. );
  232. });
  233. };
  234. const onClickRenameHandler = useCallback(() => {
  235. setIsRenameAction(true);
  236. }, []);
  237. const onClickDeleteHandler = useCallback(() => {
  238. setIsDeleteFolderModalShown(true);
  239. }, []);
  240. const onDeleteFolderModalClose = useCallback(() => {
  241. setIsDeleteFolderModalShown(false);
  242. }, []);
  243. return (
  244. <div id={`grw-bookmark-folder-item-${folderId}`} className="grw-foldertree-item-container">
  245. <li ref={(c) => { bookmarkFolderDragRef(c); bookmarkFolderDropRef(c); bookmarkItemDropRef(c) }}
  246. className="list-group-item list-group-item-action border-0 py-0 pr-3 d-flex align-items-center"
  247. onClick={loadChildFolder}
  248. >
  249. <div className="grw-triangle-container d-flex justify-content-center">
  250. {hasChildren() && (
  251. <button
  252. type="button"
  253. className={`grw-foldertree-triangle-btn btn ${isOpen ? 'grw-foldertree-open' : ''}`}
  254. onClick={loadChildFolder}
  255. >
  256. <div className="d-flex justify-content-center">
  257. <TriangleIcon />
  258. </div>
  259. </button>
  260. )}
  261. </div>
  262. {
  263. <div>
  264. <FolderIcon isOpen={isOpen} />
  265. </div>
  266. }
  267. {isRenameAction ? (
  268. <BookmarkFolderNameInput
  269. onClickOutside={() => setIsRenameAction(false)}
  270. onPressEnter={onPressEnterHandlerForRename}
  271. value={name}
  272. />
  273. ) : (
  274. <>
  275. <div className='grw-foldertree-title-anchor pl-2' >
  276. <p className={'text-truncate m-auto '}>{name}</p>
  277. </div>
  278. {hasChildren() && (
  279. <div className="grw-foldertree-count-wrapper">
  280. <CountBadge count={childCount} />
  281. </div>
  282. )}
  283. </>
  284. )
  285. }
  286. <div className="grw-foldertree-control d-flex">
  287. <BookmarkFolderItemControl
  288. onClickRename={onClickRenameHandler}
  289. onClickDelete={onClickDeleteHandler}
  290. >
  291. <div onClick={e => e.stopPropagation()}>
  292. <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control p-0 grw-visible-on-hover mr-1">
  293. <i className="icon-options fa fa-rotate-90 p-1"></i>
  294. </DropdownToggle>
  295. </div>
  296. </BookmarkFolderItemControl>
  297. <button
  298. type="button"
  299. className="border-0 rounded btn btn-page-item-control p-0 grw-visible-on-hover"
  300. onClick={onClickPlusButton}
  301. >
  302. <i className="icon-plus d-block p-0" />
  303. </button>
  304. </div>
  305. </li>
  306. {isCreateAction && (
  307. <div className="flex-fill">
  308. <BookmarkFolderNameInput
  309. onClickOutside={() => setIsCreateAction(false)}
  310. onPressEnter={onPressEnterHandlerForCreate}
  311. />
  312. </div>
  313. )}
  314. {
  315. renderChildFolder()
  316. }
  317. {
  318. renderBookmarkItem()
  319. }
  320. <DeleteBookmarkFolderModal
  321. bookmarkFolder={bookmarkFolder}
  322. isOpen={isDeleteFolderModalShown}
  323. onClickDeleteButton={onClickDeleteButtonHandler}
  324. onModalClose={onDeleteFolderModalClose} />
  325. </div>
  326. );
  327. };
  328. export default BookmarkFolderItem;