BookmarkFolderItem.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. import {
  2. FC, useCallback, useState,
  3. } from 'react';
  4. import { useTranslation } from 'next-i18next';
  5. import { DropdownToggle } from 'reactstrap';
  6. import {
  7. addBookmarkToFolder, addNewFolder, hasChildren, updateBookmarkFolder,
  8. } from '~/client/util/bookmark-utils';
  9. import { toastError, toastSuccess } from '~/client/util/toastr';
  10. import { FolderIcon } from '~/components/Icons/FolderIcon';
  11. import { TriangleIcon } from '~/components/Icons/TriangleIcon';
  12. import {
  13. BookmarkFolderItems, DragItemDataType, DragItemType, DRAG_ITEM_TYPE,
  14. } from '~/interfaces/bookmark-info';
  15. import { IPageToDeleteWithMeta } from '~/interfaces/page';
  16. import { onDeletedBookmarkFolderFunction, OnDeletedFunction } from '~/interfaces/ui';
  17. import { useSWRBookmarkInfo, useSWRxCurrentUserBookmarks } from '~/stores/bookmark';
  18. import { useSWRxBookamrkFolderAndChild } from '~/stores/bookmark-folder';
  19. import { useBookmarkFolderDeleteModal, 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 { DragAndDropWrapper } from './DragAndDropWrapper';
  25. type BookmarkFolderItemProps = {
  26. bookmarkFolder: BookmarkFolderItems
  27. isOpen?: boolean
  28. level: number
  29. root: string
  30. isUserHomePage?: boolean
  31. }
  32. export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderItemProps) => {
  33. const BASE_FOLDER_PADDING = 15;
  34. const acceptedTypes: DragItemType[] = [DRAG_ITEM_TYPE.FOLDER, DRAG_ITEM_TYPE.BOOKMARK];
  35. const {
  36. bookmarkFolder, isOpen: _isOpen = false, level, root, isUserHomePage,
  37. } = props;
  38. const { t } = useTranslation();
  39. const {
  40. name, _id: folderId, children, parent, bookmarks,
  41. } = bookmarkFolder;
  42. const [targetFolder, setTargetFolder] = useState<string | null>(folderId);
  43. const [isOpen, setIsOpen] = useState(_isOpen);
  44. const { mutate: mutateBookmarkData } = useSWRxBookamrkFolderAndChild();
  45. const { mutate: mutateUserBookmarks } = useSWRxCurrentUserBookmarks();
  46. const [isRenameAction, setIsRenameAction] = useState<boolean>(false);
  47. const [isCreateAction, setIsCreateAction] = useState<boolean>(false);
  48. const { data: currentPage } = useSWRxCurrentPage();
  49. const { mutate: mutateBookmarkInfo } = useSWRBookmarkInfo(currentPage?._id);
  50. const { open: openDeleteModal } = usePageDeleteModal();
  51. const { open: openDeleteBookmarkFolderModal } = useBookmarkFolderDeleteModal();
  52. const childrenExists = hasChildren(children);
  53. const paddingLeft = BASE_FOLDER_PADDING * level;
  54. const loadChildFolder = useCallback(async() => {
  55. setIsOpen(!isOpen);
  56. setTargetFolder(folderId);
  57. }, [folderId, isOpen]);
  58. // Rename for bookmark folder handler
  59. const onPressEnterHandlerForRename = useCallback(async(folderName: string) => {
  60. try {
  61. await updateBookmarkFolder(folderId, folderName, parent);
  62. mutateBookmarkData();
  63. setIsRenameAction(false);
  64. }
  65. catch (err) {
  66. toastError(err);
  67. }
  68. }, [folderId, mutateBookmarkData, parent]);
  69. // Create new folder / subfolder handler
  70. const onPressEnterHandlerForCreate = useCallback(async(folderName: string) => {
  71. try {
  72. await addNewFolder(folderName, targetFolder);
  73. setIsOpen(true);
  74. setIsCreateAction(false);
  75. mutateBookmarkData();
  76. }
  77. catch (err) {
  78. toastError(err);
  79. }
  80. }, [mutateBookmarkData, targetFolder]);
  81. const onClickPlusButton = useCallback(async(e) => {
  82. e.stopPropagation();
  83. if (!isOpen && childrenExists) {
  84. setIsOpen(true);
  85. }
  86. setIsCreateAction(true);
  87. }, [childrenExists, isOpen]);
  88. const onClickDeleteBookmarkHandler = useCallback((pageToDelete: IPageToDeleteWithMeta) => {
  89. const pageDeletedHandler: OnDeletedFunction = (pathOrPathsToDelete, _isRecursively, isCompletely) => {
  90. if (typeof pathOrPathsToDelete !== 'string') {
  91. return;
  92. }
  93. const path = pathOrPathsToDelete;
  94. if (isCompletely) {
  95. toastSuccess(t('deleted_pages_completely', { path }));
  96. }
  97. else {
  98. toastSuccess(t('deleted_pages', { path }));
  99. }
  100. mutateBookmarkData();
  101. mutateBookmarkInfo();
  102. };
  103. openDeleteModal([pageToDelete], { onDeleted: pageDeletedHandler });
  104. }, [mutateBookmarkInfo, mutateBookmarkData, openDeleteModal, t]);
  105. const onUnbookmarkHandler = useCallback(() => {
  106. mutateBookmarkData();
  107. mutateBookmarkInfo();
  108. }, [mutateBookmarkInfo, mutateBookmarkData]);
  109. const itemDropHandler = async(item: DragItemDataType, dragItemType: string | symbol | null) => {
  110. if (dragItemType === DRAG_ITEM_TYPE.FOLDER) {
  111. try {
  112. if (item.bookmarkFolder != null) {
  113. await updateBookmarkFolder(item.bookmarkFolder._id, item.bookmarkFolder.name, bookmarkFolder._id);
  114. mutateBookmarkData();
  115. }
  116. }
  117. catch (err) {
  118. toastError(err);
  119. }
  120. }
  121. else {
  122. try {
  123. if (item != null) {
  124. await addBookmarkToFolder(item._id, bookmarkFolder._id);
  125. mutateBookmarkData();
  126. await mutateUserBookmarks();
  127. }
  128. }
  129. catch (err) {
  130. toastError(err);
  131. }
  132. }
  133. };
  134. const isDropable = (item: DragItemDataType, type: string | null| symbol): boolean => {
  135. if (type === DRAG_ITEM_TYPE.FOLDER) {
  136. if (item.bookmarkFolder.parent === bookmarkFolder._id || item.bookmarkFolder._id === bookmarkFolder._id) {
  137. return false;
  138. }
  139. // Maximum folder hierarchy of 2 levels
  140. // If the drop source folder has child folders, the drop source folder cannot be moved because the drop source folder hierarchy is already 2.
  141. // If the destination folder has a parent, the source folder cannot be moved because the destination folder hierarchy is already 2.
  142. if (item.bookmarkFolder.children.length !== 0 || bookmarkFolder.parent != null) {
  143. return false;
  144. }
  145. return item.root !== root || item.level >= level;
  146. }
  147. if (item.parentFolder != null && item.parentFolder._id === bookmarkFolder._id) {
  148. return false;
  149. }
  150. return true;
  151. };
  152. const renderChildFolder = () => {
  153. return isOpen && children?.map((childFolder) => {
  154. return (
  155. <div key={childFolder._id} className="grw-foldertree-item-children">
  156. <BookmarkFolderItem
  157. key={childFolder._id}
  158. bookmarkFolder={childFolder}
  159. level={level + 1}
  160. root={root}
  161. isUserHomePage ={isUserHomePage}
  162. />
  163. </div>
  164. );
  165. });
  166. };
  167. const renderBookmarkItem = () => {
  168. return isOpen && bookmarks?.map((bookmark) => {
  169. return (
  170. <BookmarkItem
  171. bookmarkedPage={bookmark.page}
  172. key={bookmark._id}
  173. onUnbookmarked={onUnbookmarkHandler}
  174. onRenamed={mutateBookmarkData}
  175. onClickDeleteMenuItem={onClickDeleteBookmarkHandler}
  176. parentFolder={bookmarkFolder}
  177. level={level + 1}
  178. />
  179. );
  180. });
  181. };
  182. const onClickRenameHandler = useCallback(() => {
  183. setIsRenameAction(true);
  184. }, []);
  185. const onClickDeleteHandler = useCallback(() => {
  186. const bookmarkFolderDeleteHandler: onDeletedBookmarkFolderFunction = (folderId) => {
  187. if (typeof folderId !== 'string') {
  188. return;
  189. }
  190. mutateBookmarkInfo();
  191. mutateBookmarkData();
  192. };
  193. if (bookmarkFolder == null) {
  194. return;
  195. }
  196. openDeleteBookmarkFolderModal(bookmarkFolder, { onDeleted: bookmarkFolderDeleteHandler });
  197. }, [bookmarkFolder, mutateBookmarkData, mutateBookmarkInfo, openDeleteBookmarkFolderModal]);
  198. return (
  199. <div id={`grw-bookmark-folder-item-${folderId}`} className="grw-foldertree-item-container">
  200. <DragAndDropWrapper
  201. key={folderId}
  202. type={acceptedTypes}
  203. item={props}
  204. useDragMode={true}
  205. useDropMode={true}
  206. onDropItem={itemDropHandler}
  207. isDropable={isDropable}
  208. >
  209. <li
  210. className={'list-group-item list-group-item-action border-0 py-0 pr-3 d-flex align-items-center'}
  211. onClick={loadChildFolder}
  212. style={{ paddingLeft }}
  213. >
  214. <div className="grw-triangle-container d-flex justify-content-center">
  215. {childrenExists && (
  216. <button
  217. type="button"
  218. className={`grw-foldertree-triangle-btn btn ${isOpen ? 'grw-foldertree-open' : ''}`}
  219. onClick={loadChildFolder}
  220. >
  221. <div className="d-flex justify-content-center">
  222. <TriangleIcon />
  223. </div>
  224. </button>
  225. )}
  226. </div>
  227. {
  228. <div>
  229. <FolderIcon isOpen={isOpen} />
  230. </div>
  231. }
  232. {isRenameAction ? (
  233. <BookmarkFolderNameInput
  234. onClickOutside={() => setIsRenameAction(false)}
  235. onPressEnter={onPressEnterHandlerForRename}
  236. value={name}
  237. />
  238. ) : (
  239. <>
  240. <div className='grw-foldertree-title-anchor pl-2' >
  241. <p className={'text-truncate m-auto '}>{name}</p>
  242. </div>
  243. </>
  244. )}
  245. <div className="grw-foldertree-control d-flex">
  246. <BookmarkFolderItemControl
  247. onClickRename={onClickRenameHandler}
  248. onClickDelete={onClickDeleteHandler}
  249. >
  250. <div onClick={e => e.stopPropagation()}>
  251. <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control p-0 grw-visible-on-hover mr-1">
  252. <i className="icon-options fa fa-rotate-90 p-1"></i>
  253. </DropdownToggle>
  254. </div>
  255. </BookmarkFolderItemControl>
  256. {/* Maximum folder hierarchy of 2 levels */}
  257. {!(bookmarkFolder.parent != null) && (
  258. <button
  259. id='create-bookmark-folder-button'
  260. type="button"
  261. className="border-0 rounded btn btn-page-item-control p-0 grw-visible-on-hover"
  262. onClick={onClickPlusButton}
  263. >
  264. <i className="icon-plus d-block p-0" />
  265. </button>
  266. )}
  267. </div>
  268. </li>
  269. </DragAndDropWrapper>
  270. {isCreateAction && (
  271. <div className="flex-fill">
  272. <BookmarkFolderNameInput
  273. onClickOutside={() => setIsCreateAction(false)}
  274. onPressEnter={onPressEnterHandlerForCreate}
  275. />
  276. </div>
  277. )}
  278. {
  279. renderChildFolder()
  280. }
  281. {
  282. renderBookmarkItem()
  283. }
  284. </div>
  285. );
  286. };