BookmarkFolderItem.tsx 10 KB

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