WNomunomu 2 лет назад
Родитель
Сommit
453baaa82b

+ 3 - 1
.vscode/settings.json

@@ -19,5 +19,7 @@
 
 
   "githubPullRequests.ignoredPullRequestBranches": [
   "githubPullRequests.ignoredPullRequestBranches": [
     "master"
     "master"
-  ]
+  ],
+  "debug.allowBreakpointsEverywhere": true,
+  "debug.autoExpandLazyVariables": false
 }
 }

+ 4 - 1
apps/app/src/components/Sidebar/PageTree/ItemsTree.tsx

@@ -30,6 +30,7 @@ import PageTreeContentSkeleton from '../Skeleton/PageTreeContentSkeleton';
 
 
 import Item from './Item';
 import Item from './Item';
 import { ItemNode } from './ItemNode';
 import { ItemNode } from './ItemNode';
+import { CreatePageTreeItem } from './PageTreeItem';
 import SimpleItem from './SimpleItem';
 import SimpleItem from './SimpleItem';
 
 
 import styles from './ItemsTree.module.scss';
 import styles from './ItemsTree.module.scss';
@@ -271,10 +272,12 @@ const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
     initialItemNode = generateInitialNodeBeforeResponse(targetAndAncestorsData.targetAndAncestors);
     initialItemNode = generateInitialNodeBeforeResponse(targetAndAncestorsData.targetAndAncestors);
   }
   }
 
 
+  const PageTreeItem = CreatePageTreeItem();
+
   if (initialItemNode != null) {
   if (initialItemNode != null) {
     return (
     return (
       <ul className={`grw-pagetree ${styles['grw-pagetree']} list-group py-3`} ref={rootElemRef}>
       <ul className={`grw-pagetree ${styles['grw-pagetree']} list-group py-3`} ref={rootElemRef}>
-        <SimpleItem
+        <PageTreeItem
           key={initialItemNode.page.path}
           key={initialItemNode.page.path}
           targetPathOrId={targetPathOrId}
           targetPathOrId={targetPathOrId}
           itemNode={initialItemNode}
           itemNode={initialItemNode}

+ 170 - 10
apps/app/src/components/Sidebar/PageTree/PageTreeItem.tsx

@@ -1,27 +1,68 @@
-import React, { useCallback, useState } from 'react';
+import React, {
+  useCallback, useState, FC, useEffect, ReactNode, useMemo,
+} from 'react';
 
 
 import nodePath from 'path';
 import nodePath from 'path';
 
 
-import { pagePathUtils } from '@growi/core';
+import {
+  pathUtils, pagePathUtils, Nullable,
+} from '@growi/core';
+import { useTranslation } from 'next-i18next';
+import Link from 'next/link';
+import { useRouter } from 'next/router';
 import { useDrag, useDrop } from 'react-dnd';
 import { useDrag, useDrop } from 'react-dnd';
+import { UncontrolledTooltip, DropdownToggle } from 'reactstrap';
 
 
-import { apiv3Put } from '~/client/util/apiv3-client';
-import { toastWarning, toastError } from '~/client/util/toastr';
-import { IPageHasId } from '~/interfaces/page';
-import { mutatePageTree } from '~/stores/page-listing';
+import { bookmark, unbookmark, resumeRenameOperation } from '~/client/services/page-operation';
+import { apiv3Put, apiv3Post } from '~/client/util/apiv3-client';
+import { ValidationTarget } from '~/client/util/input-validator';
+import { toastWarning, toastError, toastSuccess } from '~/client/util/toastr';
+import { TriangleIcon } from '~/components/Icons/TriangleIcon';
+import { NotAvailableForGuest } from '~/components/NotAvailableForGuest';
+import { NotAvailableForReadOnlyUser } from '~/components/NotAvailableForReadOnlyUser';
+import {
+  IPageHasId, IPageInfoAll, IPageToDeleteWithMeta,
+} from '~/interfaces/page';
+import { useSWRMUTxCurrentUserBookmarks } from '~/stores/bookmark';
+import { IPageForPageDuplicateModal } from '~/stores/modal';
+import { useSWRMUTxPageInfo } from '~/stores/page';
+import { mutatePageTree, useSWRxPageChildren } from '~/stores/page-listing';
+import { usePageTreeDescCountMap } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
+import { shouldRecoverPagePaths } from '~/utils/page-operation';
+
+import ClosableTextInput from '../../Common/ClosableTextInput';
+import CountBadge from '../../Common/CountBadge';
+import { PageItemControl } from '../../Common/Dropdown/PageItemControl';
 
 
 import { ItemNode } from './ItemNode';
 import { ItemNode } from './ItemNode';
+import SimpleItem from './SimpleItem';
 
 
 
 
 const logger = loggerFactory('growi:cli:Item');
 const logger = loggerFactory('growi:cli:Item');
 
 
 
 
-export const CreatePageTreeItem = (SimpleItem) => {
-  const settings = '';
+export const CreatePageTreeItem = () => {
+  const getNewPathAfterMoved = (droppedPagePath: string, newParentPagePath: string): string => {
+    const pageTitle = nodePath.basename(droppedPagePath);
+    return nodePath.join(newParentPagePath, pageTitle);
+  };
+
+  const isDroppable = (fromPage?: Partial<IPageHasId>, newParentPage?: Partial<IPageHasId>, printLog = false): boolean => {
+    if (fromPage == null || newParentPage == null || fromPage.path == null || newParentPage.path == null) {
+      if (printLog) {
+        logger.warn('Any of page, page.path or droppedPage.path is null');
+      }
+      return false;
+    }
+
+    const newPathAfterMoved = getNewPathAfterMoved(fromPage.path, newParentPage.path);
+    return pagePathUtils.canMoveByPath(fromPage.path, newPathAfterMoved) && !pagePathUtils.isUsersTopPage(newParentPage.path);
+  };
 
 
   return function usePageTreeItem(props) {
   return function usePageTreeItem(props) {
-    const [shouldHide, setShouldHide] = useState(false);
+    const { t } = useTranslation();
+    const router = useRouter();
 
 
     const {
     const {
       itemNode, targetPathOrId, isOpen: _isOpen = false,
       itemNode, targetPathOrId, isOpen: _isOpen = false,
@@ -30,6 +71,39 @@ export const CreatePageTreeItem = (SimpleItem) => {
 
 
     const { page, children } = itemNode;
     const { page, children } = itemNode;
 
 
+    const [currentChildren, setCurrentChildren] = useState(children);
+    const [isOpen, setIsOpen] = useState(_isOpen);
+    const [isNewPageInputShown, setNewPageInputShown] = useState(false);
+    const [shouldHide, setShouldHide] = useState(false);
+    const [isRenameInputShown, setRenameInputShown] = useState(false);
+    const [isCreating, setCreating] = useState(false);
+
+    const { data, mutate: mutateChildren } = useSWRxPageChildren(isOpen ? page._id : null);
+    const { trigger: mutateCurrentUserBookmarks } = useSWRMUTxCurrentUserBookmarks();
+    const { trigger: mutatePageInfo } = useSWRMUTxPageInfo(page._id ?? null);
+
+    // descendantCount
+    const { getDescCount } = usePageTreeDescCountMap();
+    const descendantCount = getDescCount(page._id) || page.descendantCount || 0;
+
+
+    // hasDescendants flag
+    const isChildrenLoaded = currentChildren?.length > 0;
+    const hasDescendants = descendantCount > 0 || isChildrenLoaded;
+
+    // to re-show hidden item when useDrag end() callback
+    const displayDroppedItemByPageId = useCallback((pageId) => {
+      const target = document.getElementById(`pagetree-item-${pageId}`);
+      if (target == null) {
+        return;
+      }
+
+      //   // wait 500ms to avoid removing before d-none is set by useDrag end() callback
+      setTimeout(() => {
+        target.classList.remove('d-none');
+      }, 500);
+    }, []);
+
     const [, drag] = useDrag({
     const [, drag] = useDrag({
       type: 'PAGE_TREE',
       type: 'PAGE_TREE',
       item: { page },
       item: { page },
@@ -52,9 +126,95 @@ export const CreatePageTreeItem = (SimpleItem) => {
       }),
       }),
     });
     });
 
 
+    const pageItemDropHandler = async(item: ItemNode) => {
+      const { page: droppedPage } = item;
+
+      if (!isDroppable(droppedPage, page, true)) {
+        return;
+      }
+
+      if (droppedPage.path == null || page.path == null) {
+        return;
+      }
+
+      const newPagePath = getNewPathAfterMoved(droppedPage.path, page.path);
+
+      try {
+        await apiv3Put('/pages/rename', {
+          pageId: droppedPage._id,
+          revisionId: droppedPage.revision,
+          newPagePath,
+          isRenameRedirect: false,
+          updateMetadata: true,
+        });
+
+        await mutatePageTree();
+        await mutateChildren();
+
+        if (onRenamed != null) {
+          onRenamed(page.path, newPagePath);
+        }
+
+        // force open
+        setIsOpen(true);
+      }
+      catch (err) {
+        // display the dropped item
+        displayDroppedItemByPageId(droppedPage._id);
+
+        if (err.code === 'operation__blocked') {
+          toastWarning(t('pagetree.you_cannot_move_this_page_now'));
+        }
+        else {
+          toastError(t('pagetree.something_went_wrong_with_moving_page'));
+        }
+      }
+    };
+
+    const [{ isOver }, drop] = useDrop<ItemNode, Promise<void>, { isOver: boolean }>(
+      () => ({
+        accept: 'PAGE_TREE',
+        drop: pageItemDropHandler,
+        hover: (item, monitor) => {
+          // when a drag item is overlapped more than 1 sec, the drop target item will be opened.
+          if (monitor.isOver()) {
+            setTimeout(() => {
+              if (monitor.isOver()) {
+                setIsOpen(true);
+              }
+            }, 600);
+          }
+        },
+        canDrop: (item) => {
+          const { page: droppedPage } = item;
+          return isDroppable(droppedPage, page);
+        },
+        collect: monitor => ({
+          isOver: monitor.isOver(),
+        }),
+      }),
+      [page],
+    );
+
+    // const memoizedDrag = useMemo(() => drag, []);
+    // const memoizedDrop = useMemo(() => drop, []);
+
+    const itemRef = (c) => { drag(c); drop(c) };
+
     return (
     return (
       <div className={shouldHide ? 'd-none' : ''}>
       <div className={shouldHide ? 'd-none' : ''}>
-        <SimpleItem/>
+        <SimpleItem
+          key={props.key}
+          targetPathOrId={props.targetPathOrId}
+          itemNode={props.itemNode}
+          isOpen
+          isEnableActions={props.isEnableActions}
+          isReadOnlyUser={props.isReadOnlyUser}
+          onRenamed={props.onRenamed}
+          onClickDuplicateMenuItem={props.onClickDuplicateMenuItem}
+          onClickDeleteMenuItem={props.onClickDeleteMenuItem}
+          // itemRef={itemRef}
+        />
       </div>
       </div>
     );
     );
   };
   };

+ 87 - 52
apps/app/src/components/Sidebar/PageTree/SimpleItem.tsx

@@ -10,7 +10,7 @@ import {
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import Link from 'next/link';
 import Link from 'next/link';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
-import { useDrag, useDrop } from 'react-dnd';
+import { ConnectDragSource, useDrag, useDrop } from 'react-dnd';
 import { UncontrolledTooltip, DropdownToggle } from 'reactstrap';
 import { UncontrolledTooltip, DropdownToggle } from 'reactstrap';
 
 
 import { bookmark, unbookmark, resumeRenameOperation } from '~/client/services/page-operation';
 import { bookmark, unbookmark, resumeRenameOperation } from '~/client/services/page-operation';
@@ -50,6 +50,7 @@ interface ItemProps {
   onRenamed?(fromPath: string | undefined, toPath: string): void
   onRenamed?(fromPath: string | undefined, toPath: string): void
   onClickDuplicateMenuItem?(pageToDuplicate: IPageForPageDuplicateModal): void
   onClickDuplicateMenuItem?(pageToDuplicate: IPageForPageDuplicateModal): void
   onClickDeleteMenuItem?(pageToDelete: IPageToDeleteWithMeta): void
   onClickDeleteMenuItem?(pageToDelete: IPageToDeleteWithMeta): void
+  itemRef?
 }
 }
 
 
 // Utility to mark target
 // Utility to mark target
@@ -75,10 +76,13 @@ const markTarget = (children: ItemNode[], targetPathOrId?: Nullable<string>): vo
  * @param newParentPagePath
  * @param newParentPagePath
  * @returns
  * @returns
  */
  */
+
+//
 const getNewPathAfterMoved = (droppedPagePath: string, newParentPagePath: string): string => {
 const getNewPathAfterMoved = (droppedPagePath: string, newParentPagePath: string): string => {
   const pageTitle = nodePath.basename(droppedPagePath);
   const pageTitle = nodePath.basename(droppedPagePath);
   return nodePath.join(newParentPagePath, pageTitle);
   return nodePath.join(newParentPagePath, pageTitle);
 };
 };
+//
 
 
 /**
 /**
  * Return whether the fromPage could be moved under the newParentPage
  * Return whether the fromPage could be moved under the newParentPage
@@ -87,6 +91,8 @@ const getNewPathAfterMoved = (droppedPagePath: string, newParentPagePath: string
  * @param printLog
  * @param printLog
  * @returns
  * @returns
  */
  */
+
+//
 const isDroppable = (fromPage?: Partial<IPageHasId>, newParentPage?: Partial<IPageHasId>, printLog = false): boolean => {
 const isDroppable = (fromPage?: Partial<IPageHasId>, newParentPage?: Partial<IPageHasId>, printLog = false): boolean => {
   if (fromPage == null || newParentPage == null || fromPage.path == null || newParentPage.path == null) {
   if (fromPage == null || newParentPage == null || fromPage.path == null || newParentPage.path == null) {
     if (printLog) {
     if (printLog) {
@@ -98,6 +104,7 @@ const isDroppable = (fromPage?: Partial<IPageHasId>, newParentPage?: Partial<IPa
   const newPathAfterMoved = getNewPathAfterMoved(fromPage.path, newParentPage.path);
   const newPathAfterMoved = getNewPathAfterMoved(fromPage.path, newParentPage.path);
   return pagePathUtils.canMoveByPath(fromPage.path, newPathAfterMoved) && !pagePathUtils.isUsersTopPage(newParentPage.path);
   return pagePathUtils.canMoveByPath(fromPage.path, newPathAfterMoved) && !pagePathUtils.isUsersTopPage(newParentPage.path);
 };
 };
+//
 
 
 // Component wrapper to make a child element not draggable
 // Component wrapper to make a child element not draggable
 // https://github.com/react-dnd/react-dnd/issues/335
 // https://github.com/react-dnd/react-dnd/issues/335
@@ -140,6 +147,7 @@ const SimpleItem: FC<ItemProps> = (props: ItemProps) => {
   const isChildrenLoaded = currentChildren?.length > 0;
   const isChildrenLoaded = currentChildren?.length > 0;
   const hasDescendants = descendantCount > 0 || isChildrenLoaded;
   const hasDescendants = descendantCount > 0 || isChildrenLoaded;
 
 
+  //
   // to re-show hidden item when useDrag end() callback
   // to re-show hidden item when useDrag end() callback
   const displayDroppedItemByPageId = useCallback((pageId) => {
   const displayDroppedItemByPageId = useCallback((pageId) => {
     const target = document.getElementById(`pagetree-item-${pageId}`);
     const target = document.getElementById(`pagetree-item-${pageId}`);
@@ -147,34 +155,38 @@ const SimpleItem: FC<ItemProps> = (props: ItemProps) => {
       return;
       return;
     }
     }
 
 
-  //   // wait 500ms to avoid removing before d-none is set by useDrag end() callback
+    //   // wait 500ms to avoid removing before d-none is set by useDrag end() callback
     setTimeout(() => {
     setTimeout(() => {
       target.classList.remove('d-none');
       target.classList.remove('d-none');
     }, 500);
     }, 500);
   }, []);
   }, []);
-
-  const [, drag] = useDrag({
-    type: 'PAGE_TREE',
-    item: { page },
-    canDrag: () => {
-      if (page.path == null) {
-        return false;
-      }
-      return !pagePathUtils.isUsersProtectedPages(page.path);
-    },
-    end: (item, monitor) => {
-      // in order to set d-none to dropped Item
-      const dropResult = monitor.getDropResult();
-      if (dropResult != null) {
-        setShouldHide(true);
-      }
-    },
-    collect: monitor => ({
-      isDragging: monitor.isDragging(),
-      canDrag: monitor.canDrag(),
-    }),
-  });
-
+  //
+
+  // ここから切り分け開始
+  // const [, drag] = useDrag({
+  //   type: 'PAGE_TREE',
+  //   item: { page },
+  //   canDrag: () => {
+  //     if (page.path == null) {
+  //       return false;
+  //     }
+  //     return !pagePathUtils.isUsersProtectedPages(page.path);
+  //   },
+  //   end: (item, monitor) => {
+  //     // in order to set d-none to dropped Item
+  //     const dropResult = monitor.getDropResult();
+  //     if (dropResult != null) {
+  //       setShouldHide(true);
+  //     }
+  //   },
+  //   collect: monitor => ({
+  //     isDragging: monitor.isDragging(),
+  //     canDrag: monitor.canDrag(),
+  //   }),
+  // });
+  //
+
+  //
   const pageItemDropHandler = async(item: ItemNode) => {
   const pageItemDropHandler = async(item: ItemNode) => {
     const { page: droppedPage } = item;
     const { page: droppedPage } = item;
 
 
@@ -219,31 +231,34 @@ const SimpleItem: FC<ItemProps> = (props: ItemProps) => {
       }
       }
     }
     }
   };
   };
-
-  const [{ isOver }, drop] = useDrop<ItemNode, Promise<void>, { isOver: boolean }>(
-    () => ({
-      accept: 'PAGE_TREE',
-      drop: pageItemDropHandler,
-      hover: (item, monitor) => {
-        // when a drag item is overlapped more than 1 sec, the drop target item will be opened.
-        if (monitor.isOver()) {
-          setTimeout(() => {
-            if (monitor.isOver()) {
-              setIsOpen(true);
-            }
-          }, 600);
-        }
-      },
-      canDrop: (item) => {
-        const { page: droppedPage } = item;
-        return isDroppable(droppedPage, page);
-      },
-      collect: monitor => ({
-        isOver: monitor.isOver(),
-      }),
-    }),
-    [page],
-  );
+  //
+
+  //
+  // const [{ isOver }, drop] = useDrop<ItemNode, Promise<void>, { isOver: boolean }>(
+  //   () => ({
+  //     accept: 'PAGE_TREE',
+  //     drop: pageItemDropHandler,
+  //     hover: (item, monitor) => {
+  //       // when a drag item is overlapped more than 1 sec, the drop target item will be opened.
+  //       if (monitor.isOver()) {
+  //         setTimeout(() => {
+  //           if (monitor.isOver()) {
+  //             setIsOpen(true);
+  //           }
+  //         }, 600);
+  //       }
+  //     },
+  //     canDrop: (item) => {
+  //       const { page: droppedPage } = item;
+  //       return isDroppable(droppedPage, page);
+  //     },
+  //     collect: monitor => ({
+  //       isOver: monitor.isOver(),
+  //     }),
+  //   }),
+  //   [page],
+  // );
+  //
 
 
 
 
   const hasChildren = useCallback((): boolean => {
   const hasChildren = useCallback((): boolean => {
@@ -434,6 +449,26 @@ const SimpleItem: FC<ItemProps> = (props: ItemProps) => {
   const shouldShowAttentionIcon = page.processData != null ? shouldRecoverPagePaths(page.processData) : false;
   const shouldShowAttentionIcon = page.processData != null ? shouldRecoverPagePaths(page.processData) : false;
   const pageName = nodePath.basename(page.path ?? '') || '/';
   const pageName = nodePath.basename(page.path ?? '') || '/';
 
 
+
+  const itemRef = props.itemRef;
+
+  console.log(itemRef);
+
+  // const [isLoading, setIsLoading] = useState(true);
+
+  // useEffect(() => {
+  //   // someProp が非同期で設定されるまで待機
+  //   if (itemRef !== undefined) {
+  //     setIsLoading(false);
+  //   }
+  // }, [itemRef]);
+
+  // if (isLoading) {
+  //   return <div>Loading...</div>;
+  // }
+
+  // 上のやり方だと、意識的にitemRefを設定しなかった場合におかしくなる
+
   return (
   return (
     <div
     <div
       id={`pagetree-item-${page._id}`}
       id={`pagetree-item-${page._id}`}
@@ -441,8 +476,7 @@ const SimpleItem: FC<ItemProps> = (props: ItemProps) => {
       className={`grw-pagetree-item-container ${shouldHide ? 'd-none' : ''}`}
       className={`grw-pagetree-item-container ${shouldHide ? 'd-none' : ''}`}
     >
     >
       <li
       <li
-        // ref={(c) => { drag(c); drop(c) }}
-        { props.hoge ?? '' }
+        ref={itemRef}
         className={`list-group-item list-group-item-action border-0 py-0 pr-3 d-flex align-items-center
         className={`list-group-item list-group-item-action border-0 py-0 pr-3 d-flex align-items-center
         ${page.isTarget ? 'grw-pagetree-current-page-item' : ''}`}
         ${page.isTarget ? 'grw-pagetree-current-page-item' : ''}`}
         id={page.isTarget ? 'grw-pagetree-current-page-item' : `grw-pagetree-list-${page._id}`}
         id={page.isTarget ? 'grw-pagetree-current-page-item' : `grw-pagetree-list-${page._id}`}
@@ -559,6 +593,7 @@ const SimpleItem: FC<ItemProps> = (props: ItemProps) => {
               onRenamed={onRenamed}
               onRenamed={onRenamed}
               onClickDuplicateMenuItem={onClickDuplicateMenuItem}
               onClickDuplicateMenuItem={onClickDuplicateMenuItem}
               onClickDeleteMenuItem={onClickDeleteMenuItem}
               onClickDeleteMenuItem={onClickDeleteMenuItem}
+              itemRef={itemRef}
             />
             />
             {isCreating && (currentChildren.length - 1 === index) && (
             {isCreating && (currentChildren.length - 1 === index) && (
               <div className="text-muted text-center">
               <div className="text-muted text-center">