Просмотр исходного кода

Merge remote-tracking branch 'origin/master' into
imprv/87816-show-presentation

kaori 4 лет назад
Родитель
Сommit
cdb7eb9d47

+ 30 - 6
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -1,4 +1,4 @@
-import React, { useCallback } from 'react';
+import React, { useState, useCallback } from 'react';
 import PropTypes from 'prop-types';
 
 import { useTranslation } from 'react-i18next';
@@ -14,7 +14,7 @@ import {
 } from '~/stores/ui';
 import {
   useCurrentCreatedAt, useCurrentUpdatedAt, useCurrentPageId, useRevisionId, useCurrentPagePath,
-  useCreator, useRevisionAuthor, useIsGuestUser, useIsSharedUser, useShareLinkId,
+  useCreator, useRevisionAuthor, useCurrentUser, useIsGuestUser, useIsSharedUser, useShareLinkId,
 } from '~/stores/context';
 import { useSWRTagsInfo } from '~/stores/page';
 
@@ -31,6 +31,7 @@ import { SubNavButtons } from './SubNavButtons';
 import PageEditorModeManager from './PageEditorModeManager';
 import { GrowiSubNavigation } from './GrowiSubNavigation';
 import PresentationIcon from '../Icons/PresentationIcon';
+import CreateTemplateModal from '../CreateTemplateModal';
 import { exportAsMarkdown } from '~/client/services/page-operation';
 
 
@@ -38,15 +39,21 @@ type AdditionalMenuItemsProps = AdditionalMenuItemsRendererProps & {
   pageId: string,
   revisionId: string,
   isLinkSharingDisabled?: boolean,
+  onClickTemplateMenuItem: (isPageTemplateModalShown: boolean) => void,
+
 }
 
 const AdditionalMenuItems = (props: AdditionalMenuItemsProps): JSX.Element => {
   const { t } = useTranslation();
 
   const {
-    pageId, revisionId, isLinkSharingDisabled,
+    pageId, revisionId, isLinkSharingDisabled, onClickTemplateMenuItem,
   } = props;
 
+  const openPageTemplateModalHandler = () => {
+    onClickTemplateMenuItem(true);
+  };
+
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isSharedUser } = useIsSharedUser();
   const { open: openPresentationModal } = usePagePresentationModalStatus();
@@ -101,7 +108,7 @@ const AdditionalMenuItems = (props: AdditionalMenuItemsProps): JSX.Element => {
       <DropdownItem divider />
 
       {/* Create template */}
-      <DropdownItem onClick={() => { /* TODO: implement in https://redmine.weseek.co.jp/issues/87673 */ }}>
+      <DropdownItem onClick={openPageTemplateModalHandler}>
         <i className="icon-fw icon-magic-wand"></i> { t('template.option_label.create/edit') }
       </DropdownItem>
     </>
@@ -120,6 +127,7 @@ const GrowiContextualSubNavigation = (props) => {
   const { data: path } = useCurrentPagePath();
   const { data: creator } = useCreator();
   const { data: revisionAuthor } = useRevisionAuthor();
+  const { data: currentUser } = useCurrentUser();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: shareLinkId } = useShareLinkId();
@@ -135,6 +143,8 @@ const GrowiContextualSubNavigation = (props) => {
   const { open: openRenameModal } = usePageRenameModalStatus();
   const { open: openDeleteModal } = usePageDeleteModal();
 
+  const [isPageTemplateModalShown, setIsPageTempleteModalShown] = useState(false);
+
   const {
     editorContainer, isCompactMode, isLinkSharingDisabled,
   } = props;
@@ -175,6 +185,11 @@ const GrowiContextualSubNavigation = (props) => {
     openDeleteModal([pageToDelete]);
   }, [openDeleteModal]);
 
+  const templateMenuItemClickHandler = useCallback(() => {
+    setIsPageTempleteModalShown(true);
+  }, []);
+
+
   const ControlComponents = useCallback(() => {
     function onPageEditorModeButtonClicked(viewType) {
       mutateEditorMode(viewType);
@@ -198,6 +213,7 @@ const GrowiContextualSubNavigation = (props) => {
                   pageId={pageId}
                   revisionId={revisionId}
                   isLinkSharingDisabled={isLinkSharingDisabled}
+                  onClickTemplateMenuItem={templateMenuItemClickHandler}
                 />
               )}
               onClickDuplicateMenuItem={duplicateItemClickedHandler}
@@ -216,13 +232,21 @@ const GrowiContextualSubNavigation = (props) => {
             />
           )}
         </div>
+        {currentUser != null && (
+          <CreateTemplateModal
+            path={path}
+            isOpen={isPageTemplateModalShown}
+            onClose={() => setIsPageTempleteModalShown(false)}
+          />
+        )}
       </>
     );
   }, [
     pageId, revisionId, shareLinkId, editorMode, mutateEditorMode, isCompactMode,
-    isLinkSharingDisabled, isDeviceSmallerThanMd, isGuestUser, isSharedUser,
+    isLinkSharingDisabled, isDeviceSmallerThanMd, isGuestUser, isSharedUser, currentUser,
     isViewMode, isAbleToShowPageEditorModeManager, isAbleToShowPageManagement,
-    duplicateItemClickedHandler, renameItemClickedHandler, deleteItemClickedHandler, path,
+    duplicateItemClickedHandler, renameItemClickedHandler, deleteItemClickedHandler,
+    path, templateMenuItemClickHandler, isPageTemplateModalShown,
   ]);
 
 

+ 67 - 13
packages/app/src/components/Sidebar/PageTree/Item.tsx

@@ -10,7 +10,7 @@ import nodePath from 'path';
 
 import { pathUtils } from '@growi/core';
 
-import { toastWarning, toastError } from '~/client/util/apiNotification';
+import { toastWarning, toastError, toastSuccess } from '~/client/util/apiNotification';
 
 import { useSWRxPageChildren } from '~/stores/page-listing';
 import { IPageForPageDeleteModal } from '~/stores/ui';
@@ -19,7 +19,7 @@ import { apiv3Put } from '~/client/util/apiv3-client';
 import TriangleIcon from '~/components/Icons/TriangleIcon';
 import { bookmark, unbookmark } from '~/client/services/page-operation';
 import ClosableTextInput, { AlertInfo, AlertType } from '../../Common/ClosableTextInput';
-import { AsyncPageItemControl } from '../../Common/Dropdown/PageItemControl';
+import { PageItemControl } from '../../Common/Dropdown/PageItemControl';
 import { ItemNode } from './ItemNode';
 
 interface ItemProps {
@@ -79,24 +79,78 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
   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 { data, error } = useSWRxPageChildren(isOpen ? page._id : null);
+  const { data, mutate: mutateChildren } = useSWRxPageChildren(isOpen ? page._id : null);
 
-  const hasDescendants = (page.descendantCount != null && page?.descendantCount > 0);
+  // hasDescendants flag
+  const isChildrenLoaded = currentChildren?.length > 0;
+  const hasDescendants = (page.descendantCount != null && page?.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 [{ isDragging }, drag] = useDrag(() => ({
     type: 'PAGE_TREE',
     item: { page },
+    end: () => {
+      // in order to set d-none to dropped Item
+      setShouldHide(true);
+    },
     collect: monitor => ({
       isDragging: monitor.isDragging(),
     }),
   }));
 
-  const pageItemDropHandler = () => {
-    // TODO: hit an api to rename the page by 85175
-    // eslint-disable-next-line no-console
-    console.log('pageItem was droped!!');
+  const pageItemDropHandler = async(item, monitor) => {
+    if (page == null || page.path == null) {
+      return;
+    }
+
+    const { page: droppedPage } = item;
+
+    const pageTitle = nodePath.basename(droppedPage.path);
+    const newParentPath = page.path;
+    const newPagePath = nodePath.join(newParentPath, pageTitle);
+
+    try {
+      await apiv3Put('/pages/rename', {
+        pageId: droppedPage._id,
+        revisionId: droppedPage.revision,
+        newPagePath,
+        isRenameRedirect: false,
+        isRemainMetadata: false,
+      });
+
+      await mutateChildren();
+
+      // force open
+      setIsOpen(true);
+
+      toastSuccess('TODO: i18n Successfully moved pages.');
+    }
+    catch (err) {
+      // display the dropped item
+      displayDroppedItemByPageId(droppedPage._id);
+
+      if (err.code === 'operation__blocked') {
+        toastWarning('TODO: i18n You cannot move this page now.');
+      }
+      else {
+        toastError('TODO: i18n Something went wrong with moving page.');
+      }
+    }
   };
 
   const [{ isOver }, drop] = useDrop(() => ({
@@ -248,15 +302,15 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
    * When swr fetch succeeded
    */
   useEffect(() => {
-    if (isOpen && error == null && data != null) {
+    if (isOpen && data != null) {
       const newChildren = ItemNode.generateNodesFromPages(data.children);
       markTarget(newChildren, targetPathOrId);
       setCurrentChildren(newChildren);
     }
-  }, [data, error, isOpen, targetPathOrId]);
+  }, [data, isOpen, targetPathOrId]);
 
   return (
-    <div className={`grw-pagetree-item-container ${isOver ? 'grw-pagetree-is-over' : ''}`}>
+    <div id={`pagetree-item-${page._id}`} className={`grw-pagetree-item-container ${isOver ? 'grw-pagetree-is-over' : ''} ${shouldHide ? 'd-none' : ''}`}>
       <li
         ref={(c) => { drag(c); drop(c) }}
         className={`list-group-item list-group-item-action border-0 py-1 d-flex align-items-center ${page.isTarget ? 'grw-pagetree-is-target' : ''}`}
@@ -296,7 +350,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
           </div>
         )}
         <div className="grw-pagetree-control d-none">
-          <AsyncPageItemControl
+          <PageItemControl
             pageId={page._id}
             isEnableActions={isEnableActions}
             showBookmarkMenuItem
@@ -308,7 +362,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
             <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control p-0">
               <i className="icon-options fa fa-rotate-90 text-muted p-1"></i>
             </DropdownToggle>
-          </AsyncPageItemControl>
+          </PageItemControl>
           <button
             type="button"
             className="border-0 rounded btn-page-item-control p-0"

+ 2 - 1
packages/app/src/server/routes/apiv3/pages.js

@@ -641,7 +641,8 @@ module.exports = (crowi) => {
 
     const page = await Page.findByIdAndViewerToEdit(pageId, req.user, true);
 
-    if (page == null) {
+    const isEmptyAndNotRecursively = page?.isEmpty && isRecursively;
+    if (page == null || isEmptyAndNotRecursively) {
       res.code = 'Page is not found';
       logger.error('Failed to find the pages');
       return res.apiv3Err(new ErrorV3(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden'), 401);