BookmarkFolderItem.tsx 10 KB

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