PageTreeItem.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. import React, {
  2. useCallback, useState, FC,
  3. } from 'react';
  4. import nodePath from 'path';
  5. import { pagePathUtils } from '@growi/core';
  6. import { useTranslation } from 'next-i18next';
  7. import { useDrag, useDrop } from 'react-dnd';
  8. import { apiv3Put } from '~/client/util/apiv3-client';
  9. import { toastWarning, toastError } from '~/client/util/toastr';
  10. import { IPageHasId } from '~/interfaces/page';
  11. import { mutatePageTree, useSWRxPageChildren } from '~/stores/page-listing';
  12. import loggerFactory from '~/utils/logger';
  13. import { ItemNode } from './ItemNode';
  14. import SimpleItem, { SimpleItemProps } from './SimpleItem';
  15. const logger = loggerFactory('growi:cli:Item');
  16. type Optional = 'itemRef' | 'itemClass' | 'mainClassName';
  17. type PageTreeItemProps = Omit<SimpleItemProps, Optional> & {key};
  18. export const PageTreeItem: FC<PageTreeItemProps> = (props: PageTreeItemProps) => {
  19. const getNewPathAfterMoved = (droppedPagePath: string, newParentPagePath: string): string => {
  20. const pageTitle = nodePath.basename(droppedPagePath);
  21. return nodePath.join(newParentPagePath, pageTitle);
  22. };
  23. const isDroppable = (fromPage?: Partial<IPageHasId>, newParentPage?: Partial<IPageHasId>, printLog = false): boolean => {
  24. if (fromPage == null || newParentPage == null || fromPage.path == null || newParentPage.path == null) {
  25. if (printLog) {
  26. logger.warn('Any of page, page.path or droppedPage.path is null');
  27. }
  28. return false;
  29. }
  30. const newPathAfterMoved = getNewPathAfterMoved(fromPage.path, newParentPage.path);
  31. return pagePathUtils.canMoveByPath(fromPage.path, newPathAfterMoved) && !pagePathUtils.isUsersTopPage(newParentPage.path);
  32. };
  33. const { t } = useTranslation();
  34. const {
  35. itemNode, isOpen: _isOpen = false, onRenamed,
  36. } = props;
  37. const { page } = itemNode;
  38. const [isOpen, setIsOpen] = useState(_isOpen);
  39. const [shouldHide, setShouldHide] = useState(false);
  40. const { mutate: mutateChildren } = useSWRxPageChildren(isOpen ? page._id : null);
  41. const displayDroppedItemByPageId = useCallback((pageId) => {
  42. const target = document.getElementById(`pagetree-item-${pageId}`);
  43. if (target == null) {
  44. return;
  45. }
  46. // // wait 500ms to avoid removing before d-none is set by useDrag end() callback
  47. setTimeout(() => {
  48. target.classList.remove('d-none');
  49. }, 500);
  50. }, []);
  51. const [, drag] = useDrag({
  52. type: 'PAGE_TREE',
  53. item: { page },
  54. canDrag: () => {
  55. if (page.path == null) {
  56. return false;
  57. }
  58. return !pagePathUtils.isUsersProtectedPages(page.path);
  59. },
  60. end: (item, monitor) => {
  61. // in order to set d-none to dropped Item
  62. const dropResult = monitor.getDropResult();
  63. if (dropResult != null) {
  64. setShouldHide(true);
  65. }
  66. },
  67. collect: monitor => ({
  68. isDragging: monitor.isDragging(),
  69. canDrag: monitor.canDrag(),
  70. }),
  71. });
  72. const pageItemDropHandler = async(item: ItemNode) => {
  73. const { page: droppedPage } = item;
  74. if (!isDroppable(droppedPage, page, true)) {
  75. return;
  76. }
  77. if (droppedPage.path == null || page.path == null) {
  78. return;
  79. }
  80. const newPagePath = getNewPathAfterMoved(droppedPage.path, page.path);
  81. try {
  82. await apiv3Put('/pages/rename', {
  83. pageId: droppedPage._id,
  84. revisionId: droppedPage.revision,
  85. newPagePath,
  86. isRenameRedirect: false,
  87. updateMetadata: true,
  88. });
  89. await mutatePageTree();
  90. await mutateChildren();
  91. if (onRenamed != null) {
  92. onRenamed(page.path, newPagePath);
  93. }
  94. // force open
  95. setIsOpen(true);
  96. }
  97. catch (err) {
  98. // display the dropped item
  99. displayDroppedItemByPageId(droppedPage._id);
  100. if (err.code === 'operation__blocked') {
  101. toastWarning(t('pagetree.you_cannot_move_this_page_now'));
  102. }
  103. else {
  104. toastError(t('pagetree.something_went_wrong_with_moving_page'));
  105. }
  106. }
  107. };
  108. const [{ isOver }, drop] = useDrop<ItemNode, Promise<void>, { isOver: boolean }>(
  109. () => ({
  110. accept: 'PAGE_TREE',
  111. drop: pageItemDropHandler,
  112. hover: (item, monitor) => {
  113. // when a drag item is overlapped more than 1 sec, the drop target item will be opened.
  114. if (monitor.isOver()) {
  115. setTimeout(() => {
  116. if (monitor.isOver()) {
  117. setIsOpen(true);
  118. }
  119. }, 600);
  120. }
  121. },
  122. canDrop: (item) => {
  123. const { page: droppedPage } = item;
  124. return isDroppable(droppedPage, page);
  125. },
  126. collect: monitor => ({
  127. isOver: monitor.isOver(),
  128. }),
  129. }),
  130. [page],
  131. );
  132. const itemRef = (c) => { drag(c); drop(c) };
  133. const mainClassName = `${isOver ? 'grw-pagetree-is-over' : ''} ${shouldHide ? 'd-none' : ''}`;
  134. return (
  135. <SimpleItem
  136. key={props.key}
  137. targetPathOrId={props.targetPathOrId}
  138. itemNode={props.itemNode}
  139. isOpen
  140. isEnableActions={props.isEnableActions}
  141. isReadOnlyUser={props.isReadOnlyUser}
  142. onRenamed={props.onRenamed}
  143. onClickDuplicateMenuItem={props.onClickDuplicateMenuItem}
  144. onClickDeleteMenuItem={props.onClickDeleteMenuItem}
  145. itemRef={itemRef}
  146. itemClass={PageTreeItem}
  147. mainClassName={mainClassName}
  148. />
  149. );
  150. };